Welcome to wechat public account: Front Reading Room
Web Components allows developers to create tags that enhance existing HTML tags and extend tags written by other developers.
It provides a web standards-based way to write reusable components in small amounts of modular code.
Define a new element
Use Windows. CustomElements. Define can define a new element.
Its first argument is the tag name and its second argument is a class that inherits HTMLElement.
class AppDrawer extends HTMLElement {}
window.customElements.define('app-drawer', AppDrawer);
Copy the code
So how do you use it? You just use it like a normal HTML tag!
<! DOCTYPEhtml>
<html>
<head>
<title>Web Components</title>
</head>
<body>
<app-drawer>app-drawer</app-drawer>
<script>
class AppDrawer extends HTMLElement {}
window.customElements.define("app-drawer", AppDrawer);
</script>
</body>
</html>
Copy the code
Custom elements are no different from normal HTML elements. An instance of it can be declared in a page and then defined using JS. It can also use features of HTML elements such as event listeners. More on this later.
JS API for custom elements
The custom element function is implemented using ES2015 Class, which inherits HTMLElement, so it has a full DOM API. This also means that the properties and methods in JS become part of the DOM interface, just as we are using JS to create apis for tags.
Example:
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();
}
// Can define constructor arguments if you wish.
constructor() {
// If you define a constructor, always call super() first!
// This is specific to CE and required by the spec.
super(a);// Setup a click listener on <app-drawer> itself.
this.addEventListener('click'.e= > {
this.toggleDrawer();
});
}
toggleDrawer() {
}
}
customElements.define('app-drawer', AppDrawer);
Copy the code
In this example, we create an app-drawer element with an open attribute and a toggleDrawer() method.
In the class syntax that defines elements, this refers to the element itself. In this case it can get properties and listen for events. It works with all other DOM apis, such as getting children(this.children), selecting elements (this.querySelectorAll(‘.items’)), and so on.
Custom element naming rules
-
Dashes (-) must be included to distinguish custom elements from HTML elements and to ensure compatibility (even if HTML adds new tags, there will be no conflicts)
-
Do not register the same tag twice, otherwise DOMException will be thrown, because this is completely unnecessary.
-
Self-closing tags are not supported because only some tags are allowed to be self-closing in the HTML specification. So only
is correct.
Custom Element Reactions (Custom Element Reactions)
The custom element has its lifecycle hooks. The list is as follows:
The name of the | trigger |
---|---|
constructor | Executed when an instance is created or upgraded, usually to initialize some state, set event listeners, or create a shadow DOM |
connectedCallback | Triggered when an element is added to the DOM, this is usually the time for data requests and so on |
disconnectedCallback | Emitted when an element is removed from the DOM, usually doing some cleanup |
attributeChangedCallback(attrName, oldVal, newVal) | Triggered when a listener attribute (in the observedAttributes attribute list) is added, deleted, updated, or replaced. Also fired when the element is initialized by the parser to create or upgrade the corresponding attribute value |
adoptedCallback | Triggered when the element is moved to a new document (e.g. Document.adoptnode (el)) |
Example:
class AppDrawer extends HTMLElement {
constructor() {
super(a);// always call super() first in the constructor.
}
connectedCallback(){}disconnectedCallback(){}attributeChangedCallback(attrName, oldVal, newVal){}}Copy the code
The callback function is synchronous. For example, when el.setAttribute() is called, attributeChangedCallback() is executed immediately.
These callbacks are not reliable in all cases, such as disconnectedCallback not executing when the user closes the TAB.
JS and HTML attributes
Set HTML attributes using JS
It is common to use JS to set HTML attributes, such as execution in JS
div.id = 'my-id';
div.hidden = true;
Copy the code
The HTML attribute will become
<div id="my-id" hidden>
Copy the code
This is a very useful feature. Let’s say you want to implement styles that change based on the JS state.
In this example, we can toggle the transparency of the app-drawer element and set some properties for it by clicking.
<! DOCTYPEhtml>
<html>
<head>
<title>Web Components</title>
</head>
<style>
app-drawer[disabled] {
opacity: 0.5;
}
app-drawer {
opacity: 1;
}
</style>
<body>
<app-drawer>app-drawer</app-drawer>
<script>
class AppDrawer extends HTMLElement {
static get observedAttributes() {
return ["disabled"];
}
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(val) {
if (val) {
this.setAttribute("disabled"."");
} else {
this.removeAttribute("disabled"); }}// Only called for the disabled and open attributes due to observedAttributes
attributeChangedCallback(name, oldValue, newValue) {
// When the drawer is disabled, update keyboard/screen reader behavior.
if (this.disabled) {
this.setAttribute("tabindex"."1");
this.setAttribute("aria-disabled"."true");
} else {
this.setAttribute("tabindex"."0");
this.setAttribute("aria-disabled"."false");
}
// TODO: also react to the open attribute changing.
}
// Can define constructor arguments if you wish.
constructor() {
// If you define a constructor, always call super() first!
// This is specific to CE and required by the spec.
super(a);// Setup a click listener on <app-drawer> itself.
this.addEventListener("click".e= > {
this.toggleDrawer();
});
}
toggleDrawer() {
this.disabled = Math.random() > 0.5 ? true : false;
}
}
customElements.define("app-drawer", AppDrawer);
</script>
</body>
</html>
Copy the code
Elements to upgrade
We learned how to define an element using customElements. The definition goes with the registration, but you can actually register the element before you define it. CustomElements. Define (‘app-drawer’,… That’s ok.
Because browsers treat unknown tags differently.
If you define an element first and then call define(), this is called an element upgrade.
You can use Windows. CustomElements. WhenDefined () method to monitor element is defined at what time.
customElements.whenDefined('app-drawer').then(() = > {
console.log('app-drawer defined');
});
Copy the code
Here is an example that does something after all the child elements have been upgraded.
<share-buttons>
<social-button type="twitter"><a href="...">Twitter</a></social-button>
<social-button type="fb"><a href="...">Facebook</a></social-button>
<social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
<script>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');
let promises = [...undefinedButtons].map(socialButton= > {
return customElements.whenDefined(socialButton.localName);
});
// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() = > {
// All social-button children are ready.
// do some thing
});
</script>
Copy the code
Elements define
A custom element can manage its own content in its internal code using the DOM API. The element lifecycle also makes this management easier.
Example – Create an element using the default HTML
customElements.define('x-foo-with-markup'.class extends HTMLElement {
connectedCallback() {
this.innerHTML = "I'm an x-foo-with-markup! "; }... });Copy the code
The page will display as
<x-foo-with-markup>
<b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
Copy the code
Use Shadow DOM to create elements
The Shadow DOM provides an area of the page where elements can be rendered and styled separately. You can even hide the entire app in a TAB.
To use the Shadow DOM, call this. AttachShadow method from constructor.
<! DOCTYPEhtml>
<html>
<head>
<title>Web Components</title>
</head>
<body>
<x-foo-shadowdom>
<p><b>User's</b> custom text</p>
</x-foo-shadowdom>
<script>
let tmpl = document.createElement("template");
tmpl.innerHTML = ` <style>:host {}</style> <! -- look ma, scoped styles --> <b>I'm in shadow dom! </b> <slot></slot> `;
customElements.define(
"x-foo-shadowdom".class extends HTMLElement {
constructor() {
super(a);// always call super() first in the constructor.
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(tmpl.content.cloneNode(true)); }});</script>
</body>
</html>
Copy the code
Page rendering results
<x-foo-shadowdom>
#shadow-root
<b>I'm in shadow dom!</b>
<slot></slot> <! -- slotted content appears here -->
</x-foo-shadowdom>
Copy the code
use<template>
Create the element
Template allows you to easily declare the structure of an element.
Example – Register a Shadow DOM element using template
<template id="x-foo-from-template">
<style>
p { color: green; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.</p>
</template>
<script>
let tmpl = document.querySelector('#x-foo-from-template');
// If your code is inside of an HTML Import you'll need to change the above line to:
// let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');
customElements.define('x-foo-from-template'.class extends HTMLElement {
constructor() {
super(a);// always call super() first in the constructor.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true)); }... });</script>
Copy the code
There are a few key points from this example:
-
Define a new tag
-
Create the Shadow DOM using template
-
The element DOM is built-in due to the Shadow DOM
-
Because of the Shadow DOM, the element’s CSS is also built-in, and styles are limited to the elements themselves
Styling custom elements
Even if your element is styled using the Shadow DOM, the style of the element will be affected by the page style.
Page styles are also known as user-defined styles. If it has the same style as Shadow DOM, the custom style takes effect.
Styles undefined elements
Before an element is defined (upgraded), you can style it using the: Not (:defined) pseudo-class.
This default style is also useful, as you can let an element occupy some layout space, even if it is not defined yet.
In the following example, the element takes up some space before it is defined. Of course, when the element is defined, the app-drawer: NOT (:defined) selector is disabled.
app-drawer:not(:defined) {
/* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
display: inline-block;
height: 100vh;
width: 100vh;
}
Copy the code
Extension elements
The custom element API is useful not only for creating new HTML elements, but also for extending other custom elements and even elements built into the browser.
Extend custom elements
Extending the custom element is done using the class definition that inherits it.
Example – create
inherit from
class FancyDrawer extends AppDrawer {
constructor() {
super(a);// always call super() first in the constructor. This also calls the extended class' constructor.
}
toggleDrawer() {
// Possibly different toggle implementation?
// Use ES2015 if you need to call the parent method.
// super.toggleDrawer()
}
anotherMethod() {
}
}
customElements.define('fancy-app-drawer', FancyDrawer);
Copy the code
Inherit native HTML elements
If you want to create a more powerful
Such custom elements that inherit from HTML elements are also called customized built-in elements. It not only captures the features of the native element (attributes, methods, accessibility), but also enhances the functionality of the element. There is no better way to write a progressively enhanced Web application than with custom built-in elements.
Note: Custom built-in elements are not supported by all browsers.
Example – < FancyButton >
// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
constructor() {
super(a);// always call super() first in the constructor.
this.addEventListener('click'.e= > this.drawRipple(e.offsetX, e.offsetY));
}
// Material design ripple animation.
drawRipple(x, y) {
let div = document.createElement('div');
div.classList.add('ripple');
this.appendChild(div);
div.style.top = `${y - div.clientHeight/2}px`;
div.style.left = `${x - div.clientWidth/2}px`;
div.style.backgroundColor = 'currentColor';
div.classList.add('run');
div.addEventListener('transitionend'.e= > div.remove());
}
}
customElements.define('fancy-button', FancyButton, {extends: 'button'});
Copy the code
Notice that define() needs to specify which browser tag is inherited. This is necessary because even different tags may inherit the same DOM interface. For example, and < blockQuote > both inherit HTMLQuoteElement.
Custom built-in elements can add the is=”” attribute to native tags.
<! -- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>
<script>
// Custom elements overload 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);
</script>
Copy the code
Or use the new operator
let button = new FancyButton();
button.textContent = 'Fancy button! ';
button.disabled = true;
Copy the code
Example – extension
<! -- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">
<script>
customElements.define('bigger-img'.class extends Image {
// Give img default size if users don't specify.
constructor(width=50, height=50) {
super(width * 10, height * 10); }}, {extends: 'img'});
</script>
Copy the code
Or create an image instance
const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15.20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);
Copy the code
Some of the details
Unknown elements vs. undefined elements
HTML is open and flexible. Say you declare that a < randomTagthatDoesNTexist > tag is infallible. This is because the HTML specification allows it, in which the tag is interpreted as HTMLUnknownElement.
So for custom elements, illegal custom element names may be resolved as HTMLElement (or HTMLUnknownElement).
API reference
Elements can be defined through defines defines on global customElements.
define(tagName, constructor, options)
Example:
customElements.define('my-app'.class extends HTMLElement {... }); customElements.define('fancy-button'.class extends HTMLButtonElement {... }, {extends: 'button'});
Copy the code
get(tagName)
Passing in a valid custom element name returns the constructor for that element. Returns undefined if the element is not registered.
example
let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();
Copy the code
whenDefined(tagName)
Return a Promise, and resolve will be executed when the element is defined. If the element is already defined, execute resolve immediately. Reject is executed when the tagName passed in is an invalid name.
Example:
customElements.whenDefined('app-drawer').then(() = > {
console.log('ready! ');
});
Copy the code
History and browser support
history
Chrome 36+ implements the v0 version of the custom element API, using Document.registerElement (not customElements. Define) to define elements. V0 is deprecated.
Browser vendors currently use the current version v1, which uses customElements. Define () to define elements.
Browser support
Chrome 54, Safari 10.1, And Firefox 63 are all in v1, and Edg is in development.
You can use the following code to determine if your browser supports version V1.
const supportsCustomElementsV1 = 'customElements' in window;
Copy the code
Polyfill
Polyfill can be used for version v1 until it is widely supported in browsers.
We recommend using webComponents. Js loader to achieve optimal loading of Web Components polyfill. It uses feature detection and asynchronously loads polyfills only when needed.
Polyfill installation
npm install --save @webcomponents/webcomponentsjs
Copy the code
Use polyfill
<! -- Use the custom element on the page. -->
<my-element></my-element>
<! -- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>
<! -- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
WebComponents.waitFor(() = > {
// At this point we are guaranteed that all required polyfills have
// loaded, and can use web components APIs.
// Next, load element definitions that call `customElements.define`.
// Note: returning a promise causes the custom elements
// polyfill to wait until all definitions are loaded and then upgrade
// the document in one batch, for better performance.
return loadScript('my-element.js');
});
</script>
Copy the code
Note: Defined CSS pseudo-classes cannot be polyfilled.
conclusion
Custom elements allow us to define new HTML tags and create reusable components. Combined with other features like Shadow DOM,
, etc., we started to realize the blueprint of Web Components.
It can:
-
Create and extend reusable components across browsers (Web standards)
-
No library or framework development required (powerful JS/HTML)
-
Familiar programming model (DOM/CSS/HTML)
-
Perfect integration with other new Web platform features ((Shadow DOM,
, CSS custom properties, etc.)
-
Tight integration with browser DevTools
-
Use existing accessibility features.
Translated from
- Reusable Web Components By Eric Bidelman
Engineer @ Google working on web tooling: Headless Chrome, Puppeteer, Lighthouse
Welcome to wechat public account: Front Reading Room