Let us begin this tale with a short stroll through the layers that Polymer is built upon, and some of the rationale of how we got there.
Custom Elements are a powerful emerging web standard that allows developers to create their own elements by attaching a class to a tag-name.
The native API is very simple, it looks something like this:
document.registerElement(<name String>, {prototype: Object[, extends: String]});
There is a little bit of work one has to do to set up the class with the right prototypes and so on to construct a Custom Element. Here is an typical example (using ES5 syntax):
var ctor = function() {
return document.createElement('x-custom');
};
ctor.prototype = Object.create(HTMLElement.prototype);
ctor.prototype.constructor = ctor;
ctor.prototype.createdCallback = function() {
this.innerHTML = 'Hello World, I am a <b>Custom Element!</b>';
}
document.registerElement('x-custom', ctor);
By principle, Polymer team tries to avoid abstracting DOM APIs, especially new ones. But in this case we finally decided the ergonomic benefit was worth it. By wrapping registerElement
in our own function, we can reduce the above boilerplate to:
var ctor = Polymer({
is: 'x-custom',
created: function() {
this.innerHTML = 'Hello World, I am a <b>Custom Element!</b>';
}
});
You might notice the Polymer()
invocation defines created
instead of createdCallback
. This is a feature of Polymer.Base
, a tiny prototype that Polymer()
adds to your prototype chain as it's handling the boilerplate above. Polymer.Base
hooks the standard Custom Element lifecycle callbacks to provide helper implementations. The hooks in turn call shorter-named lifecycle methods on your prototype.
created
instead ofcreatedCallback
attached
instead ofattachedCallback
detached
instead ofdetachedCallback
attributeChanged
instead ofattributeChangedCallback
You can always fallback to using the low-level methods if you wish (iow, you could simply implement createdCallback
in your prototype).
Polymer.Base
also implements registerCallback
on your prototype. Polymer()
calls registerCallback
which allows Polymer.Base
to supply a layering system for Polymer abstractions so that no element needs to pay for features it doesn't use.
By default, the default Polymer distribution include several features. Although Polymer.Base
itself is tiny, if you examine Polymer.Base
you will probably see several methods that have been plugged-in to that prototype by feature definitions. The next few sections will explain these features and why we include them in the default set. Keep in mind that it's entirely possible to construct custom feature sets, or even use a trivial, featureless form of Polymer()
.
The first feature implements support for the properties
property. By placing a object-valued properties
property on your prototype, let's you define various aspects of your custom-elements public API.
By itself, the properties
feature doesn't do anything. It only provides API for asking questions about these special properties (see [link to docs] for details).
Polymer({
is: 'x-custom',
properties: {
user: String,
isHappy: Boolean,
count: {
type: Number,
readOnly: true,
notify: true
}
},
created: function() {
this.innerHTML = 'Hello World, I am a <b>Custom Element!</b>';
}
});
Remember that the fields assigned to count
, such as readOnly
and notify
don't do anything by themselves, it requires other features to give them life.
Many custom elements want to support configuration using HTML attributes. Custom Elements provides the attributeChanged
callback gives us the raw API for this ability, but then we have to deal with initialization and type conversion (attributes are always strings). Here is an example of a custom element that supports a user
attribute using the raw API.
Polymer({
is: 'x-custom',
created: function() {
// handle any initial value
this.attributeChanged('user');
// render
this.innerHTML = 'Hello World, my user is ' + (this.user || 'nobody') + '.';
},
attributeChanged: function(name) {
switch(name) {
case 'user':
// pretty easy since user is a String, for other types
// we have to do more work
if (this.hasAttribute('user')) {
this.user = this.getAttribute('user');
}
break;
}
}
});
Although it's relatively simple, having to write this code becomes annoying when working with multiple attributes or non-String types. It's also not very DRY.
Instead, Polymer's attributes
feature handles this work for you (using the properties
feature data). If an attribute is set that matches a property listed in the properties
object, the value is captured into the matching property. Strings are automatically converted to the specified type.
The type system includes support for Object values expressed as JSON, or Date objects expressed as any Date-parsable string representation. Boolean properties are mapped to Boolean attributes, in other words, if the attribute exists at all, its value is true, regardless of its string-value (and the value is only false if the attribute does not exist).
Here is the equivalent of the above code, taking advantage of the attributes
feature.
<script>
Polymer({
is: 'x-custom',
properties: {
user: String
},
created: function() {
// render
this.innerHTML = 'Hello World, my user is ' + (this.user || 'nobody') + '.';
}
});
</script>
<x-custom user="Scott"></x-custom>
HTML templates are an emerging web standard that we like to consider part of the Web Components family. Templates are a great way to provide archetypal DOM content for your custom element, and this is where the template
feature comes in.
As usual, we started by writing basic template support by hand. It generally looks something like this:
<template>
Hello World from x-custom!
</template>
<script>
Polymer({
is: 'x-custom',
created: function() {
var template = <find the template somehow>;
var instance = document.importNode(template.content, true);
this.appendChild(instance);
}
});
</script>
Again, it's simple, but it's a common pattern, so the template
feature does it automatically. By default it looks for a template as the first element before the script, so our code can look like this:
<template>
Hello World from x-custom!
</template>
<script>
Polymer({
is: 'x-custom'
});
</script>
Most elements need to customize the DOM instanced from a template. For this reason, it's handy to encode markers into your template to indicate special nodes, attributes, or text. Polymer calls these markers annotations. The annotations
feature scans the template (once per element, at registration time) and builds a data-structure into the prototype that identifies markers it finds in the DOM (see [link to docs] for details). Normally you do not need to work with this data directly, Polymer does it for you.
Traditionally, modifying DOM is done by querying for elements to manipulate. Here is an example:
<template>
Hello World from <span id="name"></span>!
</template>
<script>
Polymer({
is: 'x-custom',
created: function() {
this.querySelector("#name").textContent = this.name;
}
});
</script>
This example is very simple. But in real projects, repeating queries is inefficient, so query results are often stored (memoized). Also, as DOM composition becomes more tricky, crafting correct queries can be difficult. For these reasons, automatically capturing nodes makes a good feature.
The annotations-nodes
feature builds a map of instance nodes by id
in this.$
(using the annotations
feature data). Here is how the annotations-nodes
feature simplifies the above example.
<template>
Hello World from <span id="name"></span>!
</template>
<script>
Polymer({
is: 'x-custom',
created: function() {
this.$.name.textContent = this.name;
}
});
</script>
Most elements also need to listen for events. The standard DOM method addEventListener
provides the low-level support:
<template>
<button id="button">Kick Me</button>
</template>
<script>
Polymer({
is: 'x-custom',
created: function() {
this.$.button.addEventListener('click', function() {
alert('Ow!');
});
}
});
</script>
Again, this is pretty simple, but it's so common that it's worth making even simpler. The annotations-events
feature supports declaring event listeners directly in our template.
Declaring listeners in the template is convenient, and also helps us decouple view from behavior.
<template>
<button on-click="kickAction">Kick Me</button>
</template>
<script>
Polymer({
is: 'x-custom',
kickAction: function() {
alert('Ow!');
}
});
</script>
Notice that the kickAction
method doesn't know anything about button
. If we decided that kicking should be performed by a key-press, or a menu-item, the element code doesn't need to know. We can change the UI however we want. Also notice that by attaching the event declaratively, we have removed the need to give the button an id.