This article is the first part of a small series of articles on creating web components using native HTML and JS tools.
The component approach to the development of web applications relies on the creation of independent code modules that can be reused, combined on a common basis, and also have the ability to store and restore their states, interact with other components and do not depend on other components.
To implement this approach, three specifications are currently being developed, the first of which will be discussed in this article. So, we get acquainted - the specification of custom elements (custom elements) , the working draft of which is republished on 13.10.2016 and the latest version of which is dated 12/04/2017.
The user element is the most important part of the API, included in the web component package, because it provides the key features, namely:
The CustomElementRegistry interface is responsible for creating custom web page elements, which allows you to register elements, returns information about registered elements, etc. This interface is available to us as a window.customElement object, which has three methods that interest us:
The published version of the specification offers the creation of custom elements in one of two forms: an autonomous custom element (autonomous custom element) and a customized built-in element.
An autonomous user element has no features, its use, according to the specification, is expected in phrase and streaming content, it can receive any attributes, except for the is attribute, which will be discussed later. The DOM interface of such an element should be determined by the author, since element inherits from HTMLElement.
In turn, a custom inline element must be defined with the extends property. The created custom element thus has the opportunity to inherit the semantics of the element specified by the value of the extends property. The need for this feature authors specification due to the fact that not all existing behavior of HTML elements can be duplicated using only stand-alone elements.
Differences are also noticeable in the syntax of declaring elements and their use, but it is much easier to look at examples (that is, later in the text of this article).
Customized embedded elements have an is attribute, which takes into account the name of a customized embedded element (according to which it was declared).
Naturally, the is attribute, if it is declared on an autonomous element (which, according to the specification, cannot be done), there will be no effect.
Otherwise, the attributes for both kinds of elements can be any, provided they are XML-compatible (correspond to www.w3.org/TR/xml/#NT-Name and do not contain U + 003A - colons) and do not contain ASCII capital letters ( https://html.spec.whatwg.org/multipage/infrastructure.html#uppercase-ascii-letters).
The definition of a custom item includes:
Name
In accordance with the current specification, the names are valid if they correspond to the following:
[az] (PCENChar)* '-' (PCENChar)* PCENChar ::= "-" | "." | [0-9] | "_" | [az] | #xB7 | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x203F-#x2040] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
If it is easier, they start with an ASCII small letter, do not contain capital letters, and are separated by at least one hyphen.
')
Names cannot have the following values: annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph.
Local name
For a standalone user element, this name is from the definition (defined name), and for a custom built-in element, the value passed to its extends option (while the name from the definition is used as the is value of the attribute)
Constructor
A constructor is called when an instance is created or upgraded, suitable for initializing a state, setting observers or creating a shadow dom. However, there are some limitations . So, the first call in the body of the constructor should be a call to super () with no parameters; the return keyword should not appear in the body of the constructor, unless it is a normal early return (return or return this); should not call document.write () or document.open (), descendants and attributes should not be created at this stage, nor should they be accessed; there should be only the work that is really needed only once, and all other work should, if possible, be brought into connectedCallback (see below).
Prototype, JS object
List observedAttributes
The list of those attributes that change will result in calling the attributeChangedCallback method (see later). It is determined by a static getter, which should return an array of string values.
Collection of life cycle methods
4 methods are presented that correspond to the component life cycle:
is called every time an element is injected into the DOM. It is appropriate to request resources and render. Most of the work is better to put off on this method;
it is called each time an element is removed from the DOM and is used to free memory (cancel requests, cancel intervals, timers and handlers, etc.);
called when the item was moved to a new document, for example, by calling document.adoptNode ();
called each time when adding, changing or replacing attributes included in the observedAttributes list - this method will be called with three arguments: the name of the attribute that has changed, its old value and its new value.
They may not be assigned, because the specification provides for their value either as a function or as null. All the set forth callbacks are called synchronously.
Construction stack
an initially empty list, modified by the upgrade an element algorithm and HTML element constructors, whose every entry will then be either an element or a marker already created.
The minimal syntax is simple:
Creates a class that extends the class HTMLElement. The markup of the future component is specified in this.innerHTML inside the connectedCallback.
class AcEl extends HTMLElement { connectedCallback() { this.innerHTML = `<p>I'm an autonomous custom element</p>`; } }
After declaring a class, the element must be determined by calling:
customElements.define('ac-el', AcEl);
An example with the addition of the simplest behavior:
class TimerElement extends HTMLElement { connectedCallback() { this.render(); this.interval = setInterval(() => this.render(), 1000); } disconnectedCallback() { clearInterval(this.interval); // } render() { this.innerHTML = ` <div>${new Date().toLocaleString({hour: '2-digit', minute: '2-digit', second: '2-digit' })}</div> `; } } customElements.define('timer-element', TimerElement);
The use of autonomous custom elements is possible as by specifying them as a tag:
<timer-element></timer-element>
or
const timer = document.createElement('timer-element');
or
const timer = new TimerElement(); document.body.appendChild(timer);
After watching the timer work in the developer's tools, you can see that the page is not overloaded, changes are made to the DOM pointwise. Very similar to the reactor
For informational purposes, I planned to create a custom tab element through the whole cycle of articles. At this stage, the technical task looks quite simple. Tabs must consist of an arbitrary number of tabs (provided that the number of navigation elements corresponds to the number of tabs).
* Looking ahead, encapsulating styles, and providing more freedom in setting the contents of both tabs and shortcuts will be discussed in the next article.
So, I plan to create three custom elements: a navigation item, a content item, and a wrapper item. The navigation element will accept the target attribute, its contents will associate the element with the corresponding navigation element and, at the same time, will be displayed as the text of the navigation element. Implementation:
class TabNavigationItem extends HTMLElement { constructor() { super(); // this._target = null; } connectedCallback() { this.render(); // } static get observedAttributes() { return ['target']; } // attributeChangedCallback attributeChangedCallback(attr, prev, next) { if(prev !== next) { this[`_${attr}`] = next; this.render(); } } // render() { if(!this.ownerDocument.defaultView) return; this.innerHTML = ` <a href="#${this._target}">${this._target}</a> `; } }
The class for creating a taba element should have a target attribute, whose value should associate the element with the navigation element as well as the attribute where in this case the contents of the taba will be transferred (until the content is transferred to the attribute - the user is severely limited in using tabs, but the implementation of a more flexible approach we will implement in the next article).
class TabContentItem extends HTMLElement { constructor() { super(); this._target = null; this._content = null; } connectedCallback() { this.render(); } static get observedAttributes() { return ['target', 'content']; } attributeChangedCallback(attr, prev, next) { if(prev !== next) { this[`_${attr}`] = next; this.render(); } } render() { if(!this.ownerDocument.defaultView) return; this.innerHTML = ` <div>${this._content}</div> `; } }
The wrapper element itself will contain the functional logic - it will receive all the navigation elements and hang handlers on the click event, which the _target property will define and show us the desired taboo.
class TabElement extends HTMLElement { connectedCallback() { this.listener = this.showTab.bind(this); this.init(); } disconnectedCallback(){ this.navs.forEach(nav => nav.removeEventListener('click', this.listener)); } showTab(e) { e.preventDefault(); e.stopImmediatePropagation(); const target = e.target.closest('tab-nav-item')._target; [...this.tabs, ...this.navs].forEach(el => { if (el._target === target) el.classList.add('active'); else el.classList.remove('active'); }); } init() { this.navs = this.querySelectorAll('tab-nav-item'); this.tabs = this.querySelectorAll('tab-content-item'); this.navs.forEach(nav => nav.addEventListener('click', this.listener)); } }
* update, fixed handler removal
The last but most significant step is the declaration of elements:
customElements.define('tab-element', TabElement); customElements.define('tab-nav-item', TabNavigationItem); customElements.define('tab-content-item', TabContentItem);
An example of working tabs can be found here
Custom built-in elements have two differences from stand-alone custom elements: an element can inherit the built-in classes of HTML elements, and when declaring such an element, the third argument of the .define () method becomes mandatory.
Consider this example:
class JumpingButton extends HTMLButtonElement { constructor() { super(); this.addEventListener("hover", () => { // animate here }); } } customElements.define('jumping-button', JumpingButton, { extends: 'button' });
Such an element will inherit the semantics of HTMLButtonElement and will be able to expand it.
When using an element, the embedded HTML element tag will be specified, in our example, a button with an is attribute that will be passed the name from the definition of the custom element, thus:
<button is="jumping-button">Click Me!</button>
and its creation by js methods will look like this:
const jb = document.createElement("button", { is: "jumping-button" });
It is advisable not to forget that the .localName of such an element will be a “button”, unlike a stand-alone user element, for which .localName is equal to the name from the definition.
To date, not a single browser has implemented customized built-in elements, so for the time being it is theoretically necessary to consider examples.
Since adding a custom element definition to the CustomElementRegistry (on our part, it depends on calling the define () method) can occur at any time, a regular (non-custom) element can be created, after which it can later become a custom element after registering the corresponding definition (call .define ()). The upgrade algorithm provides for the course of events in which it may be preferable to register the definition of a user element after the corresponding element was originally created. This allows you to implement progressive enhancement of the content of custom elements. Agrads, however, are available only for elements in the DOM tree (i.e., for the shadow DOM, the shadowRoot must be in the document).
Next article: Web components. Part 2: Shadow DOM
The third article: Web components. Part 3: html templates and imports
Please do not judge strictly. Regards Tania_N
Source: https://habr.com/ru/post/349366/