Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Billions served, lessons learned: a Polymer story

Billions served, lessons learned: a Polymer story

Polymer went from an experiment to a Big Thing serving tons of users in major Google products and some of the biggest companies in the world in no time. Success is great! But there's always room for improvement. Mistakes were made. Lessons were learned. Regrets were had. Two years, hundreds of elements, and thousands of apps later, we've got a pretty good idea what makes an element and an app great, and what's changing in Polymer 2.0 as a result. Come hear what this means for the Polymer Elements, your elements and your apps, in a new episode of the Meownica show!

Monica Dinculescu

May 18, 2017
Tweet

More Decks by Monica Dinculescu

Other Decks in Programming

Transcript

  1. THE WEB IS LIKE A SHARK. IT HAS TO CONSTANTLY

    MOVE FORWARD OR IT DIES WOODY ALLEN
  2. THE WEB IS LIKE A SHARK. IT HAS TO CONSTANTLY

    MOVE FORWARD OR IT DIES POLYMER WOODY ALLEN
  3. <link rel="import" href="../polymer/polymer.html"> <dom-module id="paper-input"> <template> <style>...</style> <script> Polymer({ is:

    'paper-input', behaviors: [...], properties: {...}, listeners: {...}, hostAttributes: {...}, }); </script> </template> </dom-module>
  4. Lifecycle methods! class MyElement extends HTMLElement { constructor() { ...

    } connectedCallback() { ... } disconnectedCallback() { ... } attributeChangedCallback(attr, oldValue, newValue) { ... } static get observedAttributes() { return [...]; } }
  5. Lifecycle methods! class MyElement extends HTMLElement { constructor() { ...

    } connectedCallback() { ... } disconnectedCallback() { ... } attributeChangedCallback(attr, oldValue, newValue) { ... } static get observedAttributes() { return [...]; } }
  6. Lifecycle methods! class MyElement extends HTMLElement { constructor() { ...

    } connectedCallback() { ... } disconnectedCallback() { ... } attributeChangedCallback(attr, oldValue, newValue) { ... } static get observedAttributes() { return [...]; } }
  7. Lifecycle methods! class MyElement extends HTMLElement { constructor() { ...

    } connectedCallback() { ... } disconnectedCallback() { ... } attributeChangedCallback(attr, oldValue, newValue) { ... } static get observedAttributes() { return [...]; } }
  8. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } }
  9. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } Make the shadow root
  10. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } Add things to it
  11. render() { var button = document.createElement('button'); button.innerHTML = ''; button.addEventListener('click',

    this.increment.bind(this)); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); // Some styles for pretty. this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; }
  12. render() { var button = document.createElement('button'); button.innerHTML = ''; button.addEventListener('click',

    this.increment.bind(this)); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); // Some styles for pretty. this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; }
  13. render() { var button = document.createElement('button'); button.innerHTML = ''; button.addEventListener('click',

    this.increment.bind(this)); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); // Some styles for pretty. this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; }
  14. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } Update properties
  15. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } Optional: reflect attribute
  16. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } Actual useful code
  17. class MyElement extends HTMLElement { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } display() { this.output.innerHTML = ''.repeat(this.counter); } Actual useful code
  18. class MyElement extends HTMLElement { constructor() { ... } connectedCallback()

    { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } }
  19. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this.render(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } }
  20. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this.render(); this._enableProperties(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } } Turn on accessors
  21. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); } Ready!
  22. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); }
  23. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } attributeChangedCallback(attr, oldValue, newValue) { if (oldValue !== newValue) { this[attr] = newValue; } } get counter() { return this._counter; } set counter(value) { if (value != this._counter) { this._counter = parseInt(value); this.setAttribute('counter', value); this.display(); }
  24. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { if ('counter' in changedProps) { // Only reflect this this.setAttribute('counter', changedProps.counter); this.display(); } } } Actual useful code!
  25. class MyElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { ... } connectedCallback()

    { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { if ('counter' in changedProps) // Only reflect this this.setAttribute(‘counter’, changedProps.counter); this.display(); } } } MyHTMLElement.createPropertiesForAttributes(); Do the boilerplate!
  26. CSS/HTML in JS render() { var button = document.createElement('button'); button.innerHTML

    = ''; button.addEventListener('click', this.increment); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; }
  27. render() { var button = document.createElement('button'); button.innerHTML = ''; button.addEventListener('click',

    this.increment); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; } CSS/HTML in JS
  28. CSS/HTML in CSS/HTML render() { var button = document.createElement('button'); button.innerHTML

    = ''; button.addEventListener('click', this.increment); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; } <template id="my-template"> <style> :host { display: block; font-size: 30px; } span { margin-left: 10px; } button { background: transparent; font-size: inherit; border: none; cursor: pointer; } </style> <button on-click="increment"></button> <span id="output"></span> </template>
  29. CSS/HTML in CSS/HTML <template id="my-template"> <style> :host { display: block;

    font-size: 30px; } span { margin-left: 10px; } button { background: transparent; font-size: inherit; border: none; cursor: pointer; } </style> <button on-click="increment"></button> <span id="output"></span> </template> render() { var button = document.createElement('button'); button.innerHTML = ''; button.addEventListener('click', this.increment); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; }
  30. render() { var button = document.createElement('button'); button.innerHTML = ''; button.addEventListener('click',

    this.increment); this.shadowRoot.appendChild(button); this.output = document.createElement('span'); this.shadowRoot.appendChild(this.output); this.style.display = 'block'; this.style.fontSize = '30px'; this.output.style.marginLeft = '10px'; button.style.background = 'transparent'; button.style.fontSize = 'inherit'; button.style.border = 'none'; button.style.cursor = 'pointer'; } <template id="my-template"> <style> :host { display: block; font-size: 30px; } span { margin-left: 10px; } button { background: transparent; font-size: inherit; border: none; cursor: pointer; } </style> <button on-click="increment"></button> <span id="output"></span> </template> CSS/HTML in CSS/HTML
  31. <template id="my-template"> <style> :host { display: block; font-size: 30px; }

    span { margin-left: 10px; } button { background: transparent; font-size: inherit; border: none; cursor: pointer; } </style> <button on-click="increment"></button> <span id="output"></span> </template> Declarative CSS/HTML in CSS/HTML
  32. <template id="my-template"> <style> :host { display: block; font-size: 30px; }

    span { margin-left: 10px; } button { background: transparent; font-size: inherit; border: none; cursor: pointer; } </style> <button on-click="increment"></button> <span id="output"></span> </template> this.$.output CSS/HTML in CSS/HTML
  33. class MyHTMLElement extends Polymer.PropertyAccessors(HTMLElement) { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { ... } display() { ... } increment() { ... } render() { ... } } MyHTMLElement.createPropertiesForAttributes();
  34. class MyHTMLElement extends Polymer.TemplateStamp(Polymer.PropertyAccessors(HTMLElement)) { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { ... } display() { ... } increment() { ... } render() { ... } } MyHTMLElement.createPropertiesForAttributes();
  35. class MyHTMLElement extends Polymer.TemplateStamp(Polymer.PropertyAccessors(HTMLElement)) { constructor() { super(); this._counter =

    0; this.attachShadow({mode: 'open'}); } connectedCallback() { this._enableProperties(); } ready() { this.render(); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { ... } display() { ... } increment() { ... } render() { ... } } MyHTMLElement.createPropertiesForAttributes(); No more manual render!
  36. class MyHTMLElement extends Polymer.TemplateStamp(Polymer.PropertyAccessors(HTMLElement)) { constructor() { super(); this._counter =

    0; } connectedCallback() { this._enableProperties(); } ready() { super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { ... } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes();
  37. class MyHTMLElement extends Polymer.TemplateStamp(Polymer.PropertyAccessors(HTMLElement)) { constructor() { super(); this._counter =

    0; } connectedCallback() { this._enableProperties(); } ready() { this.dom = this._stampTemplate(someTemplate); this.attachShadow({mode: 'open'}).appendChild(this.dom); super.ready(); } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { ... } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes(); Stamp the template
  38. class MyHTMLElement extends Polymer.TemplateStamp(Polymer.PropertyAccessors(HTMLElement)) { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { if ('counter' in changedProps) this.setAttribute(‘counter’, changedProps.counter); this.display(); } } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes();
  39. class MyHTMLElement extends Polymer.PropertyEffects(HTMLElement)) { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { if ('counter' in changedProps) this.setAttribute(‘counter’, changedProps.counter); this.display(); } } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes();
  40. class MyHTMLElement extends Polymer.PropertyEffects(HTMLElement)) { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } _propertiesChanged(currentProps, changedProps, oldProps) { if ('counter' in changedProps) this.setAttribute(‘counter’, changedProps.counter); this.display(); } } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes();
  41. class MyHTMLElement extends Polymer.PropertyEffects(HTMLElement)) { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes();
  42. class MyHTMLElement extends Polymer.PropertyEffects(HTMLElement)) { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes(); MyHTMLElement.createReflectedProperty('counter'); Do the boilerplate!
  43. Data binding! <template id="my-template"> <style>/* ... */</style> <button on-click="increment"></button> <span

    id="output"></span> </template> display() { this.$.output.innerHTML = ''.repeat(this.counter); }
  44. Data binding! <template id="my-template"> <style>/* ... */</style> <button on-click="increment"></button> <span

    id="output">[[display(counter)]]</span> </template> display(c) { return ''.repeat(c); }
  45. class MyHTMLElement extends Polymer.PropertyEffects(HTMLElement)) { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes(); MyHTMLElement.createReflectedProperty('counter');
  46. class MyHTMLElement extends Polymer.Element { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes(); MyHTMLElement.createReflectedProperty('counter');
  47. class MyHTMLElement extends Polymer.Element { constructor() { ... } connectedCallback()

    { ... } ready() { ... } static get observedAttributes() { return ['counter', 'limit']; } display() { ... } increment() { ... } } MyHTMLElement.createPropertiesForAttributes(); MyHTMLElement.createReflectedProperty('counter');
  48. class MyHTMLElement extends Polymer.Element { static get properties() { return

    { counter: {type: Number, reflectToAttribute: true, value: 0}, limit: {type: Number} } } display() { ... } increment() { ... } }
  49. class MyHTMLElement extends Polymer.Element { static get is() { return

    'my-element'; } static get properties() { return { counter: {type: Number, reflectToAttribute: true, value: 0}, limit: {type: Number} } } display() { ... } increment() { ... } } Automatic template finding
  50. Automatic template finding <dom-module id="my-element"> <template> ... </template> <script> class

    MyHTMLElement extends Polymer.Element { ... } customElements.define(MyHTMLElement.is, MyHTMLElement); </script> </dom-module>
  51. Automatic template finding <dom-module id="my-element"> <template> ... </template> <script> class

    MyHTMLElement extends Polymer.Element { ... } customElements.define(MyHTMLElement.is, MyHTMLElement); </script> </dom-module>
  52. HTMLElement Getters and setters CSS in JS -> templates Data

    binding Everything + all the convenience 1 2 3 4 5
  53. POLYMER 2.0 kinda like jQuery for Web Components. In the

    sense that it helps you out not that it’s old and boring.
  54. class MyHTMLElement extends Polymer.Element { static get is() { return

    'my-element'; } static get properties() { return { counter: {type: Number, reflectToAttribute: true, value: 0}, limit: {type: Number} } } display() { ... } increment() { ... } }
  55. class MyHTMLElement extends Polymer.Element { static get is() { return

    'my-element'; } static get properties() { return { counter: {type: Number, reflectToAttribute: true, value: 0}, limit: {type: Number} } } display() { ... } increment() { ... } } Inheritance
  56. Base class <dom-module id="my-element"> <template> ... </template> <script> class MyHTMLElement

    extends Polymer.Element { static get is() { return 'my-element'; } display() {...} ... } customElements.define(MyHTMLElement.is, MyHTMLElement); </script> </dom-module>
  57. <dom-module id="other-element"> <script> class MyOtherElement extends MyHTMLElement { static get

    is() { return 'other-element'; } display(c) { return ''.repeat(c); } } customElements.define(MyOtherElement.is, MyOtherElement); </script> </dom-module> Child class Base Class
  58. <dom-module id="other-element"> <script> class MyOtherElement extends MyHTMLElement { static get

    is() { return 'other-element'; } display(c) { return ''.repeat(c); } } customElements.define(MyOtherElement.is, MyOtherElement); </script> </dom-module> Child class Override base class methods
  59. <dom-module id="other-element"> <template> <span id="footer">([[counter]])</span> </template> <script> class MyOtherElement extends

    MyHTMLElement { static get is() { return 'other-element'; } static get template() { var t = Polymer.DomModule.import(this.is, 'template'); let superT = MyHTMLElement.cloneNode(true); } customElements.define(MyOtherElement.is, MyOtherElement); </script> </dom-module> Child class Override base class template
  60. <dom-module id="other-element"> <template> <span id="footer">([[counter]])</span> </template> <script> class MyOtherElement extends

    MyHTMLElement { static get is() { return 'other-element'; } static get template() { let t = Polymer.DomModule.import(MyOtherElement.is, 'template'); let superT = MyHTMLElement.template.cloneNode(true); return t.content.insertBefore(t.content.firstChild, superT); } } customElements.define(MyOtherElement.is, MyOtherElement); </script> Child class Override base class template
  61. <app-route route="{{route}}" data="{{routeData}}" tail="{{subroute}}"> </app-route> <iron-pages selected="[[routeData.page]]"> <news-list id="list" name="list"

    route="[[subroute]]" category-name="{{categoryName}}" category="[[category]]" loading="[[loading]]" offline="[[offline]]" failure="[[failure]]"> </news-list> <news-article article-id="{{articleId}}" name="article" route="{{subroute}}" category-name="{{categoryName}}" category="[[category]]" article="[[article]]" loading="[[loading]]" offline="[[offline]]" failure="[[failure]]"> </news-article> </iron-pages>
  62. <app-route route="{{route}}" data="{{routeData}}" tail="{{subroute}}"> </app-route> <iron-pages selected="[[routeData.page]]"> <news-list id="list" name="list"

    route="[[subroute]]" category-name="{{categoryName}}" category="[[category]]" loading="[[loading]]" offline="[[offline]]" failure="[[failure]]"> </news-list> <news-article article-id="{{articleId}}" name="article" route="{{subroute}}" category-name="{{categoryName}}" category="[[category]]" article="[[article]]" loading="[[loading]]" offline="[[offline]]" failure="[[failure]]"> </news-article> </iron-pages>
  63. Just override! class MyHTMLElement extends Polymer.Element { static get is()

    { return 'my-element'; } attachDom(dom) { return this.attachShadow({mode:'open'}).appendChild(dom); } }
  64. Just override! class MyHTMLElement extends Polymer.Element { static get is()

    { return 'my-element'; } attachDom(dom) { return this.appendChild(dom); } }
  65. Just override! class MyHTMLElement extends Polymer.Element { static get is()

    { return 'my-element'; } static get template() { return ` <style> … </style> <button on-click="increment"></button> <span id=“output”>[[display(counter)]]</span> ` } }