Usare i custom elements

Questa traduzione è incompleta. Aiutaci a tradurre questo articolo dall’inglese

Una delle caratteristiche chiave dei Web Components standard è la capacità di creare elementi personalizzati che contengono le funzionalità che vuoi sviluppare direttamente in una pagina HTML, anzichè che sviluppare una lunga serie di singoli elementi innestati per avere, in una pagina, le funzionalità che desideri. Questo articolo ti introduce all'uso delle Custom Elements API.

Nota: I custom elements sono supportati di default in Firefox, Chrome, e Edge (76). Opera e Safari fino ad ora supportano solo custom elements proprietari.

Panoramica generale

Il controller dei custom elements in un web document è l'oggetto CustomElementRegistry — questo oggetto ti permette di registrareun custom element nella pagina, ritornare informazioni su cosa restituire informazioni su quali elementi personalizzati sono registrati etc.

Per registrare un custom element nella pagina, usa il metodo CustomElementRegistry.define(). Questo metodo ha questi argomenti:

  • Il DOMString rappresenta il nome che vuoi dare all'elemento. Ricorda che i nomi dei custom elements richiedono un trattino (kebab-case); non possono essere una singole parole.
  • Un oggetto classe che definisce le funzionalità dell'elemento.
  • Opzionalmente, un oggetto contenente una proprietà extends, che specifica le caratteristiche dell'elemento che vengono ereditate nel custom element creato.

Per esempio, possiamo definire un custom word-count element come questo:

customElements.define('word-count', WordCount, { extends: 'p' });

L'elemento è chiamato word-count, la sua classe è WordCount, ed estende l'elemento <p>.

Una classe custom element viene scritta usando la sintassi standard ES 2015. Per esempio, WordCount è strutturata così:

class WordCount extends HTMLParagraphElement {
  constructor() {
    // Always call super first in constructor
    super();

    // Element functionality written in here

    ...
  }
}

This is just a simple example, but there is more you can do here. It is possible to define specific lifecycle callbacks inside the class, which run at specific points in the element's lifecycle. For example, connectedCallback is invoked each time the custom element is appended into a document-connected element, while attributeChangedCallback is invoked when one of the custom element's attributes is added, removed, or changed.

You'll learn more about these in the Using the lifecycle callbacks section below.

There are two types of custom elements:

  • Autonomous custom elements are standalone — they don't inherit from standard HTML elements. You use these on a page by literally writing them out as an HTML element. For example <popup-info>, or document.createElement("popup-info").
  • Customized built-in elements inherit from basic HTML elements. To create one of these, you have to specify which element they extend (as implied in the examples above), and they are used by writing out the basic element but specifying the name of the custom element in the is attribute (or property). For example <p is="word-count">, or document.createElement("p", { is: "word-count" }).

Working through some simple examples

At this point, let's go through some more simple examples to show you how custom elements are created in more detail.

Autonomous custom elements

Let's have a look at an example of an autonomous custom element — <popup-info-box> (see a live example). This takes an image icon and a text string, and embeds the icon into the page. When the icon is focused, it displays the text in a pop up information box to provide further in-context information.

To begin with, the JavaScript file defines a class called PopUpInfo, which extends HTMLElement. Autonomous custom elements nearly always extend HTMLElement.

class PopUpInfo extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

    // write element functionality in here

    ...
  }
}

The preceding code snippet contains the constructor() definition for the class, which always starts by calling super() so that the correct prototype chain is established.

Inside the constructor, we define all the functionality the element will have when an instance of it is instantiated. In this case we attach a shadow root to the custom element, use some DOM manipulation to create the element's internal shadow DOM structure — which is then attached to the shadow root — and finally attach some CSS to the shadow root to style it.

// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});

// Create spans
var wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
var icon = document.createElement('span');
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
var info = document.createElement('span');
info.setAttribute('class','info');

// Take attribute content and put it inside the info span
var text = this.getAttribute('text');
info.textContent = text;

// Insert icon
var imgUrl;
if(this.hasAttribute('img')) {
  imgUrl = this.getAttribute('img');
} else {
  imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);

// Create some CSS to apply to the shadow dom
var style = document.createElement('style');

style.textContent = '.wrapper {' +
// CSS truncated for brevity

// attach the created elements to the shadow dom

shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

Finally, we register our custom element on the CustomElementRegistry using the define() method we mentioned earlier — in the parameters we specify the element name, and then the class name that defines its functionality:

customElements.define('popup-info', PopUpInfo);

It is now available to use on our page. Over in our HTML, we use it like so:

<popup-info img="img/alt.png" text="Your card validation code (CVC)
  is an extra security feature — it is the last 3 or 4 numbers on the
  back of your card."></popup-info>

Note: You can see the full JavaScript source code here.

Note: Remember that for the custom element to work, the script that registers it has to be loaded after the DOM is parsed. This can be done either by including the <script> element at the bottom of the <body>, or by including the defer attribute in your <script> element.

Internal vs. external styles

In the above example we apply style to the Shadow DOM using a <style> element, but it is perfectly possible to do it by referencing an external stylesheet from a <link> element instead.

For example, take a look at this code from our popup-info-box-external-stylesheet example (see the source code):

// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');

// Attach the created element to the shadow dom
shadow.appendChild(linkElem);

Note that <link> elements do not block paint of the shadow root, so there may be a flash of unstyled content (FOUC) while the stylesheet loads.

Many modern browsers implement an optimization for <style> tags either cloned from a common node or that have identical text, to allow them to share a single backing stylesheet. With this optimization the performance of external and internal styles should be similar.

Customized built-in elements

Now let's have a look at another customized built in element example — expanding-list (see it live also). This turns any unordered list into an expanding/collapsing menu.

First of all, we define our element's class, in the same manner as before:

class ExpandingList extends HTMLUListElement {
  constructor() {
    // Always call super first in constructor
    super();

    // write element functionality in here

    ...
  }
}

We will not explain the element functionality in any detail here, but you can discover how it works by checking out the source code. The only real difference here is that our element is extending the HTMLUListElement interface, and not HTMLElement. So it has all the characteristics of a <ul> element with the functionality we define built on top, rather than being a standalone element. This is what makes it a customized built-in, rather than an autonomous element.

Next, we register the element using the define() method as before, except that this time it also includes an options object that details what element our custom element inherits from:

customElements.define('expanding-list', ExpandingList, { extends: "ul" });

Using the built-in element in a web document also looks somewhat different:

<ul is="expanding-list">

  ...

</ul>

You use a <ul> element as normal, but specify the name of the custom element inside the is attribute.

Note: Again, you can see the full JavaScript source code here.

Using the lifecycle callbacks

You can define several different callbacks inside a custom element's class definition, which fire at different points in the element's lifecycle:

  • connectedCallback: Invoked each time the custom element is appended into a document-connected element. This will happen each time the node is moved, and may happen before the element's contents have been fully parsed.

    Note: connectedCallback may be called once your element is no longer connected, use Node.isConnected to make sure.

  • disconnectedCallback: Invoked each time the custom element is disconnected from the document's DOM.
  • adoptedCallback: Invoked each time the custom element is moved to a new document.
  • attributeChangedCallback: Invoked each time one of the custom element's attributes is added, removed, or changed. Which attributes to notice change for is specified in a static get observedAttributes method

Let's look at an example of these in use. The code below is taken from the life-cycle-callbacks example (see it running live). This is a trivial example that simply generates a colored square of a fixed size on the page. The custom element looks like this:

<custom-square l="100" c="red"></custom-square>

The class constructor is really simple — here we attach a shadow DOM to the element, then attach empty <div> and <style> elements to the shadow root:

var shadow = this.attachShadow({mode: 'open'});

var div = document.createElement('div');
var style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);

The key function in this example is updateStyle() — this takes an element, gets its shadow root, finds its <style> element, and adds width, height, and background-color to the style.

function updateStyle(elem) {
  const shadow = elem.shadowRoot;
  shadow.querySelector('style').textContent = `
    div {
      width: ${elem.getAttribute('l')}px;
      height: ${elem.getAttribute('l')}px;
      background-color: ${elem.getAttribute('c')};
    }
  `;
}

The actual updates are all handled by the life cycle callbacks, which are placed inside the class definition as methods. The connectedCallback() runs each time the element is added to the DOM — here we run the updateStyle() function to make sure the square is styled as defined in its attributes:

connectedCallback() {
  console.log('Custom square element added to page.');
  updateStyle(this);
}

The disconnectedCallback() and adoptedCallback() callbacks log simple messages to the console to inform us when the element is either removed from the DOM, or moved to a different page:

disconnectedCallback() {
  console.log('Custom square element removed from page.');
}

adoptedCallback() {
  console.log('Custom square element moved to new page.');
}

The attributeChangedCallback() callback is run whenever one of the element's attributes is changed in some way. As you can see from its properties, it is possible to act on attributes individually, looking at their name, and old and new attribute values. In this case however, we are just running the updateStyle() function again to make sure that the square's style is updated as per the new values:

attributeChangedCallback(name, oldValue, newValue) {
  console.log('Custom square element attributes changed.');
  updateStyle(this);
}

Note that to get the attributeChangedCallback() callback to fire when an attribute changes, you have to observe the attributes. This is done by specifying a static get observedAttributes() method inside custom element class - this should return  an array containing the names of the attributes you want to observe:

static get observedAttributes() { return ['c', 'l']; }

This is placed right at the top of the constructor, in our example.

Note: Find the full JavaScript source here.

Polyfills vs. classes

Custom Element polyfills may patch native constructors such as HTMLElement and others, and return a different instance from the one just created.

If you need a constructor and a mandatory super call, remember to pass along optional arguments and return the result of such a super call operation.

class CustomElement extends HTMLElement {
  constructor(...args) {
    const self = super(...args);
    // self functionality written in here
    // self.addEventListener(...)
    // return the right context
    return self;
  }
}

If you don't need to perform any operation in the constructor, you can simply omit it so that its native behavior (see following) will be preserved.

 constructor(...args) { return super(...args); }

Transpilers vs. classes

Please note that ES2015 classes cannot reliably be transpiled in Babel 6 or TypeScript targeting legacy browsers. You can either use Babel 7 or the babel-plugin-transform-builtin-classes for Babel 6, and target ES2015 in TypeScript instead of legacy.

Libraries

There are several libraries that are built on Web Components with the aim of increasing the level of abstraction when creating custom elements. Some of these libraries are snuggsi ツX-TagSlim.js, LitElementSmart, and Stencil.