Lets Build Web Components! Part 4: Polymer Library
Benny Powers 🇮🇱🇨🇦
Posted on October 14, 2018
Component-based UI is all the rage these days. Did you know that the web has its own native component module that doesn't require the use of any libraries? True story! You can write, publish, and reuse single-file components that will work in any* good browser and in any framework (if that's your bag).
In our last post, we learned how to write single-file components with nothing other than JavaScript and the DOM API.
Today we'll be diving in to the original web components library: Polymer. We'll refactor the <lazy-image> component we built last time to take advantage of Polymer's helpful features. We'll also learn how to compose entire apps from Polymer-based components using their expressive templating system and two way binding. We'll take a look at some of the fantastic ready-made paper elements published by the Polymer team. And last, we'll survey some of the Polymer project's helpful tools and learn how they are useful for any web-component project, not just Polymer apps.
The Polymer Project started way back in 2012/2013 with the goal of advancing the capabilities of the web platform. Legend has it that deep in the bowels of the Googleplex, a group of Chrome browser engineers convened a secret seance with a group of web developers to map out the future course of the web at large.
The browser engineers asked the web developers to tell them what they wanted web dev to look like in five years' time, then they set about building it. The result was the first release of the Polymer library and the beginning of the modern web components story.
Since then, the Polymer Project has come full circle, such that it's now possible to write web components without using Polymer Library at all. But the Polymer Project is still alive and kicking. They maintain a variety of web platform proposals and advocate for a more standards-based type of web development than is currently popular.
The Polymer library on the other hand has since become only one of a number of alternatives for factoring web components and component-based apps.
So don't confuse the two things. The Project is about the platform at large, the Library is about helping you build components.
Refactoring <lazy-image>
So let's dive in! And since we've already developed our <lazy-image> vanilla component, let's use it as basis to explore Polymer as well.
Our first step in refactoring <lazy-image> will be to install and import the Polymer library.
npm i -S @polymer/polymer
We'll also rename our component a little to help us keep our heads on straight:
Polymer 3.0 and the paper-elements require us to apply a transformation to all module specifiers, either in a build step, or as a server run-time thing. We'll use polymer serve, which transforms bare specifiers on the fly for us.
npm i -D polymer-cli
npx polymer serve
Another important step we should take now before we do any more mucking around is to call the super versions of all of our lifecycle callbacks.
Not doing so will cause problems, since The PolymerElement base class needs to do work when lifecycle things happen. Work like handling the polyfills, which we don't need to do manually anymore...
We can lose all of the shadowRoot- and ShadyCSS-related code now, including updateShadyStyles, because Polymer will handle that for us. Nice! Working with libraries has taken one stress - supporting the polyfills - off our minds.
Properties
Polymer lets you declare your element's properties statically, and I mean 'statically' in the sense of both static get and 'at writing time'. When you declare a property in that block, Polymer handles synchronizing attributes and properties for you. That means that when the src attribute on our element is set, Polymer will automatically update the src property on the element instance.
So now we can delete our attributeChangedCallback, safeSetAttribute, and all our getters and setters, and replace them with a static property map with some special Polymer-specific descriptors.
staticgetproperties(){return{/** Image alt-text. */alt:String,/**
* Whether the element is on screen.
* @type {Boolean}
*/intersecting:{type:Boolean,reflectToAttribute:true,notify:true,},/** Image URI. */src:String,};}
Polymer binds to properties, not attributes by default. This means that if you bind to one of your element's properties in a host element's polymer template, it won't necessarily show up as an attribute on the element. Setting the reflectToAttribute boolean on a property descriptor ensures that whenever the property changes, Polymer will also set the appropriate attribute on the element. Don't worry, though, even if you declare a property with a constructor like propName: String, attribute changes will always update the associated property, whether or not you set reflectToAttribute.
The notify boolean will make your element dispatch a custom event every time your property changes. The event will be called property-name-changed e.g. intersecting-changed for the intersecting property, and will have as it's detail property an object containing the key value that points to the new value of your property.
lazyImage.addEventListener('intersecting-changed',event=>{console.log(event.detail.value)// value of 'intersecting';})
This is the basis of Polymer's two-way binding system. It's not strictly necessary here, but we might as well expose those events, in case a user wants to bind an image's intersecting status up into an enclosing component.
So now we can also delete the setIntersecting method, since with the help of our property map and Polymer's templating system, we won't need it.
We'll have more on Polymer's property descriptors later on.
Data Binding Templates
We define a Polymer 3 element's templates with a static template getter which returns a tagged template literal.
staticgettemplate(){returnhtml`
I'm the Template!
`;}
Polymer templates feature a special syntax reminiscent of handlebars or mustache. One way (data-down) bindings are made with double-[[square brackets]], and two-way (data-up) bindings are done with double-{{curly braces}}.
In this example, whenever <some-input> fires a input-changed event, the host element updates the someProperty property on <some-element>. In JS terms, it's a simple assignment: someElementInstance.someProperty = this.myInput.
If you want to bind to an attribute, instead of a property, append the $ character to the binding: whenever myOtherProp changes, the some-attribute on <some-element> will update: someElementInstance.setAttribute('some-attribute', this.myOtherProp).
Similarly, whenever the input-changed custom event fires on <some-input>, the myInput property on the host component will be set to to event's detail.value property.
In our <polymer-lazy-image> template, we're not using any two-way binding, so we'll stick with square brackets.
The aria-hidden attribute presents a small challenge. Polymer binds Boolean values to attribute with setAttribute(name, '') and removeAttribute(name). But since aria-hidden must take the string literals "true" or "false", we can't just bind it to the Boolean value of intersecting. The <img/>src is similarly interesting. Really, we want to set it only after the element has intersected. For that, we'll need to compute the src property on the image based on the state of the intersecting property.
Polymer templates can include computed bindings. These are bound to the return value of the chosen method.
What's with this function-like syntax inside our binding expressions? That tells Polymer which element method to run and when. It will fire every time it's dependencies (i.e. the 'arguments passed' in the binding expression) are observed to change, updating the binding with the return value.
Note also that we're binding to the srcproperty on the image, not it's attribute. That's to avoid trying to load an image at URL "undefined".
computeSrc(intersecting,src){// when `intersecting` or `src` change,returnintersecting?src:undefined;}computeImageAriaHidden(intersecting){// when `intersecting` changes,returnString(!intersecting);}
Don't be misled, though, these aren't JavaScript expressions, so you can't pass in any value you want: [[computeImageAriaHidden(!intersecting)]] doesn't work, neither does [[computeImageAriaHidden(this.getAttribute('aria-hidden'))]]
Now we'll just adjust our property map and styles slightly to account for the changes in our element's API:
staticgetproperties(){return{// .../** Whether the element is intersecting. */intersecting:Boolean,/**
* Whether the image has loaded.
* @type {Boolean}
*/loaded:{type:Boolean,reflectToAttribute:true,value:false,},};}
So, we were able to substantially reduce boilerplate in our component, and trim down some of the excess logic by including it in our template, albeit with a few somewhat tiresome computed binding helpers.
Here's our completed <polymer-lazy-image> module:
import{PolymerElement,html}from'@polymer/polymer';constisIntersecting=({isIntersecting})=>isIntersecting;consttagName='polymer-lazy-image';classPolymerLazyImageextendsPolymerElement{staticgettemplate(){returnhtml`
<style>
:host {
position: relative;
}
#image,
#placeholder ::slotted(*) {
position: absolute;
top: 0;
left: 0;
transition:
opacity
var(--lazy-image-fade-duration, 0.3s)
var(--lazy-image-fade-easing, ease);
object-fit: var(--lazy-image-fit, contain);
width: var(--lazy-image-width, 100%);
height: var(--lazy-image-height, 100%);
}
#placeholder ::slotted(*),
:host([loaded]) #image {
opacity: 1;
}
#image,
:host([loaded]) #placeholder ::slotted(*) {
opacity: 0;
}
</style>
<div id="placeholder" aria-hidden$="[[computePlaceholderAriaHidden(intersecting)]]">
<slot name="placeholder"></slot>
</div>
<img id="image"
aria-hidden$="[[computeImageAriaHidden(intersecting)]]"
src="[[computeSrc(intersecting, src)]]"
alt$="[[alt]]"
on-load="onLoad"
/>
`;}staticgetproperties(){return{/** Image alt-text. */alt:String,/** Whether the element is on screen. */intersecting:Boolean,/** Image URI. */src:String,/**
* Whether the image has loaded.
* @type {Boolean}
*/loaded:{type:Boolean,reflectToAttribute:true,value:false,},};}constructor(){super();this.observerCallback=this.observerCallback.bind(this);}connectedCallback(){super.connectedCallback();// Remove the wrapping `<lazy-image>` element from the a11y tree.this.setAttribute('role','presentation');// if IntersectionObserver is available, initialize it.if ('IntersectionObserver'inwindow)this.initIntersectionObserver();// if IntersectionObserver is unavailable, simply load the image.elsethis.intersecting=true;}disconnectedCallback(){super.disconnectedCallback();this.disconnectObserver();}/**
* Loads the img when IntersectionObserver fires.
* @param {Boolean} intersecting
* @param {String} src
* @return {String}
*/computeSrc(intersecting,src){returnintersecting?src:undefined;}/**
* "true" when intersecting, "false" otherwise.
* @protected
*/computePlaceholderAriaHidden(intersecting){returnString(intersecting);}/**
* "false" when intersecting, "true" otherwise.
* @protected
*/computeImageAriaHidden(intersecting){returnString(!intersecting);}/** @protected */onLoad(){this.loaded=true;}/**
* Sets the `intersecting` property when the element is on screen.
* @param {[IntersectionObserverEntry]} entries
* @protected
*/observerCallback(entries){if (entries.some(isIntersecting))this.intersecting=true;}/**
* Initializes the IntersectionObserver when the element instantiates.
* @protected
*/initIntersectionObserver(){if (this.observer)return;// Start loading the image 10px before it appears on screenconstrootMargin='10px';this.observer=newIntersectionObserver(this.observerCallback,{rootMargin});this.observer.observe(this);}/**
* Disconnects and unloads the IntersectionObserver.
* @protected
*/disconnectObserver(){this.observer.disconnect();this.observer=null;deletethis.observer;}}customElements.define(tagName,PolymerLazyImage);
Check out the diff between the vanilla and Polymer versions, and see the component at work:
More Polymer Features
Polymer has more to offer than our simple example element can easily demonstrate. A small example is the way Polymer maps all the id'd elements in your template to an object called $:
Polymer can also bind to host properties from non-polymer elements' events with a special syntax:
<videocurrent-time="{{videoTime::timeupdate}}"/>
This means "when the timeupdate event fires, assign the local videoTime property to the video element's currentTime".
In a later iteration of <polymer-lazy-image>, we might use these kinds of bindings to synchronize internal <img> properties with our own.
For the low-down on Polymer's data-binding system, give the docs a read.
Observers and Computed Properties
Computed properties and bindings are specialized cases of Polymer observers. A simple observer looks like this:
staticgetproperties(){return{observed:{type:String,observer:'observedChanged',},};}observedChanged(observed,oldVal){console.log(`${observed} was ${oldVal}`);}
You can also define complex observers that take multiple dependencies or deeply observe objects or arrays.
staticgetproperties(){return{observed:Object,message:{type:String,value:'A property of observed has changed',},};}staticgetobservers(){return[// careful: deep observers are performance intensive!'observedChanged(message, observed.*)'],}observedChanged(message,{path,value,base}){// path: the path through the object where the change occurred// value: the new value at that path// base: the root object e.g. `observed`console.log(message,path+': '+value);}
You can also set up computed properties, similar to computed bindings:
We've already seen how we can set reflectToAttribute and notify to affect the outside world when our values update, and how to set up simple observers with the observer descriptor.
You can also set a default value with value, which takes either a literal value or a function.
Be careful! When you want to set a default value with a reference type like Array or Object, be sure to pass a function, or else every instance of your element will share the same reference.
value assignments are set once when the component initializes, then not updated again. If you need to dynamically set properties after connecting, use computed properties or observers.
Helper Elements
Polymer comes with a few helper elements that you can use in your templates to reduce the amount of imperative JavaScript you need to write. The two most commonly used are <dom-repeat> for iterating through lists and outputting DOM, and <dom-if> for conditional rendering:
<!-- Will output a new article with h2 and img for each post --><dom-repeatitems="[[posts]]"as="post"><template><article><h2>[[post.title]]</h2><imgsrc$="[[post.picture]]"></article></template></dom-repeat><!-- Will only render it's template if conditionDepending(someProp, another) is truthy --><dom-ifif="[[conditionDepending(someProp, another)]]"><template>
I'm a very lucky textNode to have [[someProp]] and [[another]] on my side.
</template></dom-if>
Polymer really shines when it comes to factoring whole apps. The Polymer Project pioneered a pretty progressive and patently special (sorry) kind of declarative app structure built largely on HTML elements. The Polymer approach makes "everything an element", leveraging HTML's built-in composability. So for example, there's the <iron-ajax> element, which can fetch resources and expose them to Polymer's data binding.
Using app-route and iron-pages elements we have a complete routing solution that will hide and show content based on the URL, and even pass route-related data to those view components.
And since <app-route> takes it's route property as data, not directly tied to window.location, you can pass portions of the route down to child views, and let them manage their own internal state with their own <app-route> children. Neat!
**Note** that for the sake of brevity, we're binding directly to subproperties of `routeData` in this example, but in a real project we'd add some helper methods to compute an intermediate `page` property from `routeData`.
For a fully-realised example of this type of app architecture, see the venerable Polymer Starter Kit on GitHub.
This template is a starting point for building apps using a drawer-based
layout. The layout is provided by app-layout elements.
This template, along with the polymer-cli toolchain, also demonstrates use
of the "PRPL pattern" This pattern allows fast first delivery and interaction with
the content at the initial route requested by the user, along with fast subsequent
navigation by pre-caching the remaining components required by the app and
progressively loading them on-demand as the user navigates through the app.
The PRPL pattern, in a nutshell:
Push components required for the initial route
Render initial route ASAP
Pre-cache components for remaining routes
Lazy-load and progressively upgrade next routes on-demand
It wouldn't be a blog post on Polymer if we didn't mention the Paper Elements, the set of material-design UI components published by the Polymer Project. But we'd also be making a huge mistake if we didn't get one thing super-clear:
PaperElements!=Polymer;
You can use the polymer library just fine without using the paper-elements, and you can use the paper-elements just fine without using the polymer library!
All we're losing here is the ability to use Polymer's data binding system. But - you guessed it - there's an element for that, called <dom-bind>
If you're looking to factor a material-design based UI with no fuss - give the paper-elements a try.
Polymer Tools
The Polymer Project - in addition to their advocacy work, JS and component libraries, and standards proposals - also publish a variety of tools that help you get your apps and components built, published, and served.
prpl-server
The Chrome team developed the PRPL pattern as a best-practice for writing and delivering performant web apps. prpl-server makes it easy to serve the smallest effective bundle to capable browsers while still supporting older browsers with larger bundles. There's a ready made binary as well as an express middleware library. Give it a try.
Polymer CLI
The Vue CLI helps you develop Vue apps. The Angular CLI helps you develop Angular apps. create-react-app helps you develop React apps.
The Polymer CLI helps you develop web apps.
True, it offers templates for Polymer 3 elements and apps, but that's not all. The polymer build and polymer serve commands will build and serve any web-component apps. Transpilation is optional. In fact, pretty much the only thing the CLI will do to your code is replace bare module specifiers like import { PolymerElement } from '@polymer/polymer'; to relative URLs that the browser can load directly.
What!? You mean no Webpack? No Babel? No hours wrestling with config files and APIs that have nothing to do with my app code?
Yeah. That's exactly what I'm talking about. Next time you have an app project, consider factoring it with web components and the Polymer CLI.
But if you want to transpile for older browsers (see prpl-server above), you can define a builds section of polymer.json:
Before you go running off to implement that <super-button> you've got in mind, why not give a search over at webcomponents.org, the largest directory of web components.
Each element is shown with its documentation, public API, and installation method. You'll also find links to npm and github.
If you're a component author, don't hesitate! Publish your components for others to benefit from.
Conclusions
The Polymer library was undeniably ahead of its time. It took the approach of demanding better of the web platform and then making that a reality, instead of just working around the platform's limitations.
Now that web components are broadly supported, does the Polymer library still have a place in our web-dev toolbox? Sure does! Some projects will naturally lend themselves to Polymer's declarative style. Some teams will discover how designers and document authors can do the work of developers with Polymer's expressive binding system.
It's not all ☀️ and 🌹🌹 though. As the platform and the wider web community has developed, so have the priorities of the Polymer project. Polymer 3 will probably be the last major release of the library, and likewise the 3.0 series will be the last release of the paper-elements.
So let's review some of the pros and cons of the Polymer library:
Pros
Cons
Expressive templating system
Can't pass JS directly to templates
Observers and computed properties, declarative event-listeners
Large dependency chain incentivizes larger Polymer-only apps
Super cool and unique approach to declarative app structure
For better or for worse, this unique declarative style is not as popular as other architectures
A mature library and component set. Tried, tested, and true
Polymer.js is all-but-deprecated, and won't receive new features unless forked
So does that mean the end for Web Components? Heck no! Polymer is far from the only game in town. A lightweight, declarative JS templating library called lit-html and a custom-element base class that leverages it called LitElement are the new hotness. God-willing, we'll cover them in our next installment.
See you then 😊
Would you like a one-on-one mentoring session on any of the topics covered here?
Acknowledgements
Thanks in no particular order to Pascal Schilp, and @ruphin for their suggestions and corrections.