Getting started
Installation
# Bower (legacy)
bower install Polymer/polymer#^2.0.0
# Polymer CLI
npm install -g polymer-cli
polymer init
polymer serve
Basic element
<dom-module id="x-custom">
<template>
<style>
:host {
display: block;
}
</style>
<h2>Hello [[name]]</h2>
</template>
<script>
class XCustom extends Polymer.Element {
static get is() {
return "x-custom";
}
static get properties() {
return {
name: { type: String, value: "World" },
};
}
}
customElements.define(XCustom.is, XCustom);
</script>
</dom-module>
Element name must contain a dash.
Lifecycle
constructor() {
super();
// Element created
}
connectedCallback() {
super.connectedCallback();
// Added to DOM
}
ready() {
super.ready();
// Properties set, local DOM ready
}
disconnectedCallback() {
super.disconnectedCallback();
// Removed from DOM
}
ready() is Polymer-specific and most commonly used.
Properties
Declaration
static get properties() {
return {
// Short form
user: String,
isHappy: Boolean,
// Full form
count: {
type: Number,
value: 0,
notify: true,
readOnly: true,
reflectToAttribute: true,
observer: '_countChanged'
},
// Computed property
fullName: {
type: String,
computed: 'computeFullName(first, last)'
}
};
}
Property options
| Option | Description |
|---|---|
type |
String, Number, Boolean, Object, Array, Date |
value |
Default value (use function for Object/Array) |
reflectToAttribute |
Sync property to attribute |
readOnly |
Creates _setProperty() setter |
notify |
Enables two-way binding, fires property-changed |
computed |
Computed property expression |
observer |
Observer method name |
Default values
static get properties() {
return {
// Primitives
name: { type: String, value: 'Guest' },
count: { type: Number, value: 0 },
// Objects/Arrays (use function!)
user: {
type: Object,
value: function() { return {}; }
},
items: {
type: Array,
value: function() { return []; }
}
};
}
Use function for Object/Array to avoid sharing instances.
Data Binding
One-way and two-way
<!-- One-way (downward) -->
<div>[[userName]]</div>
<my-el name="[[userName]]"></my-el>
<!-- Two-way (upward + downward) -->
<input value="{{userName}}" />
<my-el name="{{userName}}"></my-el>
Use [[]] for one-way, {{}} for two-way binding.
Attribute binding
<!-- Attribute binding (use $=) -->
<div class$="[[className]]"></div>
<a href$="[[url]]"></a>
<div style$="[[styleString]]"></div>
<label for$="[[inputId]]"></label>
<div data-id$="[[itemId]]"></div>
<!-- Boolean attributes -->
<button disabled$="[[isDisabled]]"></button>
<input required$="[[isRequired]]" />
Native attributes require $= suffix.
Computed bindings
<!-- Inline computation -->
<div>[[computeFullName(first, last)]]</div>
<div hidden$="[[!isVisible]]"></div>
<div>Total: [[add(price, tax)]]</div>
<!-- Method definition -->
<script>
computeFullName(first, last) {
return first + ' ' + last;
}
add(a, b) {
return a + b;
}
</script>
Polymer automatically tracks dependencies.
Binding to native properties
<!-- textContent -->
<span>[[text]]</span>
<!-- innerHTML (unsafe!) -->
<div inner-h-t-m-l="[[html]]"></div>
<!-- className -->
<div class-name="[[classes]]"></div>
Templates
dom-repeat
<!-- Basic repeater -->
<template is="dom-repeat" items="[[users]]">
<div>[[item.name]]</div>
</template>
<!-- Custom item/index names -->
<template is="dom-repeat" items="[[users]]" as="user" index-as="i">
<div>[[i]]: [[user.name]]</div>
</template>
<!-- Nested repeaters -->
<template is="dom-repeat" items="[[sections]]" as="section">
<h2>[[section.title]]</h2>
<template is="dom-repeat" items="[[section.items]]" as="item">
<div>[[item.name]]</div>
</template>
</template>
Filtering and sorting
<template
is="dom-repeat"
items="[[users]]"
filter="isEngineer"
sort="sortByName"
>
<div>[[item.name]]</div>
</template>
<script>
isEngineer(user) {
return user.role === 'engineer';
}
sortByName(a, b) {
return a.name.localeCompare(b.name);
}
// Re-run filter/sort
this.$.repeater.render();
</script>
dom-if
<!-- Basic conditional -->
<template is="dom-if" if="[[condition]]">
<div>Conditional content</div>
</template>
<!-- With restamp (destroy on false) -->
<template is="dom-if" if="[[condition]]" restamp>
<expensive-element></expensive-element>
</template>
<!-- else pattern -->
<template is="dom-if" if="[[isLoggedIn]]">
<div>Welcome back!</div>
</template>
<template is="dom-if" if="[[!isLoggedIn]]">
<div>Please log in</div>
</template>
Events
Declarative listeners
<!-- Click events -->
<button on-click="handleClick">Click</button>
<div on-tap="handleTap"></div>
<!-- Input events -->
<input on-input="handleInput" />
<input on-change="handleChange" />
<!-- Custom events -->
<my-el on-custom-event="handleCustom"></my-el>
<!-- With event details -->
<script>
handleClick(e) {
console.log('Clicked:', e.target);
console.log('Model:', e.model); // in dom-repeat
}
</script>
Event names are always lowercase.
Imperative listeners
ready() {
super.ready();
// Add listener
this.addEventListener('click', this._onClick);
// Remove listener
this.removeEventListener('click', this._onClick);
}
_onClick(e) {
console.log('Clicked!');
}
Custom events
// Fire custom event
fireCustomEvent() {
this.dispatchEvent(new CustomEvent('my-event', {
detail: { message: 'Hello', data: 123 },
bubbles: true,
composed: true // Cross shadow boundaries
}));
}
// Shorthand helper
fire(type, detail, options) {
this.dispatchEvent(new CustomEvent(type, {
detail,
bubbles: options.bubbles === undefined ? true : options.bubbles,
composed: options.composed === undefined ? true : options.composed
}));
}
Set composed: true to cross shadow DOM boundaries.
Observers
Simple observers
static get properties() {
return {
name: {
type: String,
observer: '_nameChanged'
}
};
}
_nameChanged(newValue, oldValue) {
console.log('Name:', oldValue, '->', newValue);
}
Simple observer for single property.
Complex observers
static get observers() {
return [
// Multiple dependencies
'userChanged(firstName, lastName)',
// Sub-property changes
'addressChanged(address.*)',
// Array mutations
'usersChanged(users.splices)',
// Mixed
'complexChange(user.*, items.splices)'
];
}
userChanged(first, last) {
console.log('User:', first, last);
}
addressChanged(changeRecord) {
console.log('Path:', changeRecord.path);
console.log('Value:', changeRecord.value);
}
usersChanged(changeRecord) {
if (changeRecord) {
changeRecord.indexSplices.forEach((s) => {
console.log('Added:', s.addedCount);
console.log('Removed:', s.removed.length);
});
}
}
Observer patterns
// Observe all sub-properties
"userChanged(user.*)";
// Observe array mutations
"itemsChanged(items.splices)";
// Observe specific sub-property
"nameChanged(user.name)";
// Multiple arguments
"compute(a, b, c.*, d.splices)";
Observers fire once all dependencies are defined.
Data manipulation
set and get
// Set property
this.name = "John";
// Set sub-property (notifies observers)
this.set("user.name", "John");
this.set("address.city", "NYC");
// Get by path
let name = this.get("user.name");
let city = this.get("address.city");
// Set multiple
this.setProperties({
name: "John",
age: 30,
});
Use set() for sub-properties to trigger updates.
Array mutations
// Push
this.push("users", { name: "John" });
// Pop
this.pop("users");
// Splice
this.splice("users", index, 1); // Remove 1
this.splice("users", index, 0, item); // Insert
this.splice("users", index, 1, item); // Replace
// Unshift
this.unshift("users", { name: "Jane" });
// Shift
this.shift("users");
Must use Polymer methods for array mutations.
notifyPath
// Manual mutation (bad - no update)
this.user.name = "John";
// Notify path (triggers update)
this.user.name = "John";
this.notifyPath("user.name");
// For arrays
this.users[0].name = "John";
this.notifyPath("users.0.name");
// Notify entire object changed
this.notifyPath("user");
Use when mutating objects/arrays directly.
Styling
Shadow DOM styles
/* Host element */
:host {
display: block;
background: white;
}
/* Host with class */
:host(.blue) {
color: blue;
}
/* Host state */
:host(:hover) {
background: gray;
}
:host([disabled]) {
opacity: 0.5;
}
/* Slotted content */
::slotted(*) {
color: red;
}
::slotted(.highlight) {
font-weight: bold;
}
::slotted(span) {
text-decoration: underline;
}
Styles are scoped to the element.
CSS custom properties
/* Define with defaults */
:host {
background: var(--my-el-bg, white);
color: var(--my-el-color, black);
padding: var(--my-el-padding, 16px);
}
.title {
font-size: var(--my-el-title-size, 24px);
}
External theming:
my-element {
--my-el-bg: blue;
--my-el-color: white;
--my-el-padding: 20px;
}
Shared styles
<!-- Define shared styles -->
<dom-module id="my-styles">
<template>
<style>
.red {
color: red;
}
.bold {
font-weight: bold;
}
</style>
</template>
</dom-module>
<!-- Include in element -->
<dom-module id="my-element">
<template>
<style include="my-styles">
/* Additional styles */
.blue {
color: blue;
}
</style>
<div class="red bold">Text</div>
</template>
</dom-module>
Style mixins
/* Define mixin */
:host {
--my-mixin: {
color: red;
font-weight: bold;
};
}
/* Apply mixin */
.title {
@apply --my-mixin;
}
External:
my-element {
--my-mixin: {
color: blue;
font-size: 20px;
};
}
Advanced
Mixins
// Define mixin
const MyMixin = (superClass) =>
class extends superClass {
static get properties() {
return {
mixinProp: String,
};
}
mixinMethod() {
console.log("From mixin");
}
};
// Use mixin
class MyElement extends MyMixin(Polymer.Element) {
static get is() {
return "my-element";
}
}
// Multiple mixins
class MyElement extends Mixin1(Mixin2(Mixin3(Polymer.Element))) {
static get is() {
return "my-element";
}
}
Helper elements
<!-- Array selector -->
<array-selector items="[[users]]" selected="{{selectedUser}}"> </array-selector>
<!-- Custom style -->
<custom-style>
<style>
html {
--theme-color: blue;
}
</style>
</custom-style>
<!-- dom-bind (template without element) -->
<dom-bind>
<template>
<div>[[message]]</div>
</template>
</dom-bind>
Property effects
static get properties() {
return {
// Read-only (creates _setCount)
count: {
type: Number,
readOnly: true
},
// Reflect to attribute
active: {
type: Boolean,
reflectToAttribute: true
},
// Notify changes (fires active-changed)
active: {
type: Boolean,
notify: true
}
};
}
// Use read-only setter
this._setCount(42);
Gotchas
Common pitfalls
CamelCase properties
// In JavaScript
firstName: String
// In HTML (dash-case)
<my-el first-name="John"></my-el>
Native attributes need $=
<!-- Wrong -->
<div class="[[myClass]]"></div>
<!-- Correct -->
<div class$="[[myClass]]"></div>
Array mutations
// Wrong (no update)
this.users.push({ name: "John" });
// Correct
this.push("users", { name: "John" });
Object/Array defaults
// Wrong (shared instance!)
items: { type: Array, value: [] }
// Correct
items: {
type: Array,
value: function() { return []; }
}
Performance tips
<!-- Use restamp for expensive conditionals -->
<template is="dom-if" if="[[show]]" restamp>
<expensive-element></expensive-element>
</template>
<!-- Detach for hidden repeaters -->
<template is="dom-repeat" items="[[items]]">
<template is="dom-if" if="[[item.visible]]">
<div>[[item.name]]</div>
</template>
</template>
Use restamp to fully destroy/recreate DOM.
Naming conventions
// Public methods
doSomething() {}
// Private methods (convention)
_internalMethod() {}
// Property observers
_propertyChanged() {}
// Event handlers
_onEventName() {}
handleEventName() {}
Also see
- Polymer 2 Documentation - Official documentation
- Lit - Recommended successor to Polymer
- Web Components - Underlying standards
- Custom Elements - Custom elements guide
- Shadow DOM - Shadow DOM guide