From 1f54006aacbc61878f4790aec949c2de7a59ff19 Mon Sep 17 00:00:00 2001 From: Eric Bidelman Date: Mon, 27 Jun 2016 16:09:38 -0700 Subject: [PATCH] Custom elements v1 article --- .../primers/customelements/index.markdown | 873 ++++++++++++++++++ src/static/styles/_global.scss | 1 + .../partials/components/_wf-talkinghead.scss | 24 + 3 files changed, 898 insertions(+) create mode 100644 src/content/en/fundamentals/primers/customelements/index.markdown create mode 100644 src/static/styles/partials/components/_wf-talkinghead.scss diff --git a/src/content/en/fundamentals/primers/customelements/index.markdown b/src/content/en/fundamentals/primers/customelements/index.markdown new file mode 100644 index 00000000000..9b7976c77f7 --- /dev/null +++ b/src/content/en/fundamentals/primers/customelements/index.markdown @@ -0,0 +1,873 @@ +--- +layout: shared/narrow +title: "Using custom elements to build reusable web components" +description: "Custom elements allow web developers to define new HTML tags, extend existing ones, and create reusable web components." +published_on: 2016-06-29 +updated_on: 2016-06-29 +authors: + - ericbidelman +translation_priority: 1 +order: 4 +notes: + extend: + - "Some browsers have expressed distaste for implementing the is=\"\" syntax. This is unfortunate for accessibility and progressive enhancement. If you think extending native HTML elements is useful, voice your thoughts on Github." + children: + - Overwriting an element's children with new content is generally not a good idea because it's unexpected. Users would be surprised to have their markup thrown out. A better way to add element-defined content is to use shadow DOM, which we'll talk about next. +--- + + + + + +
+ Already familiar with custom elements? +

This article describes the new Custom Elements v1 spec. If you've been using custom elements, chances are you're familiar with the v0 version shipped in Chrome 33. The concepts are the same, but the v1 spec has important API differences. Keep reading too see what's new or check out the section on History and browser support for more info.

+
+ +### TL;DR {#tldr} + +With [Custom Elements][spec], web developers can **create new HTML tags**, +beef-up existing HTML tags, or extend the components other developers have authored. +The API is the foundation of [web components](http://webcomponents.org/). It brings +a web standards-based way to create reusable components using nothing more than +vanilla JS/HTML/CSS. The result is less code, modular code, and more reuse in our apps. + +## Introduction {#intro} + +{% include shared/toc.liquid %} + +The browser gives us an excellent tool for structuring web applications. +It's called HTML. You may have heard of it! It's declarative, portable, well supported, and easy to work with. Great as HTML may be, its vocabulary and extensibility is limited. [HTML living standard](https://html.spec.whatwg.org/multipage/) lacks a way to automatically associate JS behavior with your markup...until now. + + +Custom elements are the answer to modernizing HTML; filling in the missing pieces, +and bundling structure with behavior. If HTML doesn't provide the solution to a problem, +we can create a custom element that does. **Custom elements teach the browser new tricks while preserving the benefits of HTML**. + +## Defining a new element {#define} + +To define a new HTML element we need the power of JavaScript! + +The `customElements` interface is used for defining a custom element and teaching +the browser about a new tag. Call `customElements.define()` with the tag name you want +to create and a JavaScript `class` that extends the base `HTMLElement`. + +**Example** - defining a mobile drawer panel, ``: + +{% highlight javascript %} +class AppDrawer extends HTMLElement {...} +window.customElements.define('app-drawer', AppDrawer); + +// Or use an anonymous class if you don't want a named constructor in current scope. +window.customElements.define('app-drawer', class extends HTMLElement {...}); +{% endhighlight %} + +Example usage: + +{% highlight html %} + +{% endhighlight %} + +It's important to remember that using a custom element is no different than using a `
`. +Instances can be declared on the page, created dynamically in JavaScript, event listeners can be attached, +etc. Keep reading for more examples. + +### Defining an element's JavaScript API {#jsapi} + +The functionality of a custom element is defined using an ES2015 `class` which extends `HTMLElement`. +Extending `HTMLElement` ensures the custom element inherits the entire DOM API and +means any properties/methods that you add to the class become part of the element's DOM interface. +Essentially, use the class to create a **public JavaScript API** for your tag. + +**Example** - defining the DOM interface of ``: + +{% highlight javascript %} +class AppDrawer extends HTMLElement { + + // A getter/setter for an open property. + get open() { + return this.hasAttribute('open'); + } + + set open(val) { + // Reflect the value of the open property as an HTML attribute. + if (val) { + this.setAttribute('open', ''); + } else { + this.removeAttribute('open'); + } + this.toggleDrawer(); + } + + // A getter/setter for a disabled property. + get disabled() { + return this.hasAttribute('disabled'); + } + + set disabled(val) { + // Reflect the value of the disabled property as an HTML attribute. + if (val) { + this.setAttribute('disabled', ''); + } else { + this.removeAttribute('disabled'); + } + } + + // Can define constructor arguments if you wish. + constructor() { + // If you define a ctor, always call super() first! + // This is specific to CE and required by the spec. + super(); + + // Setup a click listener on itself. + this.addEventListener('click', e => { + // Don't toggle the drawer if it's disabled. + if (this.disabled) { + return; + } + this.toggleDrawer(); + }); + } + + toggleDrawer() { + ... + } +} + +customElements.define('app-drawer', AppDrawer); +{% endhighlight %} + +In this example, we're creating a drawer that has a `open` property, `disabled` property, +and a `toggleDrawer()` method. It also [reflects properties as HTML attributes](#reflectattr). + +A neat feature of custom elements is that **`this` inside a class definition refers to +the DOM element itself**. In our example, `this` refers to ``. This (😉) is how the element can attach a `click` listener to itself! And you're not limited to event listeners. The entire DOM API is available inside element code. Use `this` to access the element's properties, inspect its children (`this.children`), query nodes (`this.querySelectorAll('.items')`), etc. + +**Rules on creating custom elements** + +1. The name of a custom element **must contain a dash (-)**. So ``, ``, and `` are all valid names, while `` and `` are not. This requirement is so the HTML parser can distinguish custom elements from regular elements. It also ensures forward compatibility when new tags are added to HTML. +2. You can't register the same tag more than once. Attempting to do so will throw a `DOMException`. Once you've told the browser about a new tag, that's it. No take backs. +3. Custom elements cannot be self-closing because HTML only allows a few elements to be self-closing. Always declare a closing tag (<app-drawer></app-drawer>". + +## Extending elements {#extend} + +The Custom Elements API is useful for creating new HTML elements, but it's also +useful for extending other custom elements or even the browser's built-in HTML. + +### Extending a custom element {#extendcustomeel} + +Extending another custom element is done by extending its class definition. + +**Example** - create `` that extends ``: + +{% highlight javascript %} +class FancyDrawer extends AppDrawer { + constructor() { + super(); // always call super() first in the ctor. + ... + } + + toggleDrawer() { + // Possibly different toggle implementation? + // Use ES2015 if you need to call the parent method. + // super.toggleDrawer() + } + + anotherMethod() { + ... + } +} + +customElements.define('fancy-app-drawer', FancyDrawer); +{% endhighlight %} + +### Extending native HTML elements {#extendhtml} + +Let's say you wanted to create a fancier ` +{% endhighlight %} + +create an instance in JavaScript: + +{% highlight javascript %} +// Custom elements overloads createElement() to support the is="" attribute. +let button document.createElement('button', {is: 'fancy-button'}); +button.textContent = 'Fancy button!'; +button.disabled = true; +document.body.appendChild(button); +{% endhighlight %} + +use the `new` operator: + +{% highlight javascript %} +let button = new FancyButton(); +button.textContent = 'Fancy button!'; +button.disabled = true; +{% endhighlight %} + +Here's another example that extends ``. + +**Example** - extending ``: + +{% highlight javascript %} +customElements.define('bigger-img', class extends Image { + constructor(width, height) { + super(width * 10, height * 10); + } +}, {extends: 'img'}); +{% endhighlight %} + +Users declare this component as: + +{% highlight html %} + + +{% endhighlight %} + +or create an instance in JavaScript: + +{% highlight html %} +const BiggerImage = customElements.get('bigger-img'); +const image = new BiggerImage(15, 20); // pass ctor values like so. +console.assert(image.width === 150); +console.assert(image.height === 200); +{% endhighlight %} + +{% include shared/note.liquid list=page.notes.extend %} + +## Custom element reactions {#reactions} + +A custom element can define special lifecycle hooks for running code during +interesting times of its existence. These are called **custom element reactions**. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameCalled when
constructorAn instance of the element is created or upgraded. Useful for initializing state, settings up event listeners, or creating shadow dom. See the spec for restrictions on what you can do in the constructor.
connectedCallbackCalled every time the element is inserted into the DOM. Useful for running setup code, such as fetching resources or rendering. Generally, you should try to delay work until this time.
disconnectedCallbackCalled every time the element is removed from the DOM. Useful for running clean up code (removing event listeners, etc.).
attributeChangedCallback(attrName, oldVal, newVal)An attribute was added, removed, updated, or replaced. Also called for initial values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes property will receive this callback.
+ +The browser calls the `attributeChangedCallback()` for any attributes whitelisted +in the `observedAttributes` array (see [Observing changes to attributes](#attrchanges)). +Essentially, this is a performance optimization. When users change a common +attribute like `style` or `class`, you don't want to be spammed with tons of callbacks. + +**Reaction callbacks are synchronous**. If someone calls `el.setAttribute(...)` +on your element, the browser will immediately call `attributeChangedCallback()`. Similarly, +you'll receive a `disconnectedCallback()` right after your element is removed from +the DOM (e.g. the user calls `el.remove()`). + +**Example:** adding custom element reactions to ``: + +{% highlight javascript %} +class AppDrawer extends HTMLElement { + constructor() { + super(); // always call super() first in the ctor. + ... + } + connectedCallback() { + ... + } + disconnectedCallback() { + ... + } + attributeChangedCallback(attrName, oldVal, newVal) { + ... + } +} +{% endhighlight %} + +Define reactions if/when it make senses. If your element is sufficiently complex and opens a connection to IndexedDB in `connectedCallback()`, do the necessary cleanup work in `disconnectedCallback()`. But be careful! You can't rely on your element being removed from the DOM in all circumstances. For example, `disconnectedCallback()` will never be called if the user closes the tab. + +## Properties and attributes + +### Reflecting properties to attributes {#reflectattr} + +It's common for HTML properties to reflect their value back to the DOM as an HTML attribute. +For example, when the values of `hidden` or `id` are changed in JS: + +{% highlight javascript %} +div.id = 'my-id'; +div.hidden = true; +{% endhighlight %} + +the values are applied to the live DOM as attributes: + +{% highlight html %} +