Web Components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.
Web components are based on existing web standards. Features to support web components are currently being added to the HTML and DOM specs, letting web developers easily extend HTML with new elements with encapsulated styling and custom behavior.
We'll be building a vanilla web component and prepare all the necessary elements to publish on webcomponents.org
node -v
)npm install -g bower
npm install -g polymer-cli
npm install -g generator-polymer-init-vanilla-web-component
The next steps are reproduced in the my-element repository, we'll reference the commits during the codelab for you to check your progress.
The Polymer CLI provides a number of tools for scaffolding, serving and testing web componentsβboth those built with the Polymer library, and those built with the vanilla web components APIs. The generator-polymer-init-vanilla-web-component
package is a plugin for the Polymer CLI for scaffolding vanilla web components.
Create a directory for your element project. It's best practice for the name of your project directory to match the name of your element.
mkdir my-element && cd my-element
Initialize your element using the vanilla-web-component generator. Polymer CLI asks you a few questions as it sets up your element project.
polymer init vanilla-web-component
Leave the name of the element as "my-element" and add a description of the element (e.g. "My first web component").
Polymer CLI will create these project files and install the dependencies:
README.md
Documentation on how to use the elementindex.html
Entry point - by default it redirects to the demo pagemy-element.html
The element implementationdemo/index.html
Demo page for my-elementtest/index.html
Test page loading all the test suitestest/my-element.html
Test page for my-elementbower.json
Configuration file for Bower - used to define your element's dependenciesbower_components/
Folder with project dependenciespackage.json
Configuration file for npm - used for running testsRun the following command to start a web server for your element, then navigate to the demo page by using the --open
flag. The CLI will find a free port (default 8081
), but you can specify which port to use with the -p
flag followed by the port number.
polymer serve --open
Run the following command to test your element.
polymer test
Checkout commit "generator"
Let's implement a sample feature and write tests for it.
We want to expose 3 properties:
current
settable via html attribute or js property previous
settable via html attribute or js propertydifference
read-only, computed as current - previous
We also want to display:
difference > 0
difference < 0
difference = 0
Before starting the implementation, let's update demo/index.html
to use our new properties:
<demo-snippet>
<template>
<h1>My stock</h1>
<p>Was 5, now is 10</p>
<my-element current="10" previous="5"></my-element>
<p>Was 10, now is 5</p>
<my-element current="5" previous="10"></my-element>
<p>Was 2, now is 2</p>
<my-element current="2" previous="2"></my-element>
</template>
</demo-snippet>
Checkout commit "update demo"
This is what we see on our demo:
The final result we'll implement should look like this:
First, let's declare the properties. In my-element.html
file, we want to update the this._properties
object and define the properties' getters and setters
constructor() {
super();
/**
* @type {!Object}
* @private
*/
this._properties = {
current: null,
previous: null,
difference: null,
};
}
/**
* @property {number|null} current
*/
get current() {
return this._properties.current;
}
/**
* @property {number|null} previous
*/
get previous() {
return this._properties.previous;
}
/**
* @property {number|null} difference
* @readonly
*/
get difference() {
return this._properties.difference;
}
set current(val) {
if (val !== this.current) {
this._properties.current = val;
this._updateRendering();
}
}
set previous(val) {
if (val !== this.previous) {
this._properties.previous = val;
this._updateRendering();
}
}
Checkout commit "define current, previous, difference"
Note that difference
has only a getter, as it is a computed property.
We want to update difference
when either current
or previous
change. Let's add _updateDifference
method and invoke it in the setters:
set current(val) {
val = Number.isFinite(val) ? val : null;
if (val !== this.current) {
this._properties.current = val;
this._updateDifference();
this._updateRendering();
}
}
set previous(val) {
val = Number.isFinite(val) ? val : null;
if (val !== this.previous) {
this._properties.previous = val;
this._updateDifference();
this._updateRendering();
}
}
/**
* @private
*/
_updateDifference() {
this._properties.difference = this._computeDifference(this.current, this.previous);
}
/**
* @param {number|null} current
* @param {number|null} previous
* @return {number|null}
* @private
*/
_computeDifference(current, previous) {
return (current === null || previous === null) ? null : current - previous;
}
Checkout commit "compute difference"
Note we also added a value check on the setters to ensure current
and previous
values are either a finite number or null
. Like this, we can afford to only check for null
in the implementation of _computeDifference
, and gain in performance.
Let's see how we're doing so far: navigate in Chrome to demo/index.html, open the developer console, and observe what is the value of document.querySelector('my-element').difference
We'd expect it to be different from null
, as the element has current and previous defined. This is because we are not observing the right attributes!
Before fixing that though, let's check if updating current
and previous
does update difference
: try setting current and previous from the developer console and observe if difference is correctly updated.
Let's observe the right attributes, and ensure we normalize strings to numbers
static get observedAttributes() {
return ['current', 'previous'];
}
attributeChangedCallback(name, old, value) {
if (old !== value) {
// Normalize strings to numbers.
this[name] = Number(value);
}
}
Checkout commit "observe attributes"
Now our element is able to accept setting properties through attributes π
Next, let's update the rendering part.
First, let's simplify the element's template to have only one <h2>
which we'll update to contain our symbol:
<template id="my-element">
<style>
:host {
display: block;
}
</style>
<h2></h2>
</template>
Then, let's change _updateRendering
to actually render our symbol:
/**
* @private
*/
_updateRendering() {
// Avoid rendering when not connected.
if (this.shadowRoot && this.isConnected) {
const h2 = this.shadowRoot.querySelector('h2');
h2.textContent = this._computeSymbol(this.difference);
}
}
/**
* @param {number|null} difference
* @return {string}
* @private
*/
_computeSymbol(difference) {
return difference === null ? '' :
difference === 0 ? 'π' :
difference > 0 ? 'π' : 'π';
}
Checkout commit "update rendering"
And we're done!
Next, let's make our tests page green again.
We want to update the ChangedPropertyTestFixture
to set the correct attributes:
<test-fixture id="ChangedPropertyTestFixture">
<template>
<my-element current="10" previous="0"></my-element>
</template>
</test-fixture>
Our tests should check at least for the values of current, previous, difference
and that the rendering of the symbol is done correctly.
test('instantiating the element with default properties works', function() {
var element = fixture('BasicTestFixture');
assert.equal(element.current, null);
assert.equal(element.previous, null);
assert.equal(element.difference, null);
var elementShadowRoot = element.shadowRoot;
var elementHeader = elementShadowRoot.querySelector('h2');
assert.equal(elementHeader.textContent, '');
});
test('setting a property on the element works', function() {
var element = fixture('ChangedPropertyTestFixture');
assert.equal(element.current, 10);
assert.equal(element.previous, 0);
assert.equal(element.difference, 10);
var elementShadowRoot = element.shadowRoot;
var elementHeader = elementShadowRoot.querySelector('h2');
assert.equal(elementHeader.textContent, 'π');
});
Checkout commit "update tests"
Consider unit tests as the manifesto of how your element is expected to be used.
Verify tests are all green by running polymer test
.
Remember to always keep README.md
, bower.json
and package.json
documentation updated π
e.g. we might want to update the description and snippet:
Checkout commit "update description"
In order to publish your element on webcomponents.org, you'll have to first publish your repository on github.
In order to publish on webcomponents.org, your element must provide:
Newer versions of your element will be automatically visible on Webcomponents.org after 10-15 minutes from tagging and releasing of the version. See more details at https://www.webcomponents.org/publish.
The vanilla-web-component generator we used in this tutorial already fulfilled these requirements:
package.json
and bower.json
- see tag and create a release on githubpackage.json
, bower.json
and README.md
- feel free to update as neededAdditionally, webcomponents.org allows to include an inline demo in your README.md for people to try your element. You can preview the result at https://www.webcomponents.org/preview (you will need to first setup Github preview integration).
The inline demo is enabled through a comment block in the README.md - already included for you by vanilla-web-component generated elements.
Finally, ensure you test, document and preview your inline demo of your components before publishing them on webcomponents.org.
Another benefit of publishing your repository on github is that you can integrate Travis CI to automate your tests.
You'll need to enable your repository to be run by Travis CI in your Travis Profile and to configure the build via a .travis.yml
file in your repository.
language: node_js
sudo: required
node_js: '6'
addons:
firefox: latest
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
script:
- xvfb-run npm test
dist: trusty
After that is done, any new commit or creation of new branches will trigger a build on Travis.
Most of the code we wrote is repetitive, e.g. see our getters/setters. Also, we have our custom way to keep track of the exposed properties, handle computed properties, and update the rendering.
While this gives us complete control over what is happening in our element, it makes it harder to maintain the codebase, and we might soon feel the urge of building some sugar code to handle these mundane tasks.
Polymer provides the minimal sugar code and syntax which allows our element to focus on its functionality. In particular:
The polymer branch shows how my-element.html
implementation can be simplified:
<link rel="import" href="../polymer/polymer-element.html">
<dom-module id="my-element">
<template>
<style>
:host {
display: block;
}
</style>
<h2>[[_computeSymbol(difference)]]</h2>
</template>
</dom-module>
<script>
(() => {
'use strict';
class MyElement extends Polymer.Element {
static get is() {
return 'my-element';
}
static get properties() {
return {
/**
* @property {number|null} current
*/
current: {
type: Number,
value: null
},
/**
* @property {number|null} previous
*/
previous: {
type: Number,
value: null
},
/**
* @property {number|null} difference
* @readonly
*/
difference: {
type: Number,
computed: '_computeDifference(current, previous)'
}
};
}
/**
* @param {number|null} current
* @param {number|null} previous
* @return {number|null}
* @private
*/
_computeDifference(current, previous) {
return (current === null || previous === null) ? null : current - previous;
}
/**
* @param {number|null} difference
* @return {string}
* @private
*/
_computeSymbol(difference) {
return difference === null ? '' :
difference === 0 ? 'π' :
difference > 0 ? 'π' : 'π';
}
}
customElements.define(MyElement.is, MyElement);
})();
</script>