NexusCS

Polymer 2

Web Frameworks
Polymer 2 is a library for building web components using the Web Components standards. Note: Polymer is in maintenance mode — Lit is recommended for new projects.
polymer
web-components
shadow-dom
custom-elements

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