Polymer is a library that helps you create custom HTML elements. In this codelab, you'll build a web app with custom elements.
In this codelab, you're going to build a simple game in the form of a Web app. The app will:
|
To start the tutorial, you will need some software:
You'll also need to have some basic skills and knowledge:
That said, I assume you know about as much about web development as I do (very little!). This tutorial is for beginners.
Git is a version control tool.
git --version
If all is well, Git tells you its version info:
If you don't see a Git version number at this point, you may need to refer to the official Git installation instructions.
Node is a JavaScript runtime environment. npm is a package manager for Node. They will both be installed when you install Node.
npm install npm@latest -g
node -v
npm -v
If you don't see version numbers for Node and npm, you may need to refer to the official installation instructions on the npm website.
The Polymer CLI is what you'll use to generate your app from a template, view the working app locally, and package it up ready for deployment.
npm install -g polymer-cli
polymer --version
Congratulations! You've configured a development environment for Polymer apps.
$ git clone https://github.com/ComcastSamples/polymer-whose-flag.git
$ cd polymer-whose-flag
$ polymer init
whose-flag
and whose-flag-app
. Match flags to their countries!
? Overwrite package.json? (Ynaxdh)
type n
and press Enter.? Overwrite README.md? (Ynaxdh)
type Y
and press Enter.The Polymer team is aware of them, and those warnings won't prevent you from working on this Codelab.
ls
and have a look at the file structure Polymer CLI has set up for you.The Polymer CLI has generated the files and folders composing the basic structure of an app. Here's a short explanation of these files and folders:
File or folder | What's inside? |
| Your app has a set of dependencies - chunks of code that it depends on to work. These dependencies are managed with npm, a package manager, and stored in the |
| Describes your app's dependencies to the package manager, npm. |
| Your app's landing page. |
| Stores metadata for your app that helps browsers to load it efficiently. |
| README files usually describe how to install and use the app. Currently, yours contains some default content generated by the Polymer CLI. |
| Folder that stores the code we'll write for your app |
| A placeholder folder for automated tests. |
Let's take a look at src/whose-flag-app/whose-flag-app.js
. Open this file in a text editor.
import {html, PolymerElement} from '@polymer/polymer/polymer-element.js';
/**
* @customElement
* @polymer
*/
class WhoseFlagApp extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h2>Hello [[prop1]]!</h2>
`;
}
static get properties() {
return {
prop1: {
type: String,
value: 'whose-flag-app'
}
};
}
}
window.customElements.define('whose-flag-app', WhoseFlagApp);
Here are some important things to note:
return html`...`
block is where we define the visual elements of the page. We will work inside the return html`...`
block, adding interface elements to construct the app.<style>
block defines the look and feel of the interface elements in the <template>
block. This is where we will put CSS rules that define styles.class
block contains code that defines your app and provides its functionality. This is also where we'll add things like methods that get called when the user clicks a button.prop1
is a property. It is declared in the class definition for WhoseFlagApp
:static get properties() {
return {
prop1: {
type: String,
value: 'whose-flag-app'
}
};
}
This means you can use it in your HTML, like this:
<h2>Hello [[prop1]]!</h2>
The square brackets denote a Polymer feature called data binding. We'll learn more about this soon.
Cool - you made an app! It's that easy. The app doesn't do much yet, but let's take a look at it.
In the main folder for your app (the root polymer-whose-flag
folder), type:
polymer serve --open
The Polymer development server starts up, and the --open
flag automatically opens a browser window for you:
You can serve your app at any time to see how it looks.
In the following steps, you'll import and use interface elements from WebComponents.org to define the visual appearance of your app.
src/whose-flag-app
, open whose-flag-app.js
in a text editor. The first line in this file is a link to another file:import {html, PolymerElement} from '@polymer/polymer/polymer-element.js';
This is an ES Module Import. It is like a library - a JavaScript file that contains elements and functions that you can use to build other elements. This line is here because you need the html
and PolymerElement
functions from the Polymer library in order to do Polymer things.
import '@polymer/app-layout/app-layout.js';
This tells Polymer that you're going to use the app-layout
elements. Note that you don't need to import <app-toolbar>
itself; we're importing the whole collection of app-layout
elements.
whose-flag-app.js
:<h2>Hello [[prop1]]!</h2>
This is just placeholder text, and you can safely delete it. Replace it with this:
<app-header>
<app-toolbar>
<div main-title>Whose flag is this?</div>
</app-toolbar>
</app-header>
Points to note:
html
tagged template literal, just like you would with a standard <h1>
or <input>
element.<app-toolbar>
element looks for a main-title
div element in order to figure out how to format it.If you want a preview of your app at any time, you can run polymer serve --open
from the whose-flag
root project folder, using a command line. You can also leave the Polymer development server running--just remember to refresh your browser to see your changes.
Your app should look like this:
In the following steps, we will add the rest of the user interface.
<iron-image>
(which was preinstalled in your node_modules folder) by placing the following code after the existing import links in whose-flag-app.js
(remember, whose-flag-app.js
is in src/whose-flag-app
).import '@polymer/iron-image/iron-image.js';
whose-flag-app.js
, after the </app-header>
close tag:<div id="flag-image-container">
<iron-image
id="flag-image"
preload fade src="data/svg/BR.svg">
</iron-image>
</div>
<iron-image>
makes it easy to get your images to render nicely. The preload
attribute makes sure that the image does not render until the browser has fully loaded it. Until the image is fully loaded, the preload
attribute uses the image's background color (defined in CSS--we'll get to that shortly) as a placeholder.
The fade
attribute works with the preload attribute. When the browser has fully loaded the image, it renders the image with a nice fade effect.
Add three buttons to the simple app interface.
whose-flag-app.js
, import the <paper-button>
element:import '@polymer/paper-button/paper-button.js';
</iron-image>
tag. We'll put these buttons inside the <div id="flag-image-container"> ...</div>
block to make sure they align with the image.<div id="answer-button-container">
<paper-button id="optionA" class="answer">Brazil</paper-button>
<paper-button id="optionB" class="answer">Uruguay</paper-button>
</div>
<p>A message will go here, telling you if you got it right.</p>
<paper-button class="another" id="another">Another!</paper-button>
Your app should look like this:
At the moment, these buttons don't do anything. That's fine as you are just defining the interface of your app.
In the static get properties() {...}
block, locate the following code, and delete it:
prop1: {
type: String,
value: 'whose-flag-app'
}
This step won't change the appearance of your app.
Click here to see what your project should look like at the end of this step.
Let's give your app some style. You will use an existing color scheme found in the <paper-styles>
component, which provides an easy implementation of material design.
paper-styles
by adding the following code to the list of imports at the start of whose-flag-app.js
: import '@polymer/paper-styles/color.js';
<style></style>
block with the following code: <style>
:host {
display: block;
font-family: Roboto, Noto, sans-serif;
}
paper-button {
color: white;
}
paper-button.another {
background: var(--paper-blue-500);
width: 100%;
}
paper-button.another:hover {
background: var(--paper-light-blue-500);
}
paper-button.answer {
background: var(--paper-purple-500);
flex-grow: 1;
}
paper-button.answer:hover {
background: var(--paper-pink-500);
}
app-toolbar {
background-color: var(--paper-blue-500);
color: white;
margin: 20px 0;
}
iron-image {
border: solid;
width: 100%;
--iron-image-width: 100%;
background-color: white;
}
#flag-image-container {
max-width: 600px;
width: 100%;
margin: 0 auto;
}
#answer-button-container {
display: flex; /* or inline-flex */
flex-flow: row wrap;
justify-content:space-around;
}
</style>
html`...`
tagged template literal definition is scoped to that element. This means that it will only apply inside that element - the rest of the page won't know anything about it.:host
selector comes in. The :host
selector allows you to style the web component itself. In this case, you use it to define a font choice for the whole app.paper-button.another:hover {
background: var(--paper-light-blue-500);
}
--paper-light-blue-500
is a custom CSS property. You imported this custom CSS property with paper-styles
, so it is now available to use.
The code extract above takes --paper-light-blue-500
and assigns it to act as the value of the background color for paper-buttons
of class another
when they are being hovered over.
Remember that you can serve and test your app at any time by typing the following command from the root whose-flag
project folder:
polymer serve --open
The text of your buttons is currently hard-coded. You need to change this so that you can display the button text from a data source. To do this, you will bind the text of your buttons to properties of your whose-flag-app
element.
At the moment, whose-flag-app
has no properties:
static get properties() {
return {
};
}
whose-flag-app
: static get properties() {
return {
countryA: {
type: String,
value: "Brazil"
},
countryB: {
type: String,
value: "Uruguay"
}
};
}
Their values remain hard-coded for now. Now, you can bind the button text to these properties.
whose-flag-app.js
: <paper-button id="optionA" class="answer">Brazil</paper-button>
<paper-button id="optionB" class="answer">Uruguay</paper-button>
Replace the hard-coded button text with data bindings:
<paper-button id="optionA" class="answer">[[countryA]]</paper-button>
<paper-button id="optionB" class="answer">[[countryB]]</paper-button>
This syntax is called data binding. [[countryA]]
will be replaced by the value of this.countryA
from your element. [[countryB]]
will be replaced by the value of this.countryB
.
outputMessage
: static get properties() {
return {
countryA: {
type: String,
value: "Brazil"
},
countryB: {
type: String,
value: "Uruguay"
},
outputMessage: {
type: String,
value: ""
}
};
}
<p>A message will go here, telling you if you got it right.</p>
Replace it with a data binding:
<p>[[outputMessage]]</p>
properties
function, after the definition of outputMessage
, create two more properties: correctAnswer: {
type: String,
value: "Brazil"
},
userAnswer: {
type: String,
value: "Brazil"
}
Remember to add a comma between each property.
Now listen for, and handle, the event that is fired when the user selects an answer. When a user clicks on an HTML button, the button fires an event. A web developer can listen for that event and create a function that will run whenever that event is fired. This function is called an event listener.
WhoseFlagApp
:_selectAnswer(event) {
let clickedButton = event.target;
this.userAnswer = clickedButton.textContent;
if (this.userAnswer == this.correctAnswer) {
this.outputMessage = `${this.userAnswer} is correct!`;
}
else {
this.outputMessage = `Nope! The correct answer is ${this.correctAnswer} !`;
}
}
Here's where this function will sit inside your code:
<script>
...
class WhoseFlagApp extends Polymer.Element {
static get properties() {
return {
...
}
};
}
_selectAnswer(event) {
...
}
}
...
</script>
Now, make sure this function gets called.
on-click="_selectAnswer"
to the attributes of each answer button:<paper-button id="optionA" class="answer" on-click="_selectAnswer">[[countryA]]</paper-button>
<paper-button id="optionB" class="answer" on-click="_selectAnswer">[[countryB]]</paper-button>
Notes:
on-click="_selectAnswer"
syntax is special to Polymer. Polymer lets you declaratively specify a function to call when a button is clicked._selectAnswer
function is called, it automatically receives the event that called it as a parameter. Hence, you can write _selectAnswer(event)
and use the event
parameter to gain access to the details of that event.var clickedButton = event.target;
textContent
, like so: this.userAnswer = clickedButton.textContent;
Click here to see what your project code should look like now.
We'll add some code to load the country data from a file. First, have a look inside the whose-flag/data/
folder that you downloaded earlier. This folder contains:
countrycodes.json
which stores a list of country codes and the names of those countries.Here's an extract so you can see how the data is laid out:
{ "countrycodes":[
{
"code": "AF",
"name": "Afghanistan"
},
{
"code": "AL",
"name": "Albania"
},
{
"code": "DZ",
"name": "Algeria"
},...}]}
svg/
which contains images of each country's flag. The filenames for each image file are the same as their country code. We'll use this to display names and flags for the countries in the game.Instead of hardcoding the country names and flag image filename, you'll take these values from countrycodes.json
. Loading this file is simple--you can use a Polymer component called <iron-ajax>
.
<iron-ajax>
by adding the following line to the imports section in whose-flag-app.js
:whose-flag-app.js
import '@polymer/iron-ajax/iron-ajax.js';
Now we can use <iron-ajax> to load the file.
<app-header>
<app-toolbar>
<div main-title>Whose flag is this?</div>
</app-toolbar>
</app-header>
<div id="flag-image-container">
<iron-image
id="flag-image"
preload fade src="data/svg/BR.svg">
</iron-image>
</app-header>
and <div id="flag-image-container">
:whose-flag-app.js
<iron-ajax
auto
url="data/countrycodes.json"
handle-as="json"
on-response="_handleResponse"></iron-ajax>
Your code should look like this:
whose-flag-app.js
<app-header>
<app-toolbar>
<div main-title>Whose flag is this?</div>
</app-toolbar>
</app-header>
<iron-ajax
auto
url="data/countrycodes.json"
handle-as="json"
on-response="_handleResponse"></iron-ajax>
<div id="flag-image-container">
<iron-image
id="flag-image"
preload fade src="data/svg/BR.svg">
</iron-image>
Let's look at what this code does:
auto
is a setting that makes <iron-ajax>
load automatically when its URL or parameters change.url
specifies where <iron-ajax>
will load from. handle-as
tells <iron-ajax>
to treat the loaded file as json. on-response
specifies the function to be called (_handleResponse
) when <iron-ajax>
receives a response - that is, when it has loaded the file. This type of function is known as an event listener. We will add the event listener, _handleResponse
, to the scripts inside the WhoseFlagApp class definition. Before we do so, we need to add a property that it will use.
properties
function, add a property called countryList
:whose-flag-app.js
static get properties() {
return {
countryA: {
type: String,
value: "Brazil"
},
countryB: {
type: String,
value: "Uruguay"
},
outputMessage: {
type: String,
value: ""
},
correctAnswer: {
type: String,
value: "Brazil"
},
userAnswer: {
type: String
},
countryList: {
type: Object
}
};
}
Now, we'll create the _handleResponse
method. This method will do three things:
countryList
.WhoseFlagApp
:_handleResponse(event) {
this.countryList = event.detail.response.countrycodes;
this.countryA = this.countryList[0];
this.countryB = this.countryList[1];
this.correctAnswer = this.countryA;
}
The data inside countrycodes.json
is made up of objects with key-value pairs. Each object has a code and a name. this.countryA
and this.countryB
each contain one of these objects. For example:
{
"code": "AF",
"name": "Afghanistan"
}
You can reference the items within the objects. For example, countryA.code
is the string "AF"
and countryA.name
is the string "Afghanistan"
.
You will now change your code to use this data structure.
whose-flag-app.js
<div id="answer-button-container">
<paper-button class="answer" id="optionA" on-click="_selectAnswer">[[countryA]]</paper-button>
<paper-button class="answer" id="optionB" on-click="_selectAnswer">[[countryB]]</paper-button>
</div>
Replace it with the following code:
whose-flag-app.js
<div id="answer-button-container">
<paper-button class="answer" id="optionA" on-click="_selectAnswer">[[countryA.name]]</paper-button>
<paper-button class="answer" id="optionB" on-click="_selectAnswer">[[countryB.name]]</paper-button>
</div>
Now that we have assigned values from the data file to the country options, we can delete the hard-coded values in the properties function.
static get properties() {
return {
countryA: {
type: Object
},
countryB: {
type: Object
},
outputMessage: {
type: String,
value: ""
},
correctAnswer: {
type: Object
},
userAnswer: {
type: String
},
countryList: {
type: Object
}
};
Note: You will be changing any property that holds an item from countrycodes.json
to be of type Object
instead of type String
. (userAnswer
and outputMessage
don't change type, by the way! userAnswer
is the textContent
of the button the user clicks, while outputMessage
is the confirmation message telling the user whether they answered correctly. They are still String
s.)
correctAnswer
is now an Object
with name
and code
fields, instead of a String
, update _selectAnswer
to accommodate this: _selectAnswer(event) {
let clickedButton = event.target;
this.userAnswer = clickedButton.textContent;
if (this.userAnswer == this.correctAnswer.name) {
this.outputMessage = `${this.userAnswer} is correct!`;
}
else {
this.outputMessage = `Nope! The correct answer is ${this.correctAnswer.name}!`;
}
}
countrycodes.json
. Find the following code:<iron-image
id="flag-image"
preload fade src="data/svg/BR.svg">
</iron-image>
Change it to the following code:
whose-flag-app.js
<iron-image
id="flag-image"
preload fade src="data/svg/[[correctAnswer.code]].svg">
</iron-image>
In this step, we make the game dynamic and playable.
Here's the existing code for selecting the countries to be displayed:
_handleResponse(event) {
this.countryList = event.detail.response.countrycodes;
this.countryA = this.countryList[0];
this.countryB = this.countryList[1];
this.correctAnswer = this.countryA;
}
We'll write a function, __getRandomCountry
, that selects a random country from the array of countries(this.countryList)
. We'll use this function to randomize the options the user is presented with.
The JavaScript math function, Math.random()
, will help with this. Math.random
returns a random number between 0 and 1, for example, 0.30201092490461323
or 0.06303133640534542
.
To get a random integer that we can use as an array index, we can multiply the random number by the length of the array and extract the integer part using Math.floor
:
__getRandomCountry() {
return Math.floor(Math.random() * (this.countryList.length));
}
whose-flag-app.js
, inside the class definition for WhoseFlagApp:
...
class WhoseFlagApp extends Polymer.Element {
...
_handleResponse(event) {
this.countryList = event.detail.response.countrycodes;
this.countryA = this.countryList[0];
this.countryB = this.countryList[1];
this.correctAnswer = this.countryA;
}
__getRandomCountry() {
return Math.floor(Math.random() * (this.countryList.length));
}
}
...
__getRandomCountry
, instead of hard-coded integers, as an array index for this.countryList
:this.countryA = this.countryList[this.__getRandomCountry()];
this.countryB = this.countryList[this.__getRandomCountry()];
Modify your code to make sure that countryA
and countryB
are always different from each other.
this.countryA
and this.countryB
in a while loop: _handleResponse(event) {
this.countryList = event.detail.response.countrycodes;
while (!this.countryA || !this.countryB || (this.countryA.code == this.countryB.code)){
this.countryA = this.countryList[this.__getRandomCountry()];
this.countryB = this.countryList[this.__getRandomCountry()];
}
this.correctAnswer = this.countryA;
}
Now we'll randomize which button's answer is correct.
__handleResponse
function, after this.correctAnswer = this.countryA;
:let coin = (Math.floor(Math.random() * 2));
this.correctAnswer = coin == 1 ? this.countryA : this.countryB;
Now that your app is capable of generating random questions, give the user an option to play again!
WhoseFlagApp
: _restart() {
window.location.reload();
}
_restart
function to the Another! button:<paper-button class="another" id="another" on-click="_restart">Another!</paper-button>
:focus
styles/data/svg/.svg
For answers to these, view the commits to the steps
branch of github.com/ComcastSamples/polymer-whose-flag
Now you'll generate a build of your app. You'll package up the app into a format that you can push to an external-facing web server.
polymer.json
. Save it in the root whose-flag
project folder.polymer.json
, and save the file:{
"sources": [
"data/*",
"data/svg/*"
]
}
This makes sure that your app's data and images are included in the build.
Note: If you already have a polymer.json
file in your project folder, delete its contents and replace them with the code sample above.
polymer build
./whose-flag/
folder, type ls
to see the changes:You'll notice the build
folder. This folder contains the default
folder, which in turn contains all of the files that your app needs in order to function "in the wild" on a web server. This is the stuff that you will deploy.
Before you can deploy the build, you need to set up a place to host it. You can use any web server to deploy a Polymer app, but we're going to use Firebase Hosting, a web app hosting platform.
Setting up app hosting is a once-off step. In future, deploying your app will be even simpler!
Firebase is an app hosting tool that you'll use to deploy your app on the Internet.
npm install -g firebase-tools
firebase --version
whose-flag
, but Firebase may add some random numbers to yours in order to make it unique.Take note of this project ID - you'll need it when you configure your app.
whose-flag
folder, type:firebase login firebase init
Question | Answer |
|
|
|
|
|
|
|
|
|
|
Done! Firebase has initialized a project for you. Let's look at the changes this command has made to your local app folder structure.
/whose-flag/
folder, type ls -a
(the -a
flag shows hidden files).File or folder | What's inside? |
.firebaserc | Stores the name of your Firebase project. |
database.rules.json | Stores the rules that define how Firebase should structure and control interaction with your app data. |
firebase.json | Records some of the configuration decisions you just made. Importantly, this file stores the location of the built app (build/default) - the stuff that gets uploaded. |
Now, you need to deploy the build.
/whose-flag/
folder, type firebase deploy
.