Polymer is a library that helps you create custom HTML elements. In this codelab, you'll build a web app with custom elements.

What you will build

In this codelab, you're going to build a simple game in the form of a Web app. The app will:

  • Present a flag to the user
  • Invite the user to guess which country the flag belongs to
  • Tell the user whether they got the answer correct or incorrect
  • Let the user click a button to show them another flag to identify

What you'll learn

What you'll need

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.

Install Git

Git is a version control tool.

Download the Git Installer

  1. Run the Git installer.
  2. Check whether Git is correctly installed: 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.

Install Node and npm

Node is a JavaScript runtime environment. npm is a package manager for Node. They will both be installed when you install Node.

Download the Node installer

  1. Download and run the Node installer (this will install npm as well).
  2. Update npm to the latest version: npm install npm@latest -g
  3. Check that Node and npm are correctly installed. Run the following commands:

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.

Install the Polymer CLI

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.

  1. Use npm to install the Polymer CLI: npm install -g polymer-cli
  2. Check that the Polymer CLI installed correctly: polymer --version

Congratulations! You've configured a development environment for Polymer apps.

Clone the project & Install dependencies

  1. In your terminal, from the folder you hold your projects in, clone the Build a Polymer 3.0 App From Scratch Codelab
    $ git clone https://github.com/ComcastSamples/polymer-whose-flag.git
  2. Change into the directory you just cloned
    $ cd polymer-whose-flag
  3. Initialize your Polymer app
    $ polymer init
  4. When prompted to choose the type of project you want to create, select Polymer 3.0 Application.

  1. Press Enter to accept the default application name and main element name supplied by the Polymer CLI: whose-flag and whose-flag-app.
  2. When prompted for a description of your app, type: Match flags to their countries!
  3. Press Enter. When prompted with
    ? Overwrite package.json? (Ynaxdh)
    type n and press Enter.
  1. When prompted with
    ? Overwrite README.md? (Ynaxdh) 
    type Y and press Enter.
  2. The Polymer CLI creates the scaffolding for your app. You may see warnings such as these:

    The Polymer team is aware of them, and those warnings won't prevent you from working on this Codelab.

  1. Type ls and have a look at the file structure Polymer CLI has set up for you.

What is all this stuff?

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?

node_modules

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 node_modules folder.

package.json

Describes your app's dependencies to the package manager, npm.

index.html

Your app's landing page.

manifest.json

Stores metadata for your app that helps browsers to load it efficiently.

README.md

README files usually describe how to install and use the app. Currently, yours contains some default content generated by the Polymer CLI.

src/

Folder that stores the code we'll write for your app

test/

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.

whose-flag-app.js

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:

whose-flag-app.js

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.

Install the toolbar

Use the toolbar

  1. From 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:

whose-flag-app.js

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.

  1. Add the following line under the existing ES Module Import:

whose-flag-app.js

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.

  1. Locate the following text in whose-flag-app.js:

whose-flag-app.js

<h2>Hello [[prop1]]!</h2>

This is just placeholder text, and you can safely delete it. Replace it with this:

whose-flag-app.js

<app-header>
   <app-toolbar>
     <div main-title>Whose flag is this?</div>
   </app-toolbar>
 </app-header>

Points to note:

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.

Add an image

  1. Import <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';
  1. Add the following code to whose-flag-app.js, after the </app-header> close tag:

whose-flag-app.js

<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 buttons

Add three buttons to the simple app interface.

  1. In whose-flag-app.js, import the <paper-button> element:

whose-flag-app.js

import '@polymer/paper-button/paper-button.js';
  1. Insert the following code straight after the closing </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.

Delete placeholder code

In the static get properties() {...} block, locate the following code, and delete it:

whose-flag-app.js

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.

Here's a demo of the app at this stage.

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.

  1. Import the color scheme inside paper-styles by adding the following code to the list of imports at the start of whose-flag-app.js:

whose-flag-app.js

import '@polymer/paper-styles/color.js';
  1. Replace the existing <style></style> block with the following code:

whose-flag-app.js

    <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>
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

Go here to see what your project code should look like now.

Here's a demo of the app at this stage.

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:

whose-flag-app.js

  static get properties() {
    return {
    };
  }
  1. Add two properties to whose-flag-app:

whose-flag-app.js

  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.

  1. Find the following lines in whose-flag-app.js:

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:

whose-flag-app.js

  <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.

  1. Create another property called outputMessage:

whose-flag-app.js

    static get properties() {
    return {
      countryA: {
        type: String,
        value: "Brazil"
      },
      countryB: {
        type: String,
        value: "Uruguay"
      },
      outputMessage: {
        type: String,
        value: ""
      }
    };
  }
  1. Locate the following placeholder text:

whose-flag-app.js

  <p>A message will go here, telling you if you got it right.</p>

Replace it with a data binding:

whose-flag-app.js

  <p>[[outputMessage]]</p>
  1. Add a property to hold the correct answer, and a property to hold the user's answer. In the properties function, after the definition of outputMessage, create two more properties:

whose-flag-app.js

  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.

  1. Add the following function to the class definition of WhoseFlagApp:

whose-flag-app.js

_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.

  1. Add on-click="_selectAnswer" to the attributes of each answer button:

whose-flag-app.js

<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:

Click here to see what your project code should look like now.

Here's a demo of the app at this stage.

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:

Here's an extract so you can see how the data is laid out:

countrycodes.json

{ "countrycodes":[
{
 "code": "AF",
 "name": "Afghanistan"
},
{
 "code": "AL",
 "name": "Albania"
},
{
 "code": "DZ",
 "name": "Algeria"
},...}]}

Use <iron-ajax> to load countrycodes.json

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>.

  1. Import <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.

  1. Locate the following piece of code inside whose-flag-app.js:

whose-flag-app.js

    <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>
  1. Insert the following block of code between </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:

Create 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.

  1. Inside the 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:

  1. Place the following code inside the class definition for WhoseFlagApp:

whose-flag-app.js

_handleResponse(event) {
  this.countryList = event.detail.response.countrycodes;
  this.countryA = this.countryList[0];
  this.countryB = this.countryList[1];
  this.correctAnswer = this.countryA;
}

Use the data loaded from countrycodes.json

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:

countrycodes.json

{
 "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.

  1. Find the following code:

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.

  1. Change the properties section of your code to look like this:

whose-flag-app.js

     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 Strings.)

  1. Since correctAnswer is now an Object with name and code fields, instead of a String, update _selectAnswer to accommodate this:

whose-flag-app.js

     _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}!`;
       }
     }
  1. Finally, replace the hard-coded filename with a reference to the data loaded from countrycodes.json. Find the following code:

whose-flag-app.js

<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>

Click here to see what your project should look like now.

Here's a demo of the app at this stage.

In this step, we make the game dynamic and playable.

Here's the existing code for selecting the countries to be displayed:

whose-flag-app.js

_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));
}
  1. Place the new function in whose-flag-app.js, inside the class definition for WhoseFlagApp:

whose-flag-app.js

...

    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));
      }
    }
   ...
  1. Use __getRandomCountry, instead of hard-coded integers, as an array index for this.countryList:

whose-flag-app.js

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.

  1. Enclose the code for assigning values to 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.

  1. Add the following code to the __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!

  1. Add the following function to the class definition for WhoseFlagApp:
_restart() {
  window.location.reload();
}
  1. Hook up the _restart function to the Another! button:
<paper-button class="another" id="another" on-click="_restart">Another!</paper-button>

Click here to see what your project should look like now.

Here's a demo of the finished app!

Here are some bonus challenges for you to try:

  1. Add some :focus styles
  2. Fix the 404 for /data/svg/.svg
  3. Have the “ANOTHER!” button NOT reload the page
  4. Write Tests
  5. Make the webapp a Progressive Web App

For answers to these, view the commits to the steps branch of github.com/ComcastSamples/polymer-whose-flag

Build the app for production

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.

  1. With a text editor, create a new file called polymer.json. Save it in the root whose-flag project folder.
  2. Paste the following text into 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.

  1. Type polymer build.
  2. In the main /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.

Deploy your app with Firebase

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!

Install Firebase tools

Firebase is an app hosting tool that you'll use to deploy your app on the Internet.

  1. Use npm to install the Firebase command line tools:

npm install -g firebase-tools

  1. To check that the Firebase tools installed correctly:

firebase --version

Set up app hosting with Firebase

  1. Click here to sign up for a Firebase account. If you have gmail, this is as simple as signing in to Firebase with your gmail address.
  2. Once you've signed in to Firebase, from the Firebase Console, click Create a project.
  3. Give your project a name, then click Create Project.
  4. Firebase assigns your project an ID and redirects you to an overview of your project. In the URL, you'll see the ID your project has been assigned. Mine is 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.

  1. From the root whose-flag folder, type:
firebase login
firebase init
  1. Firebase asks you which CLI features to set up. Make sure Firebase Hosting is selected. (Use the arrow keys to navigate the menu, and the Space bar to select or deselect an option.)

  1. Press Enter.
  2. Select the project ID of the project you created in the Firebase Console, and press Enter.
  3. Firebase asks you a set of questions about your app setup. Here's how to answer them:

Question

Answer

What file should be used for Database Rules?

database.rules.json

Do you want to install dependencies with npm now?

Y

What do you want to use as your public directory?

build/default

Configure as a single-page app (rewrite all urls to /index.html)?

N

File build/default/index.html already exists. Overwrite?

N

Done! Firebase has initialized a project for you. Let's look at the changes this command has made to your local app folder structure.

  1. In the /whose-flag/ folder, type ls -a (the -a flag shows hidden files).

What are these new files and folders?

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.

Deploy the build to your hosted Firebase project

Now, you need to deploy the build.

  1. From the /whose-flag/ folder, type firebase deploy.

  1. Visit the URL in the firebase deploy output. The URL will be based on the project ID that Firebase gave you in Step 4. For example, mine is this: https://whose-flag.firebaseapp.com