layout | title |
---|---|
default |
Polymer core |
The {{site.project_title}} core provides a thin layer of code that expresses
its opinion and provides the extra sugaring that all {{site.project_title}} elements use. It is provided in the file polymer.js
.
Note: You can find working examples of the concepts on this page in /toolkit-ui, /polymer-elements, and /polymer-ui-elements. {: .alert .alert-success }
At the heart of {{site.project_title}} are Custom Elements. Thus, it should be no surprise that defining a {{site.project_title}} element is similar to the way you define a standard Custom Element. The major difference is that {{site.project_title}} elements are created using <polymer-element>
, not <element>
.
To register and supercharge <tag-name>
as a {{site.project_title}} element, use <polymer-element>
to create its definition:
<polymer-element name="tag-name">
<template>
<!-- shadow DOM here -->
</template>
</polymer-element>
If you need to set methods or properties on the element's prototype
, use {{site.project_title}}()
.
Its first argument is the name of the element you're creating. The second argument (optional) is an object that defines your element's prototype
. In the following example the registration call defines a property message
and the ready
callback:
<polymer-element name="tag-name">
<template>
<!-- shadow DOM here -->
</template>
<script>
{{site.project_title}}('tag-name', {
message: "Hello!",
ready: function() {
// Component is ready. Use it.
}
});
</script>
</polymer-element>
{{site.project_title}} has first class support for the Custom Element lifecycle callbacks, though implements them with shorter names for convenience.
Allow the lifecycle callbacks are optional:
{{site.project_title}}('tag-name', {
ready: function() { ... },
inserted: function () { ... },
removed: function() { ... },
attributeChanged: function(attrName, oldVal, newVal) {
//var newVal = this.getAttribute(attrName);
console.log(attrName, 'old: ' + oldVal, 'new:', newVal);
},
});
Below is a table with the names of lifecycle method according to the Custom Elements specificationn vs. the names {{site.project_title}} uses.
Spec | {{site.project_title}} | Called when |- readyCallback | ready | an instance of the element is created insertedCallback | inserted | an instance was inserted into the document removedCallback | removed | an instance was removed from the document attributeChangedCallback | attributeChanged | an attribute was added, removed, or updated {: .table }
The polyfill(s) parse element definitions and handle their upgrade asynchronously. If you try to fetch the element from the DOM before things have settled, you'll get a big fat null
. In these situations, an including page should wait for the WebComponentsReady
event
before working with the node.
Example:
<head>
<link rel="import" href="path/to/x-foo.html">
</head>
<body style="opacity:0">
<x-foo></x-foo>
<script>
window.addEventListener('WebComponentsReady', function() {
document.body.style.opacity = 1; // show body now that registration is done.
var xFoo = document.querySelector('x-foo');
// Do something with x-foo.
});
</script>
</body>
Tip: Use the WebComponentsReady
event to mitigate FOUC in browsers that don't
support the CSS :unknown
pseudo class.
{: .alert .alert-success }
When you publish a property name, you're making that property two-way data-bound and part of the element's "public API". Published properties can be initialized by an HTML attribute of the same name.
There are two ways to publish properties:
- Preferred - Include its name in the
<polymer-element>
'sattributes
attribute. - Include the name in a
publish
object on your prototype.
As an example, here's an element that publishes three public properties, foo
, bar
, and baz
:
<polymer-element name="x-foo" attributes="foo bar baz">
<script>
Polymer('x-foo');
</script>
</polymer-element>
By default, properties defined in attributes
are null
:
<polymer-element name="x-foo" attributes="foo">
<script>
Polymer('x-foo'); // x-foo has a foo property with null value.
</script>
</polymer-element>
As such, you can provide default values using the prototype
:
<polymer-element name="x-foo" attributes="foo">
<script>
Polymer('x-foo', { // x-foo has a foo property with default value false.
foo: false
});
</script>
</polymer-element>
<!-- Same, but using the alternate "publish" object. -->
<polymer-element name="x-foo">
<script>
Polymer('x-foo', {
publish: {
foo: false
}
});
</script>
</polymer-element>
Attributes are a great way for users of your element to configure it, declaratively. They can customize a published property by passing an initial value on the attribute with the same name:
<x-foo foo="true"></x-foo>
Note: As of today, property values are not reflected back into markup. Also, setting
an attribute using .setAttribute()
) has no effect.
{: .alert }
When attribute values are converted to property values, {{site.project_title}} attempts to convert the value to the correct type, depending on the default value of the property.
<polymer-element name="x-foo" attributes="foo">
<script>
Polymer('x-foo', {
foo: false // hint that foo is Boolean
});
</script>
</polymer-element>
Published properties are data-bound inside of {{site.project_title}} elements and accessible
via MDV's {%raw%}{{}}{%endraw%}
. These bindings are by reference and are two-way.
For example, we can define a name-tag
element that publishes two properties,
name
and nameColor
.
<polymer-element name="name-tag" attributes="name nameColor">
<template>
Hello! My name is <span style="color:{{"{{nameColor"}}}}">{{"{{name"}}}}</span>
</template>
<script>
{{site.project_title}}('name-tag', {
nameColor: "orange"
});
</script>
</polymer-element>
In this example, name
has initial value of null
and nameColor
has a value of "orange".
Thus, the <span>
's color will be orange.
Generally, attributes are string values, but {{site.project_title}} makes it possible to bind references between elements using attributes. The binding engine interprets reference bindings by interrogating the attribute's type. This means you can bind an an object to an HTML attribute!
Let's modify the name-tag
example to take an object instead of individual properties.
<polymer-element name="name-tag" attributes="person">
<template>
Hello! My name is <span style="color:{{"{{person.nameColor"}}}}">{{"{{person.name"}}}}</span>
</template>
<script>
{{site.project_title}}('name-tag', {
ready: function() {
this.person = {
name: "Scott",
nameColor: "orange"
}
}
});
</script>
</polymer-element>
Now, imagine we make a new component called <visitor-creds>
that uses name-tag
:
<polymer-element name="visitor-creds">
<template>
<name-tag person="{{"{{person"}}}}"></name-tag>
</template>
<script>
{{site.project_title}}('visitor-creds', {
ready: function() {
this.person = {
name: "Scott2",
nameColor: "red"
}
}
});
</script>
</polymer-element>
When an instance of <visitor-creds>
is created, its person
property (an object)
is also bound to <name-tag>
's person
property. Now both components are using
the same person
object.
Important: Be careful when your properties are objects or arrays. Element registration
is evaluated once. This means only one instance of an object used in property initialization is ever created. Because of the nature of prototype
, you may run into unexpected "shared state" across different instances of the same element if you're setting an initial value for a property which is an object or array. Do this type of initialization in ready()
rather than directly on the prototype
.
{: .alert .alert-error }
{{site.project_title}} supports declarative binding of events to methods in the component.
It uses special on-event
syntax to trigger this binding behavior.
<polymer-element name="g-cool" on-keypress="keypress">
<template>
<button on-click="buttonClick"></button>
</template>
<script>
{{site.project_title}}('g-cool', {
keypress: function(event) { ...},
buttonClick: function(event) { ... }
});
</script>
</polymer-element>
In this example, the on-keypress
declaration maps the standard DOM "keypress"
event to the keypress
method in the component. Within the component template, the on-click
declaration maps a custom buttonClick
event to the buttonClick
method in the component. This is achieved again without the need for any glue code.
Some things to notice:
- The value of an event handler attribute is the string name of a method on the component. Unlike traditional syntax, you cannot put executable code in the attribute.
- The event handler is passed the following arguments:
inEvent
is the standard event object.inDetail
: A convenience form ofinEvent.detail
.inSender
: A reference to the node that declared the handler. This is often different frominEvent.target
(the lowest node that received the event) andinEvent.currentTarget
(the component processing the event), so {{site.project_title}} provides it directly.
In addition to the above features, which are focused around making the core functionality of components simple and easy to use, {{site.project_title}} provides syntactical sugar that makes more advanced component features easy to create.
All properties on {{site.project_title}} elements can be watched for changes by implementing a propertyNameChanged
handler. When the value of a watched property changes, the appropriate change handler is automatically invoked.
<polymer-element name="g-cool" attributes="better best">
<script>
{{site.project_title}}('g-cool', {
plain: '',
best: '',
betterChanged: function(inOldValue) {
},
bestChanged: function(inOldValue) {
}
});
</script>
</polymer-element>
In this example, there are two watched properties, better
and best
. The betterChanged
and bestChanged
function will be called whenever better
or best
are modified, respectively.
Another useful feature of {{site.project_title}} is node reference marshalling. Every node in a component's shadow DOM that is tagged with an id
attribute is automatically referenced in the component's this.$
hash.
For example, the following defines a component whose template contains an <input>
element whose id
attribute is nameInput
. The component can refer to that element with the expression this.$.nameInput
.
<polymer-element name="x-form">
<template>
<input type="text" id="nameInput">
</template>
<script>
{{site.project_title}}('x-form', {
logNameValue: function() {
console.log(this.$.nameInput.value);
}
});
</script>
</polymer-element>
A {{site.project_title}} element can extend another element by using the extends
attribute. The parent's properties and methods are inherited by the child element,
data-bound, and accessible via MDV.
<polymer-element name="polymer-cool">
<!-- UI-less element -->
<script>
{{site.project_title}}('polymer-cool', {
praise: 'cool'
});
</script>
</polymer-element>
<polymer-element name="polymer-cooler" extends="polymer-cool">
<template>
{%raw%}{{praise}}{%endraw%} <!-- "cool" -->
</template>
<script>
{{site.project_title}}('polymer-cooler');
</script>
</polymer-element>
When you override an inherited method, you can call the parent's method with this.super()
.
<polymer-element name="polymer-cool">
<script>
{{site.project_title}}('polymer-cool', {
praise: 'cool',
makeCoolest: function() {
this.praise = 'coolest';
}
});
</script>
</polymer-element>
<polymer-element name="polymer-cooler" extends="polymer-cool" on-click="makeCoolest">
<template>polymer-cooler is {%raw%}{{praise}}{%endraw%}</template>
<script>
{{site.project_title}}('polymer-cooler', {
praise: 'cooler',
makeCoolest: function() {
this.super(); // calls polymer-cool's makeCoolest()
}
});
</script>
</polymer-element>
<polymer-cooler></polymer-cooler>
In this example, when the user clicks on a <polymer-cooler>
element, its
makeCoolest()
method is called, which in turn calls the parent's version
using this.super()
. The praise
property (inherited from <polymer-cool>
) is set
to "coolest".
Source: base.js
Many things in {{site.project_title}} happen asynchronously. Changes are gathered up and executed all at once, instead of executing right away. Batching changes creates an optimization that (a) prevents duplicated work and (b) reduces unwanted FOUC.
Change watchers and situations that rely on MDV data-bindings are examples that fit under this async behavior. For example, conditional templates may not immediately render after setting properties because changes to those renderings are saved up and performed all at once after you return from JavaScript.
To do work after changes have been processed, {{site.project_title}} provides asyncMethod()
.
It's similar to window.setTimeout()
, but automatically binds this
to the correct value:
// asyncMethod(inMethod, inArgs, inTimeout)
this.asyncMethod(function() {
this.foo = 3;
}, null, 1000);
// Roughly equivalent to:
//setTimeout(function() {
// this.foo = 3;
//}.bind(this), 1000);
The second argument to asyncMethod()
, inArgs
, is an optional object or array of arguments to
pass to the callback.
In the case of property changes that result in DOM modifications, follow this pattern:
Polymer('my-element', {
propChanged: function() {
// If "prop" changing results in our DOM changing, schedule an update after
// the new microtask.
this.asyncMethod(this.updateValues);
},
updateValues: function() {...}
});
{{site.project_title}} core provides a convenient fire()
method for
sending custom events. Essentially, it's a wrapper around your standard node.dispatchEvent(new CustomEvent(...))
. In cases where you need to fire an event after microtasks have completed,
use the asynchronous version: asyncFire()
.
Example:
<polymer-element name="ouch-button">
<template>
<button on-click="onClick">Send hurt</button>
</template>
<script>
Polymer('ouch-button', {
onClick: function() {
this.fire('ouch', {msg: 'That hurt!'}); // fire(inType, inDetail, inToNode)
}
});
</script>
</polymer-element>
<ouch-button></ouch-button>
<script>
document.querySelector('ouch-button').addEventListener('ouch', function(e) {
console.log(e.type, e.detail.msg); // "ouch" "That hurt!"
});
</script>
Tip: If your element is within another {{site.project_title}} element, you can
use the special on-* handlers
to deal with the event: <ouch-button on-ouch="myMethod"></ouch-button>
{: .alert .alert-success }