Web Components

Overview

Various websites often need some of the same modules, such as calendars, palettes, etc., and such modules are called "components". Web Components are the component specifications native to the browser.

Using component development has many advantages.

(1) Conducive to code reuse. Components are the embodiment of modular programming ideas, which can be used across platforms and frameworks. There are unified practices for building, deploying, and interacting with other UI elements.

(2) Very easy to use. To load or unload components, just add or delete a line of code.

(3) Development and customization are very convenient. Component development does not need to use a framework, just use the native syntax. Developed components often leave an interface for users to set common properties, such as the heading property of the above code, which is used to set the title of the dialog box.

(4) The component provides methods for encapsulating HTML, CSS, and JavaScript to achieve isolation from other codes on the same page.

In future website development, you can combine components like building blocks to form a website. This prospect is very attractive.

Web Components is not a single specification, but a series of technical components, the following are its four components.

  • Custom Elements
  • Template
  • Shadow DOM
  • HTML Import

When using, it is not necessary to use the above four APIs. Among them, Custom Element and Shadow DOM are more important, and Template and HTML Import only play a supporting role.

Custom Element

Introduction

The web page elements defined by the HTML standard sometimes do not meet our needs. At this time, the browser allows the user to customize the web page elements, which is called Custom Element. Simply put, it is a user-defined web page element, which is the core of Web components technology.

For example, you can customize a web element called <my-element>.

<my-element></my-element>

Note that the label name of the custom web page element must contain a hyphen -, and one or more hyphens are acceptable. This is because the HTML element tag names built into the browser do not contain hyphens, which can be effectively distinguished.

The following code first defines a class of custom elements.

class MyElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        /* scoped styles */
      </style>
      <slot></slot>
    `;
  }

  static get observedAttributes() {
    // Return list of attributes to watch.
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // Run functionality when one of these attributes is changed.
  }

  connectedCallback() {
    // Run functionality when an instance of this element is inserted into the DOM.
  }

  disconnectedCallback() {
    // Run functionality when an instance of this element is removed from the DOM.
  }
}

There are several points to note in the above code.

-The base class of the custom element class is HTMLElement. Of course, it can also be based on subclasses of HTMLElement, such as HTMLButtonElement. -Shadow DOM is defined inside the constructor. The so-called Shadow DOM means that this part of the HTML code and styles are not directly exposed to users. -Classes can define life cycle methods, such as connectedCallback().

Then, the window.customElements.define() method is used to register the mapping between custom elements and this class.

window.customElements.define("my-element", MyElement);

After registration, every <my-element> element on the page is an instance of the MyElement class. As long as the browser parses the <my-element> element, it will run the MyElement constructor.

Note that if you use Custom Element without registration, the browser will think this is an unknown element and will treat it as an empty div element.

After the window.customElements.define() method defines the Custom Element, you can use the window.customeElements.get() method to get the element's construction method. This allows in addition to directly inserting HTML pages, Custom Element can also use scripts to insert pages.

window.customElements.define(
  'my-element',
  class extends HTMLElement {...}
);
const el = window.customElements.get('my-element');
const myElement = new el();
document.body.appendChild(myElement);

If you want to extend existing HTML elements (such as <button>), it is also possible.

class GreetingElement extends HTMLButtonElement

When registering, you need to provide extended elements.

customElements.define("hey-there", GreetingElement, { extends: "button" });

When using it, just add the is attribute to the element.

<button is="hey-there" name="World">Howdy</button>

Life Cycle Method

Custom Element provides some life cycle methods.

class MyElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // here the element has been inserted into the DOM
  }
}

In the above code, the connectedCallback() method is the life cycle method of the MyElement element. Every time the element is inserted into the DOM, this method will be executed automatically.

  • connectedCallback(): Called when the DOM is inserted. This may happen more than once, such as when an element is removed and then added again. Class settings should be executed in this method as much as possible, because various attributes and sub-elements are available at this time.
  • disconnectedCallback(): executed when the DOM is removed.
  • attributeChangedCallback(attrName, oldVal, newVal): Called when adding, deleting, updating or replacing attributes. It is also called when the element is created or upgraded. Note: This method will only be executed if the attributes are added to observedAttributes.
  • adoptedCallback(): Called when a custom element is moved to a new document, such as when executing document.adoptNode(element).

Below is an example.

class GreetingElement extends HTMLElement {
  constructor() {
    super();
    this._name = "Stranger";
  }
  connectedCallback() {
    this.addEventListener("click", (e) => alert(`Hello, ${this._name}!`));
  }
  attributeChangedCallback(attrName, oldValue, newValue) {
    if (attrName === "name") {
      if (newValue) {
        this._name = newValue;
      } else {
        this._name = "Stranger";
      }
    }
  }
}
GreetingElement.observedAttributes = ["name"];
customElements.define("hey-there", GreetingElement);

In the above code, the GreetingElement.observedAttributes attribute is used to specify the attributes in the whitelist, the above example is the name attribute. As long as the value of this attribute changes, the attributeChangedCallback method is automatically called.

The method of using the above class is as follows.

<hey-there>Greeting</hey-there>
<hey-there name="Potch">Personalized Greeting</hey-there>

The attributeChangedCallback method is mainly used for attributes passed in from outside, like the name="Potch" in the above example.

The order of life cycle method calls is as follows: constructor -> attributeChangedCallback -> connectedCallback, that is, attributeChangedCallback is executed earlier than connectedCallback. This is because attributeChangedCallback is equivalent to adjusting the configuration and should be done before inserting the DOM.

The following example can see this more clearly, modify the color of the Custom Element before inserting the DOM.

class MyElement extends HTMLElement {
  constructor() {
    this.container = this.shadowRoot.querySelector('#container');
  }
  attributeChangedCallback(attr, oldVal, newVal) {
    if(attr === 'disabled') {
      if(this.hasAttribute('disabled') {
        this.container.style.background = '#808080';
      } else {
        this.container.style.background = '#ffffff';
      }
    }
  }
}

Custom attributes and methods

Custom Element allows custom attributes or methods.

class MyElement extends HTMLElement {
  ...

  doSomething() {
    // do something in this method
  }
}

In the above code, doSomething() is a custom method of MyElement, and its usage is as follows.

const element = document.querySelector("my-element");
element.doSomething();

Custom attributes can use all the syntax of JavaScript class, so you can also set evaluators and evaluators.

class MyElement extends HTMLElement {
  ...

  set disabled(isDisabled) {
    if(isDisabled) {
      this.setAttribute('disabled', '');
    }
    else {
      this.removeAttribute('disabled');
    }
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }
}

The evaluator and evaluator in the above code can be used for usage like <my-input name="name" disabled>.

window.customElements.whenDefined()

The window.customElements.whenDefined() method is executed after a Custom Element is defined by the customElements.define() method to "upgrade" an element.

window.customElements.whenDefined("my-element").then(() => {
  // my-element is now defined
});

If you need to react when an attribute value changes, you can put it into the observedAttributes array.

class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ["disabled"];
  }

  constructor() {
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.innerHTML = `
      <style>
        .disabled {
          opacity: 0.4;
        }
      </style>

      <div id="container"></div>
    `;

    this.container = this.shadowRoot("#container");
  }

  attributeChangedCallback(attr, oldVal, newVal) {
    if (attr === "disabled") {
      if (this.disabled) {
        this.container.classList.add("disabled");
      } else {
        this.container.classList.remove("disabled");
      }
    }
  }
}

Callback

The prototype of a custom element has some attributes, which are used to specify a callback function to be triggered when a specific event occurs.

-createdCallback: Triggered when the instance is generated -attachedCallback: Triggered when the instance is inserted into the HTML document -detachedCallback: triggered when the instance is removed from the HTML document -attributeChangedCallback(attrName, oldVal, newVal): triggered when the instance's attributes are changed (add, remove, update)

Below is an example.

var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function () {
  console.log("created");
  this.innerHTML = "This is a my-demo element!";
};

proto.attachedCallback = function () {
  console.log("attached");
};

var XFoo = document.registerElement("x-foo", { prototype: proto });

Using the callback function, you can easily insert HTML statements in custom elements.

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function () {
  this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
};

var XFoo = document.registerElement("x-foo-with-markup", {
  prototype: XFooProto,
});

The above code defines the createdCallback callback function. When the instance is generated, the function runs and inserts the following HTML statement.

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

Child elements of Custom Element

When users use Custom Element, they can place child elements inside. Custom Element provides <slot> to reference internal content.

The following <image-gallery> is a Custom Element. The user has placed child elements inside.

<image-gallery>
  <img src="foo.jpg" slot="image" />
  <img src="bar.jpg" slot="image" />
</image-gallery>

The template inside <image-gallery> is as follows.

<div id="container">
  <div class="images">
    <slot name="image"></slot>
  </div>
</div>

The final synthesized code is as follows.

<div id="container">
  <div class="images">
    <slot name="image">
      <img src="foo.jpg" slot="image" />
      <img src="bar.jpg" slot="image" />
    </slot>
  </div>
</div>

<template> tag

Basic usage

The <template> tag represents the HTML code template of the component.

<template>
  <h1>This won't display!</h1>
  <script>
    alert("this won't alert!");
  </script>
</template>

The inside of <template> is normal HTML code, and the browser will not add these codes to the DOM.

The following code inserts the code inside the template into the DOM.

let template = document.querySelector("template");
document.body.appendChild(template.content);

Note that the code inside the template can only be inserted once, and an error will be reported if the above code is executed the second time.

If you need to insert the template multiple times, you can copy the internal code of <template> and insert it again.

document.body.appendChild(template.content.cloneNode(true));

In the above code, the parameter true of the cloneNode() method means that the copy includes all child nodes.

The element that accepts the insertion of <template> is called the host element. In <template>, you can style the host element.

<template>
  <style>
    :host {
      background: #f8f8f8;
    }
    :host(:hover) {
      background: #ccc;
    }
  </style>
</template>

document.importNode()

The document.importNode method is used to clone the DOM node of an external document.

var iframe = document.getElementsByTagName("iframe")[0];
var oldNode = iframe.contentWindow.document.getElementById("myNode");
var newNode = document.importNode(oldNode, true);
document.getElementById("container").appendChild(newNode);

The above example is to clone the node oldNode in the iframe window into the current document.

Note that after the node is cloned, it must be added to the current document with the appendChild method, otherwise it will not be displayed. From another perspective, this means that before inserting an external document node, the node must be prepared using the document.importNode method.

The document.importNode method accepts two parameters. The first parameter is the DOM node of the external document, and the second parameter is a Boolean value indicating whether to clone together with the child nodes. The default is false. In most cases, the second parameter must be explicitly set to true.

Shadow DOM

The so-called Shadow DOM means that the browser encapsulates templates, style sheets, attributes, JavaScript codes, etc. into an independent DOM element. The external settings cannot affect its internals, and the internal settings will not affect the externals, which is very similar to the way the browser handles native web page elements (such as the <video> element).

There are two biggest benefits of Shadow DOM. One is that it can hide details from users and provide components directly. The other is that it can encapsulate internal style sheets without affecting the outside world.

There is a Shadow Root inside the Custom Element. It is the root element that connects to the external DOM.

// attachShadow() creates a shadow root.
let shadow = div.attachShadow({ mode: "open" });
let inner = document.createElement("b");
inner.appendChild(document.createTextNode("Hiding in the shadows"));

// shadow root supports the normal appendChild method.
shadow.appendChild(inner);
div.querySelector("b"); // empty

In the above code, <div> contains <b>, but the DOM method cannot see it, and the style of the page does not affect it.

mode:'open' means that in the developer tools, you can see and interact with the DOM inside Custom HTML. mode: closed will not allow users of Custom Element to interact with internal code.

Inside the shadow root, specify the HTML code by specifying the innerHTML attribute or using the <template> element.

Inside Shadow DOM, you can set the style by adding <style> (or <link>) to the root.

let style = document.createElement("style");
style.innerText = "b { font-weight: bolder; color: red; }";
shadowRoot.appendChild(style);

let inner = document.createElement("b");
inner.innerHTML = "I'm bolder in the shadows";
shadowRoot.appendChild(inner);

The style added by the code above will only affect the elements in the Shadow DOM.

Inside the CSS style of Custom Element, :root represents the root element. For example, Custom Element is an inline element by default, and the following code can be changed to a block-level element.

:host {
  display: block;
}

:host([disabled]) {
  opacity: 0.5;
}

Note that the external style will override the setting of :host, for example, the following style will override :host.

my-element {
  display: inline-block;
}

Using CSS custom properties can be the default style that Custom Element can be overridden. Below is the external style, my-element is Custom Element.

my-element {
  --background-color: #ff0000;
}

Then, the default style can be specified internally, which is used when the user does not specify a color.

:host {
  --background-color: #ffffff;
}

#container {
  background-color: var(--background-color);
}

The following example is to add a separate template for Shadow DOM.

<div id="nameTag">Zhang San (Chinese character)</div>

<template id="nameTagTemplate">
  <style>
    .outer {
      border: 2px solid brown;
    }
  </style>

  <div class="outer">
    <div class="boilerplate">Hi! My name is</div>
    <div class="name">Bob</div>
  </div>
</template>

The above code is a div element and template. The next step is to apply the template to the div element.

HTML Import

Basic operation

For a long time, web pages can load external style sheets, scripts, pictures, and multimedia, but cannot easily load other web pages. Both iframe and ajax can only provide partial solutions and have great limitations. HTML Import is proposed to solve the problem of loading external web pages.

The following code is used to test whether the current browser supports HTML Import.

function supportsImports() {
  return "import" in document.createElement("link");
}

if (supportsImports()) {
  // stand by
} else {
  // not support
}

HTML Import is used to load external HTML documents into the current document. We can encapsulate the HTML, CSS, and JavaScript of the component in a file, and then use the following code to insert the web page that needs to use the component.

<link rel="import" href="dialog.html" />

The above code inserts a dialog box component into the web page, which is encapsulated in the dialog.html file. Note that the styles and JavaScript scripts in the dialog.html file are valid for the entire web page inserted.

Assuming that webpage A loads webpage B through HTML Import, that is, B is a component, then the style sheet and script of webpage B are also valid for webpage A (to be precise, only the style in the style tag is valid for webpage A, and the link tag is loaded The style sheet is invalid for page A). So you can put multiple style sheets and scripts on the B webpage and load them from there. This is a very convenient loading method for large frames.

If B and A are not in the same domain, then CORS must be turned on in the domain where A is located.

<!-- example.com must open CORS -->
<link rel="import" href="http://example.com/elements.html" />

In addition to using the link tag, you can also use JavaScript to call the link element to complete HTML Import.


var link = document.createElement('link');
link.rel = 'import';
link.href = 'file.html'
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);

When HTML Import is successfully loaded, the load event will be triggered on the link element. When the loading fails (such as a 404 error), the error event will be triggered. You can specify callback functions for these two events.

<script async>
  function handleLoad(e) {
    console.log("Loaded import: " + e.target.href);
  }
  function handleError(e) {
    console.log("Error loading import: " + e.target.href);
  }
</script>

<link
  rel="import"
  href="file.html"
  onload="handleLoad(event)"
  onerror="handleError(event)"
/>

In the above code, the definition of the handleLoad and handleError functions must be in front of the link element. Because when the browser element encounters the link element, it immediately parses and loads the external web page (synchronous operation). If these two functions are not defined at this time, an error will be reported.

HTML Import is loaded synchronously, which will block the rendering of the current web page. This is mainly for the consideration of style sheets, because the style sheets of external web pages are also valid for the current web page. If you want to avoid this, you can add an async attribute to the link element. Of course, this also means that if the component is defined on the external web page, it cannot be used immediately. You must wait for the HTML Import to complete before it can be used.

<link rel="import" href="/path/to/import_that_takes_5secs.html" async />

However, HTML Import will not block the current web page parsing and script execution (that is, blocking rendering). This means that while loading, the script of the main page will continue to execute.

Finally, HTML Import supports multiple loading, that is, the loaded webpage loads other webpages at the same time. If these web pages load the same external script repeatedly, the browser will only crawl and execute the script once. For example, if webpage A loads webpage B, they all need to load jQuery, and the browser will only load jQuery once.

Script execution

The content of the external webpage is not automatically displayed in the current webpage, it is only stored in the browser and loaded into the current webpage when it is called. In order to load web pages, DOM operations must be used to obtain the loaded content. Specifically, it uses the import attribute of the link element to get the loaded content. This is completely different from iframes.

var content = document.querySelector('link[rel="import"]').import;

When the following situations occur, the link.import property is null.

  • The browser does not support HTML Import
  • The link element does not declare rel="import"
  • The link element is not added to the DOM
  • The link element has been removed from the DOM
  • CORS is not turned on for the domain name

The following code is used to select the element whose id is template from the loaded external webpage, and then clone it and add it to the DOM of the current webpage.

var el = linkElement.import.querySelector("# template");

document.body.appendChild(el.cloneNode(true));

The current webpage can get the external webpage, and vice versa, the script in the external webpage can not only get its own DOM, but also get the DOM of the current webpage where the link element is located.

// The following code is located on the external web page that is loaded (import)

// importDoc points to the loaded DOM
var importDoc = document.currentScript.ownerDocument;

// mainDoc points to the DOM of the main document
var mainDoc = document;

// Add the style sheet of the subpage to the main document
var styles = importDoc.querySelector('link[rel="stylesheet"]');
mainDoc.head.appendChild(styles.cloneNode(true));

The above code adds the style sheet of the loaded external web page to the current web page.

The script of the loaded external webpage is executed directly in the context of the current webpage, because its window.document refers to the document of the current webpage, and the functions it defines can be directly referenced by the script of the current webpage.

Web Component packaging

For Web Component, an important application of HTML Import is to automatically register Custom Element in the loaded web page.

<script>
  // Define and register <say-hi>
  var proto = Object.create(HTMLElement.prototype);

  proto.createdCallback = function () {
    this.innerHTML = "Hello, <b>" + (this.getAttribute("name") || "?") + "</b>";
  };

  document.registerElement("say-hi", { prototype: proto });
</script>

<template id="t">
  <style>
    ::content > * {
      color: red;
    }
  </style>
  <span>I'm a shadow-element using Shadow DOM!</span>
  <content></content>
</template>

<script>
  (function () {
    var importDoc = document.currentScript.ownerDocument; //Point to the loaded webpage

    // Define and register <shadow-element>
    var proto2 = Object.create(HTMLElement.prototype);

    proto2.createdCallback = function () {
      var template = importDoc.querySelector("#t");
      var clone = document.importNode(template.content, true);
      var root = this.createShadowRoot();
      root.appendChild(clone);
    };

    document.registerElement("shadow-element", { prototype: proto2 });
  })();
</script>

The above code defines and registers two elements: <say-hi> and <shadow-element>. Using these two elements on the main page is very simple.

<head>
  <link rel="import" href="elements.html" />
</head>
<body>
  <say-hi name="Eric"></say-hi>
  <shadow-element>
    <div>( I'm in the light dom )</div>
  </shadow-element>
</body>

It is not difficult to think that this means that HTML Import makes Web Component shareable, and other people can use it in their own pages as long as they copy elements.html.

Polymer.js

Web Components is a very new technology, in order to make old browsers can also use, Google launched a function library Polymer.js. This library not only helps developers define their own web page elements, but also provides many pre-made components that can be used directly.

Directly used components

The components provided by Polymer.js can be directly inserted into web pages, such as the google-map below. .

<script src="components/platform/platform.js"></script>
<link rel="import" href="google-map.html" />
<google-map lat="37.790" long="-122.390"></google-map>

For another example, to insert a clock in a web page, you can directly use the label below.

<polymer-ui-clock></polymer-ui-clock>

The usage of custom tags is exactly the same as other tags, and you can also use CSS to specify its style.

polymer-ui-clock {
  width: 320px;
  height: 320px;
  display: inline-block;
  background: url("../assets/glass.png") no-repeat;
  background-size: cover;
  border: 4px solid rgba(32, 32, 32, 0.3);
}

Installation

If you use bower installation, at least two core parts, platform and core components, need to be installed.


bower install --save Polymer/platform
bower install --save Polymer/polymer

You can also install all pre-defined interface components.


bower install Polymer/core-elements
bower install Polymer/polymer-ui-elements

It is also possible to install only a single component.


bower install Polymer/polymer-ui-accordion

At this time, bower.json in the root directory of the component will indicate the dependent modules of the component, and these modules will be installed automatically.


{
  "name": "polymer-ui-accordion",
  "private": true,
  "dependencies": {
    "polymer": "Polymer/polymer#0.2.0",
    "polymer-selector": "Polymer/polymer-selector#0.2.0",
    "polymer-ui-collapsible": "Polymer/polymer-ui-collapsible#0.2.0"
  },
  "version": "0.2.0"
}

Custom components

The following is an example of the simplest custom component.

<link rel="import" href="../bower_components/polymer/polymer.html" />

<polymer-element name="lorem-element">
  <template>
    <p>lorem ipsum</p>
  </template>
</polymer-element>

The above code defines the lorem-element component. It is divided into three parts.

(1) import command

The import command means to load the core module

(2) polymer-element label

The polymer-element tag defines the name of the component (note that the component name must contain a hyphen). It can also use the extends attribute to indicate that the component is based on a certain web page element.

<polymer-element name="w3c-disclosure" extends="button"> </polymer-element>

(3) template tag

The template tag defines the template of the web page element.

How to use components

In the web page that calls the component, first load the polymer.js library and component files.

<script src="components/platform/platform.js"></script>
<link rel="import" href="w3c-disclosure.html" />

Then, it is divided into two cases. If the component is not based on any existing HTML page elements (that is, the extends attribute is not used when defining it), the component can be used directly.

<Lorem-element> </Lorem-element>

At this time, a line of "Lorem ipsum" will be displayed on the web page.

If the component is based on (extends) an existing web page element, the is attribute must be used to specify the component on that kind of element.


<button is="w3c-disclosure">Expand section 1</button>