I Choose You Polymer

Lately I’ve been working on a web application using API data from one of my favourite websites, Untappd. Even though the API is not available to everyone, I wanted to create a way which allows others with a key to reuse components of my application. After doing some research I came to the decision to use an open-source Javascript library by Google called Polymer. It’s specifically designed for building web applications, and offers a number of pre-built components. The cherry on top is that Polymer works really well with service workers, another API I’ve been excited to try.

Laying the Foundation

For a typical Polymer element all markup, JavaScript, and stylesheets are placed within a single html file. This is so it’s easy to place your element into another project without hassle. You can get a good starting point for making your own custom elements using the Polymer seed-element.

For a standard Polymer element, the html file will look something like this:

<!-- Imports -->
<link rel="import" href="../polymer/polymer.html">

<!-- Begin the dom module -->
<dom-module id="my-element">
  <template>
    <!-- Inline Stylesheet -->
    <style>
      :host {
        display: block;
        box-sizing: border-box;
      }
      h1 {
        font-color: #000;
      }
    </style>

    <!-- Markup -->
    <h1>&lt;seed-element&gt;</h1>
  </template>

  <!-- Javascript -->
  <script>
    Polymer({
      is: 'my-element',
      properties: {
        title: Boolean
      }
  </script>
</dom-module>
<!-- End the dom module -->

For my case I wanted to create an element which displays a list of check-ins and some profile data from Untappd. This requires making an API call, fortunately Polymer has an element called iron-ajax which makes this simple. Using Bower you’re able to add project dependencies to your custom elements, in my case I added a dependency for the iron-ajax element plus a few more to style my content.

"dependencies": {
  "polymer": "Polymer/polymer#^1.2.0",
  "iron-ajax": "PolymerElements/iron-ajax#^1.2.0",
  "iron-flex-layout": "PolymerElements/iron-flex-layout#^1.3.1",
  "paper-styles": "PolymerElements/paper-styles#^1.1.4",
  "paper-button": "PolymerElements/paper-button#^1.0.12",
  "paper-input": "PolymerElements/paper-input#^1.1.13"
},
"devDependencies": {
  "iron-component-page": "PolymerElements/iron-component-page#^1.0.0",
  "web-component-tester": "^4.0.0"
}

Once you have your Bower file setup correctly the elements need to be imported into your html file.

<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
<link rel="import" href="../bower_components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../bower_components/paper-styles/typography.html">
<link rel="import" href="../bower_components/paper-styles/shadow.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">

Creating the API call is simple with the iron-ajax element. All you need to do is use the <iron-ajax></iron-ajax> tag.

<iron-ajax
    auto
    url="https://api.untappd.com/v4/user/checkins/[[username]]?client_id=[[clientid]]&client_secret=[[clientsecret]]"
    handle-as="json"
    last-response="">
</iron-ajax>

You’re then able to handle the API responses using {{response}}. The JSON I’m querying is stored within an array with each object representing an individual check-in. The equivalent of a for loop with Polymer is the dom-repeat template tag. In the example below it’s looking for the object nested within {{response.checkins.item}} and then assigning the dom structure to every element it finds within the array. Each item within the template tag is then handled with {{item}}.

<template is="dom-repeat" items="{{response.checkins.items}}">
  <div class="vertical-section-container">
    <div class="vertical-section checkin-card">
      <div class="flex-horizontal">
        <div class="label-container">
          <img class="beer-label" src="{{item.beer.beer_label}}">
        </div>
        <div class="flexchild centered">
          <h2>{{item.beer.beer_name}}</h2>
          <h3>by {{item.brewery.brewery_name}}</h3>
          <p>{{item.checkin_comment}}</p>
          <a href="https://untappd.com/user/[[username]]/checkin/{{item.checkin_id}}" tabindex="-1">
            <paper-button raised>View on Untappd</paper-button>
          </a>
        </div>
      </div>
    </div>
  </div>
</template>

If the content you want to display is not stored within an array, or if you want to just grab a certain item in the index you’d use something like this:

<div class="vertical-section-container">
  <div class="vertical-section checkin-card">
    <div class="flex-horizontal">
      <div class="label-container">
        <img class="profile-label" src="{{response.checkins.items.0.user.user_avatar}}" />
      </div>
      <div class="flexchild centered">
        <h1>{{response.checkins.items.0.user.first_name}} {{response.checkins.items.0.user.last_name}}</h1>
        <p>{{response.checkins.items.0.user.bio}}</p>
        <a href="https://untappd.com/user/[[username]]" tabindex="-1">
          <paper-button raised>Follow on Untappd</paper-button>
        </a>
      </div>
    </div>
  </div>
</div>

Element Properties

You’ll probably have noticed by now that certain things are stored within square brackets, such as [[username]]. These are data binds that are assigned within the properties object.

<script>
  Polymer({
    is: 'untappd-data',

    properties: {

      // Stores the username, and notifies of changes.
      username: {
        type: String,
        value: '',
        notify: true
      },

      // Stores the API Client ID.
      clientid: {
        type: String,
        value: ''
      },

      // Stores the API Client Secret.
      clientsecret: {
        type: String,
        value: ''
      },

      // Toggles the profile block on/off.
      profile: {
        type: Boolean,
        value: false
      },

      // Toggles the search block on/off.
      search: {
        type: Boolean,
        value: false
      }
    }
  });
</script>

In my case I have setup a two-way data-bind which I’m using for an input box. Because I have notify: true toggled on for the username property it will result in any attached events to re-fire if its data is changed. For this case I’ve setup an input box using the Polymer paper-input element, and have set the value to the username property which looks for changes.

<iron-ajax
    auto
    url="https://api.untappd.com/v4/user/checkins/[[username]]?client_id=[[clientid]]&client_secret=[[clientsecret]]"
    handle-as="json"
    last-response="{{response}}">
</iron-ajax>

<div class="vertical-section-container">
  <div class="vertical-section">
    <paper-input label="Untappd Username" value="{{username::change}}"></paper-input>
  </div>
</div>

With the two-way binding setup, it will now result in the API being recalled whenever a new username is placed within the input box. This now allows people to search for other users and have the results populate without reloading the page.

Search Block

Properties are also ways you can give customization options to your element. Because you need a key to query the Untappd API I’ve created a property so users can then pass their API details through the elements markup, and because the username property already exists you can also pass in a default username to display when the page first loads.

Keep in mind that exposing your API key/secret like this is often not a good idea. This was setup purely for demonstration purposes.

<untappd-data username="untappd_username"
clientid="client_id"
clientsecret="client_secret">
</untappd-data>

Service Workers

Polymer works great with the service worker API, and even includes a number of pre-built tools such as the platinum-sw element which simplifies the process. An example implementation of this would be like so:

<paper-toast id="caching-complete"
             duration="6000"
             text="Caching complete! This app will work offline.">
</paper-toast>

<platinum-sw-register auto-register
                      clients-claim
                      skip-waiting
                      base-uri="bower_components/platinum-sw/bootstrap"
                      on-service-worker-installed="displayInstalledToast">
  <platinum-sw-cache default-cache-strategy="fastest"
                     cache-config-file="cache-config.json">
  </platinum-sw-cache>
</platinum-sw-register>

There’s a lot going on here, and it’s recommended that you read the full platinum-sw documentation so you can fully understand all of the components of this element. Within the site’s Javascript there’s also code which triggers the install toast if the service worker is not disabled.

app.displayInstalledToast = function() {
  if (!Polymer.dom(document).querySelector('platinum-sw-cache').disabled) {
    Polymer.dom(document).querySelector('#caching-complete').show();
  }
};

The boilerplate service worker code I utilized was found in the Polymer Yeoman generator, and the service worker toolbox.

The biggest hurdle I had to overcome was with dynamic content. While everything else on the page worked, whenever you viewed it in offline mode you’d get this:

Broken Offline

After some digging it turns out that the service worker was taking control of the page after the AJAX request was made, resulting in everything being cached but the actual response. Fortunately there’s a property you can add to the platinum-sw-register element called reload-on-install which reloads the page once the service worker has taken control, forcing the AJAX content to be cached. There’s other methods that can be used such as delaying the AJAX request, however reload-on-install was the easiest fix for this example. Now when you visit the page while offline you get this:

Working Offline

Conclusion & Additional Reading

If you’d like to take a look at the code for my custom element you can find it on Github. If you’d like to get started with Polymer yourself, I found the following articles and videos to be really helpful.