Polymer 3.0 Preview — Building a mini card game

The longer title should be: Experiment with Polymer 3.0 Preview — Building a mini card game with Polymer + Typescript + Webpack.

Webpack and Typescript are not necessarily in Polymer but that’s the tools I familiar with and I like Typescript(probably you should give it a try)!

  • I have no prior experience using Polymer. I’m blogging this from a noob Polymer + normal Angular/Vue developer perspective.

Here is the demo and sourcode:

Why

Being a long time AAV developer (AngularJs, Angular, Vue), I did follow the Polymer news, but didn’t really take a closer look into it(for some reasons) , until last week — during Polymer Summit 2017, the Polymer team announces two big changes:

  1. Polymer is moving from Bower to NPM (with a twist, you need Yarn with a specific configuration).
  2. Polymer is switching to using ES6 modules instead of HTML Imports

Suddenly, I feel like “it’s time to try it out!” (I always feel the HTML import syntax looks weird). Probably the marketing slogan works too, #useThePlatform sound welcoming :)!

On a serious note, wouldn’t it be good if you can building performant component using the platform? (Polymer is not a framework. It’s a thin sugar layer to use the web-components)

After going through the Polymer 3.0 Preview hands-on published by the Polymer team, I found a few things that doesn’t look “normal” for a typical AAV developer (me).

  • import statement with .js and import directly from node_modules using relative path — It’s a minimalistic example. However, it’s not how we import in Angular, Vue and React “normally”. It’s the Bower way of import but it’s not how I normally work with modules.
  • HTML in JS — Personally, I prefer to separate the do HTML template unless the it’s so short (<10 lines). Also, with proper syntax highlight.

Therefore, I decide to build a Polymer 3.0 project from scratch with the way I used to.

The GIF

Project Setup

Let start with the project setup and the dependencies:

Dependencies

There are very little dependencies:-

  • @polymer/polymer@next — need for polymer
  • @webcomponents/webcomponentsjs — polyfills, need for polymer
  • lodash — useful utilities functions

Dev dependencies

Since we are using webpack and typescript, there are a couple of development dependencies

  • typescript
  • webpack
  • webpack-dev-server — starting a localhost with live reload
  • a few webpack loaders — ts-loader for process Typescript, html-loader to process HTML
  • a few webpack plugins

Folder structures

  • build — all webpack configurations here (development and production config are be different)
  • src — all source code here, with a few sub-folders: components, environments, utils.
  • static — all our assets here(e.g. images)
  • dist — the bundled code that we are going to upload to server, do not check in
folder structure in a glance

Important notes

  1. Add a new property "flat": true in your package.json file because Polymer needs it.
  2. I wouldn’t go through the Webpack settings lines by lines, you may head over to the files for details. We had two versions of Webpack config — development and production. The development version will start a dev server, do hot reload, while production should be generating minified files with hash names.
  3. My goal is to import the HTML file as string into our Typescript file and use that as template. Therefore we need “tell” Typescript that we can import HTML. (By default, ES6 module doesn’t support HTML. Therefore, we add the lines below in typescript definition typings.d.ts).

The above line means — for all source files that end with .html, import them as string.


Show me the Polymer code!

Hang on! Before we walk through the code, let’s take a quick look at our page. We’ll build 5 components in this project.

app component is the root component, and the other 4 are the child components as shown below.

Each component consists of 2 files:

  • index.ts — Our component logic will live here.
  • <name>.template.html — HTML and CSS will live here.

Alright, let’s dive into each of these components.

app component

Let’s look at the app component template, nothing too complicated. We use all 4 mentioned components here:

  1. Whenever user clicks on the reset button in top bar, top bar will fire an event. The parent (in this case, our app component) listens to the event and will call theresetGame handler function. Custom event in Polymer component always start with on-* and must be kebab case. When assigning a handler (e.g. resetGame), you can’t put resetGame(), it should be resetGame (in AAV we can do either).
  2. app component will keep track of the total game time. We’ll pass the time object to top message component for display purpose, the time is updated every second.
  3. The cards component accepts an array of 12 cards. app component listens to the on-card-flipped event. app component will start couting the game time when the first card is flipped.
  4. The cards component will also fire an event whenever all cards are matched. app component listens to that event and stop the game.
  5. <template is="dom-if" if="[[isGameCompleted]]"> is to the conditional for template (similar to Angular *ngIf or Vue v-if).
  6. pop up modal accept a time object to display game completion time. When user clicks on the reset button, it will fire an event. Again, in our case, the app component will listen to this event and reset the game.
  7. The data binding syntax[[]] in Polymer means one way binding. Read more about Polymer data binding here. (Dear Angular dev, don’t confuse it with []!).

Here is the TS code:-

The logic is not too complicated, so I wouldn’t bloat this article with all the logics. Few notes here:

  1. Custom element MUST be ES6 class. In our case, we extend our class from PolymerElement. In Angular, we use class and decorator @Component. In Vue, component is not necessarily to be a class, although it could be(I am using that).
  2. Look at the line we import dom-if. We must import it because we are using dom-if. You could import it into each component like what I did here, or create a shared file(e.g. vendor.ts), import it at top level if it’s going to be used throughout the project (might not a good practice though).
  3. The getter static get properties(). Those fields that you need to bind to the template, you need to place it here. The property declaration syntax is quite flexible. It can be simply{ user: String } , or { user: { type: String, value: 'jane' }} . Read further in Polymer documentation. Property can be computed properties, with observer, notify, readonly, etc.
  4. You might notice that I declared isGameCompleted both in the static properties getter and as the class properties, it is because we need strong typings. In Angular and Vue component class, we only need to declare once as the class property. Probably a custom decorator can be made to enhance this.
  5. Custom element has a few lifecycle events that you can hook on. Polymer added an additional one-time initialization event calledready, invoked the first time the element is added to the DOM. For full list of lifecycle hooks, read here.
  6. You may also notice that our shuffleCards method is marked as private, this is a Typescript feature.

top bar component

next, take a look at our top bar component.

2 interesting points here:

  1. Button on-tap event — Polymer provides mixin to handle gesture events. Therefore, instead of click, I use the tap event.
  2. Styling — same as Angular, the style is scoped by default. For example, if you have .container css class in two different elements, it would not impact each others. In Angular and Vue, we can choose whether or not to scope the style. Not sure if it’s configurable in Polymernot digging into how Polymer can do it yet.

There are 3 things to note here:

  1. The GestureEventListners mixin. In app component, we extend our component from PolymerElement, but this time, we extend it with mixin so we can handle the on-tap event.
  2. In the reset() function, you might be wondering what the heck is (this as any) if you are new to Typescript. It’s type casting.

Typescript is smart, it can or at least try to “guess” the properties and methods available in the class. In this case, when we call this.dispatchEvent , Typescript cannot find dispatchEvent in the class, so it will throw warning. Why Typescript can’t “guess” in this case? It’s because the parent class PolymerElement didn’t provide type information.

To get around this, there are a few ways, the best is of course Polymer has official type definition file that ship with the library, like Angular and Vue, or we write our own (and contribute it to @types), or simply bypass it by telling Typescript don’t inference (or it can be any type).

I did the later. (this as any) is for that purpose. You can do (<any>this) . alternately. Troublesome? Maybe. I saw a Github discussion on the Polymer definition file, maybe… soon? When it’s in, you wouldn’t need to do the above.

3. The last thing to note is the dispatchEvent. It’s similar to Angular’s eventEmitter and Vue this.$emit . You can fire a custom event with/without parameter. Other elements (e.g. our app component in this case) can listen to the event.

top message component

Top message component display to running time. time is a property passed in by parent component.

The input time is expect to be an object like something this:-

{ hour: 0, minute: 4, second: 15 }

The display format however, expect to be always 2 digits for each unit, e.g 00: 04: 15.

Let’s look at our template:

  1. What we do here is we convert our time object to displayTime object with correct padding.
  2. I wish I can do something like Pipe in Angular or filter in Vue with Polymer like this:
{{ time.hour | lpadTime }}: {{ time.minute | lpadTime }}: {{ time.second | lpadTime }}

No official solution for that. However, I did found a solution by Addy Osmani — polymer-filters, but I’m not sure how to use that or write that in Polymer 3.0 Preview.

So, here go our Typescript:

  1. Let’s jump straight into the properties getter, since the displayTime is a computed output, we can configured it with the computed property and assign a function to handle that. In our case, timeChanged function will be invoked whenever time changed. I prefer the way Angular and Vue ways on declaring a computed field. Simply with getter or Vue’s computed object.

pop up modal component

Our pop up modal component has nothing special. It’s sort of combination of what we code in top bar and top message.

It accept a time input to display the completion time, and it has a reset button that user can click.

Therefore, we’ll skip this and jump to our main component — cards!

cards component

Our main component.

The card component received an input array of cards and display it accordingly. User can tap on the card and flip it. If two flipped cards do not match, it will be fold back to cover.

The template is straight forward.

  1. We had some pretty long CSS to handle the stacking of the card value and the cover, the flip animation and scale nicely in different device sizes. You can check it out in the source code.
  2. We loop through the card by using the dom-repeat helper provided by the Polymer.
  3. When user taps on the card, it will trigger the flip function.
  4. Each of the card will has a CSS class card. We will also toggle flipped CSS class depends on the card's isFlipped or isMatched property. If the item is flipped or already matched, we will always show the avenger, else we’ll show the cover.

In Angular, I can do something like this:

<div class="card" [class.flipped]="item.isFlipped || item.isMatched">

Alternatively, I can do something like this in Vue:

<div :class="['card', (item.isFlipped || item.isMatched)? 'flipped' : '']">

However, in Polymer, I must do something like this:

<div class$="[[getClass(item)]]" on-tap="flip">

getClass is the function that will evaluate my item and return me the relevant css classes.

Let’s look at our Typescript file:

  1. Let’s look at line number 41 flip({ model }). Remember that the flip function is called by the card’s on-tap event. Normally we will flip function would look like flip(event). The event object has a lot of properties with we don’t need in our logic. What we want here is the event.model property. With ES6, we can utilize the destructing assignment feature. Therefore, we use flip({ model }) .
  2. However, where is the model came from? Turn out, when you dom-repeat a list. Polymer will automatically add a model property for you. model contains two properties: index and item which item is our card item. Read more about handling events in dom-repeat here.

Register our components

After we wrote all our custom elements, we need to register all of it. The normal way I find in Polymer tutorial(e.g. this tutorial) is at each of the component file, we have this line:

customElements.define('my-top-message', MyTopMessage)

or declare a staticis getter in every class to return the name string, so you can register like this later:

customElements.define(MyTopMessage.is, MyTopMessage)

Please take note that you must register the name as kebab-case , else it WON’T work. Probably I’ve been spoiled by Angular and Vue, we can register component as camelCase and the framework will handle that for us, everything it will be fine.

Besides, I found it a bit repetitive to repeat the above syntax repeat and repeat. Plus, in my case, these elements will be bundled and used together.

So I created an index.ts file in components folder, loop each of the elements and register it using lodash kebab case helper.

Probably not suitable for library distribution, but I am not distributing a library in this case.

Summary: Overall experience (Positive!)

It feel natural to use ES6 Module. I like it. The delta to pick up Polymer is not high if you have knowledge on Angular, React or Vue.

Here come the question:

Should we #useThePlatform (Polymer) or #buildOnUs (Angular)? (sorry, I didn’t know any Vue slogan or hashtag)

For your information, both Angular and Vue are compatible with custom elements, source from: https://custom-elements-everywhere.com/.

Custom elements

I like what the website stated:

Making sure frameworks and custom elements can be BFFs

It’s not an either one option, it can be mixed and match. And that, is something I would like to dive into as well.

Here are the full source code:

Thanks, Happy coding!

TL;DR;

Catchas

  • NPM or Yarn — I’m using Yarn in most of my projects so it’s not really an issue now. One of the reason we switch to Yarn is the package version(well, and the speed). With the the new NPM feature — package lock, I’m trying that to see if we can switch back to NPM.
  • The flat import effect — You will get a bunch of questions during installing package(e.g. webpack). It prompts you to select which version of dependencies you need. The immediate sentence pops in my mind is “How the hell I know?”, so I make the choice base on my luck. Probably instead of adding flat in *package.json*, can try outyarn install <package> --flat for those polymer related package while yarn add <package> for the rest? Gonna test this out.

To do

  • Make uglifyJS works, not sure if it’s related to this Github issue.
  • Test, test, test, no test now!
  • PWA-ify it. Make it Progressive Web Apps.
  • Material design it, exploring other available components (e.g. paper-button etc)
  • Split to individual style template.
  • Using preprocessors for HTML and CSS — Pug and SCSS.
  • Create Typescript decorators — something like Angular @Input and @Output.
  • Mix and match with Angular and Vue, since both are 100% compatible with custom elements(https://custom-elements-everywhere.com/).
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.