Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
561 lines (418 sloc) 14.6 KB
layout title description
post
The future of Polymer & lit-html
A summary of the future of Polymer and a path to follow going ahead.

Polymer & Lit Element

The future of Polymer is something many people have been wondering about since the introduction of version 3, and the creation of lit-html.

On one of our largest projects, I started to (slowly) migrate from 1.x to 2.x around the same time the Polymer team were working on building 3.x and lit-html.

Polymer 3.x and lit-element seem to achieve the same goal in different ways.

So where do we go after Polymer 2.x?

For a while, this question had no clear answer. However, it seems we now have a rough idea:

  • If coming from Polymer 2, Polymer 3 is something you should upgrade to, as it can be mostly automated through the modulizer
  • If starting from new, you should use lit-element
  • If you want to go all in, do a full conversion and drink buckets of coffee (like I may have done), go straight to lit-element

It looks to me like Polymer 3 will be the last Polymer standalone library. Going forward, it will likely be split into a "legacy" (PolymerElement) and a "core", which is the core library without the opinionated element classes.

What I figured I'll do with this post is summarise the basic migration from Polymer to Lit.

Update (August 2018)

Since the writing of this post, LitElement removed its dependency on Polymer completely. It also introduced a lot of the functionality I had previously written about the lack of.

This post has now been updated.

For new-comers to Web Components

This post is mostly aimed at helping developers who already use Polymer gain an understanding of the path it will take in future.

However, if you're new to it, I still recommend reading some of this and the lit-html website to see how it all works.

These days, web components have wide support in all major browsers and I would recommend them often over using a full blown framework.

Defining an element

Polymer 2.x

<dom-module id="my-polymer-element">
  <template>My element!</template>
  <script>
    class MyPolymerElement extends Polymer.Element {
      static get is() { return 'my-polymer-element'; }
    }
    customElements.define('my-polymer-element', MyPolymerElement);
  </script>
</dom-module>

Polymer 3.x

import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';

class MyPolymerElement extends PolymerElement {
  static get is() { return 'my-polymer-element'; }
  static get template() {
    return html`My element!`;
  }
}

customElements.define('my-polymer-element', MyPolymerElement);

LitElement

import {LitElement, html} from '@polymer/lit-element';

class MyLitElement extends LitElement {
  render() {
    return html`My element!`;
  }
}

customElements.define('my-lit-element', MyLitElement);

As you can see, there's not much difference between Polymer 3 and Lit.

The huge difference between Polymer 2 and Lit is the loss of dom-module. Templates are now defined in JavaScript... which is a kind of love or hate situation.

There is a proposal to allow these templates to come from HTML files, for those who want it separate.

Bindings

All bindings in Lit are native JavaScript expressions unlike Polymer's custom HTML syntax.

Here's a quick summary of how bindings have changed:

Polymer Lit
[[foo]] ${this.foo}
{% raw %}{{foo}}{% endraw %} ${this.foo} with an event listener (e.g. input on inputs)
<my-el prop="[[foo]]"> <my-el .prop=${this.foo}>
<my-el prop$="[[foo]]"> <my-el prop=${this.foo}>
<input checked="[[checked]]"> <input ?checked=${this.checked}>
<button on-click="_onClick"> <button @click=${(e) => this._onClick(e)}>

There's a nice summary here about the different syntaxes used.

Two-way bindings

Bindings are one-way in Lit, seeing as they are simply passed down in JavaScript expressions. There is no concept of two-way binding or pushing new values upwards in the tree.

Instead of two-way bindings, you can now make use of events or a state store.

This can be very simple to implement if an event already exists, such as with an input element:

render() {
  return html`<input type="text" @input=${(e) => this._onInput(e)}>`;
}

_onInput(e) {
  this._value = e.currentTarget.value;
}

As you can see, we simply use the native input event and update our property to the value.

Helper Elements

Polymer has a few helper elements to provide a solution to very common concepts/functionality.

Instead of these DOM elements, you can simply implement the same logic by taking advantage of JavaScript's features natively, using conditionals and flow control like you would in any other part of your code.

dom-repeat

dom-repeat becomes unnecessary as we can simply map an array when rendering in Lit.

Polymer

Item List:
<dom-repeat items="[[foo]]">
  <template>
    <span>[[item.prop]]</span>
  </template>
</dom-repeat>

Lit

const items = this.foo.map(item => html`<span>${item.prop}</span>`);

return html`
  Item List:
  ${items}
`;

dom-if

Similarly, a dom-if is as simple as a ternary or a separate condition in the render method.

Polymer

<template is="dom-if" if="[[condition]]">
  <span>Condition was truthy</span>
</template>
<template is="dom-if" if="[[!condition]]">
  <span>Condition was falsy</span>
</template>

Lit

return html`<span>Condition was ${this.condition ? 'truthy' : 'falsy'}</span>`;

Custom Styles

Custom styles existed to allow sharing of styles between elements:

<dom-module id="my-custom-style">
  <template>
    <style>* { color: hotpink; }</style>
  </template>
</dom-module>

<!-- meanwhile, elsewhere... -->

<style include="my-custom-style">
/* ... */
</style>

With Lit, we no longer have such a concept as a style can simply be shared through interpolation:

render() {
  const sharedStyles = getSharedStyles(); // Could come from a file, wherever.
  return html`
    <style>${sharedStyles}</style>
    <span>My element!</span>`;
}

As you can see, the CSS you template in could come from wherever you like. So there is no longer a need for a module and what not.

Properties

Now the fun part! Properties...

Properties in Lit are configured in a very similar fashion to Polymer. They define their type and a couple of options.

Polymer

static get properties() {
  return {
    myProperty: Boolean,
    mySecondProperty: {
      type: String,
      reflectToAttribute: true
    }
  };
}

Lit

static get properties() {
  return {
    myProperty: { type: Boolean },
    mySecondProperty: {
      type: String,
      reflect: true
    }
  };
}

As you can see, the two definitions are very similar. Though Lit does drop a few options. Lit also goes a little further by providing a useful decorator for this too:

@property({ type: Boolean })
myProperty = false;

Do note, though, decorators will need something like TypeScript or Babel to be used (for now).

Private and Protected Properties

A note worth making on private and protected properties is that they should still be defined as Lit properties in our properties getter:

static get properties() {
  return {
    myProp: { type: String },
    _myProtectedProp: { type: String },
    __myPrivateProp: { type: String }
  }
}

The reason for this is that, by default, Lit will not reflect properties to attributes. This means our private/protected properties will remain invisible in the DOM but can still be observed by Lit to trigger re-renders.

Property Configuration

As mentioned above, a few options of properties have gone away. So here is a what we can do instead...

reflectToAttribute

Lit maintains this functionality but under the option reflect instead:

class MyElement extends LitElement {
  static get properties() {
    return {
      myProp: {
        type: String,
        reflect: true
      }
    };
  }
}

value

Default values are as simple as setting them in the constructor:

class MyElement extends LitElement {
  static get properties() {
    return { myProp: { type: String } };
  }

  constructor() {
    super();
    this.myProp = 'default value';
  }
}

readOnly

There is no concept of a read-only property in Lit, but we can kind of re-create it with regular JavaScript getters:

class MyElement extends LitElement {
  static get properties() {
    return { _myProperty: { type: String } };
  }

  get readOnlyProperty() {
    return this._myProperty;
  }
}

Then any time we want to update it, we simply set _myProperty and Lit will know to trigger a re-render. We can use the methods discussed earlier in this post to also reflect the read-only property to the attribute in _didRender.

notify

Seeing as there's no such thing as "two-way bindings" in Lit, the notify option of properties disappears. There is no use for it anymore.

To implement something similar, you should really use some kind of state store or create your own (if you only need a very simple one). Something like Redux will do nicely.

If you're still using Polymer elements, you can even dispatch a my-property-changed event yourself so parent Polymer elements will pick it up:

this.dispatchEvent(new CustomEvent('my-property-changed'));

computed

Instead of Polymer's computed option, you can simply use a getter:

class MyElement extends LitElement {
  static get properties() {
    return {
      prop1: { type: String },
      prop2: { type: String }
    };
  }

  get computedProperty() {
    return `${this.prop1}${this.prop2}`;
  }

  render() {
    return html`Value is: ${this.computedProperty}`;
  }
}

Seeing as our render method should be called any time the depended upon properties change, everything should work fine here and our computed property will be retrieved each time.

Observers

I haven't yet thought of a great way to do observers, but I think most of the need for them goes away with the ability to just call methods in our render method.

However, if you do want to do something when a property changes, I suppose the best place for it is in the updated method:

updated(changedProps) {
  if (changedProps.has('myProp')) {
    this._onMyPropChanged(this.myProp, changedProps.get('myProp'));
  }
}

If you were using Polymer to observe deep changes like sub-properties of objects and splices of arrays, you can consider one of the following solutions:

  • Use a state store
  • Use something like immutable so deep changes will create a new object and thus trigger a re-render
  • Implement (or use a library) a deep-comparison method to behave as a dirty check.

Events

Event handlers can be added to elements in a similar way to Polymer:

render() {
  return html`<button @click=${(e) => this._onClick(e)}>`;
}

Though it is probably a good idea to create these handlers in your constructor to avoid re-creating the function every time:

constructor() {
  super();
  this._boundOnClick = this._onClick.bind(this);
}

render() {
  return html`<button @click=${this._boundOnClick}>`;
}

For adding events to the current element, it makes sense to simply use the native connectedCallback:

constructor() {
  super();
  this._boundOnClick = this._onClick.bind(this);
}

connectedCallback() {
  super.connectedCallback();
  this.addEventListener('click', this._boundOnClick);
}

disconnectedCallback() {
  super.disconnectedCallback();
  this.removeEventListener('click', this._boundOnClick);
}

Routing

The app-route element is a bit of an anti-pattern. It was always a little unusual to define routing information in markup. It was pretty much a wrapper around a useful library.

Anyhow, when using Lit we have no such element so you may be wondering what we should use instead.

The answer to this is: whatever you want.

Here are a few libraries:

I opted with page.js as I have a fairly simple routing strategy and only need to re-render when one parameter changes:

class MyElement extends LitElement {
  static get properties() {
    return { _view: { type: String } };
  }

  constructor() {
    page('/view/:view', (ctx) => {
      this._view = ctx.params.view;
    });
    page();
  }
}

However, this is just a quick example and should be taken with a grain of salt. There are several problems, like re-using this element will try to re-define the routes and make bad things happen!

Also, I only ever have one instance of this element so don't have to worry about doing this logic in the constructor.

Wrap up

As you can see, lit-element is quite different from Polymer.

The migration between the two isn't something you can easily do in a large project, it will definitely take some of time.

I asked a few questions when looking into Lit:

  • Will it keep its name and location (in the polymer project)?
  • Will it look anything like it does now when it reaches 1.0?
  • Will the browser APIs it depends on change as drastically as they did with Polymer? (even sometimes disappearing from underneath us, HTML imports...)

These and plenty more questions are really to be asked to the Polymer team, but I personally would hold off on using it in production until they have answers.

The last question is one a lot of people will be asking because Polymer experienced it a few times. Most or all of the specs are now stable and widely accepted, so its safe to say Lit likely won't go through those same drastic changes.

My opinion on this is that you let Lit settle a bit, see it reach 1.0 and then make your decision.

If you want to move from Polymer 2.x, it is probably best to go to Polymer 3.x for now using the modulizer and then gradually migrate to LitElement how you like.

For a long time, Web Components and Polymer were seen as unstable, ever-changing and experimental. Maybe now we can see some wider adoption and grow our community more than ever before.