Those familiar with React or any other javascript framework, are already aware of the component based architecture. You break the UI into re-usable pieces of code and stitch them together when required.
Custom elements in HTML are a way to extend native HTML elements. Javascript frameworks simulate the behavior of components in a web page whereas custom elements provide a native HTML-ly way to do so. A web component uses custom elements along with other techniques such as the shadow DOM.
Types Of Custom Elements
- Autonomous custom elements
- Customized built-in elements
Autonomous custom elements extend the generic HTMLElement class. On the other hand, a customized custom element extends a specific HTML elements' class and builds on top of existing functionality. For example, if you want a custom anchor element, you can extend the HTMLAnchorElement.
Defining Custom Elements
To define a custom element, we need to create a javascript class extending the native HTMLElement class. Try creating the below custom element in a codepen:
class Demo extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
this.textContent = "hello"
}
}
customElements.define("demo", Demo)
And then invoking it in HTML:
<demo></demo>
It won't let you create it. Why? See the error.
Uncaught SyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "demo" is not a valid custom element name
This is not a bug, this is intentionally done to separate custom elements from native HTML elements. Custom elements must contain a hyphen in their name to make custom elements recognizable and distinct from HTML elements.
So now, if you change it to something like demo-webc
and change the class name to DemoWebC
, it works.
class DemoWebC extends HTMLElement {
constructor() {
super()
this.customProperty = "custom"
}
connectedCallback() {
this.textContent = "hello"
}
}
// two arguments: tag name, class name
customElements.define("demo-webc", DemoWebC)
It always recommended to call the super()
method first in the constructor as it initializes default properties of the HTMLElement
class by invoking its constructor.
The connectedCallback()
method is for detecting when the element is loaded into the page. There's also a method named disconnectedCallback()
which detects if the element is removed from the page. A third method name adoptedCallback()
says that the element has moved to a new page.
You can define custom properties inside the constructor and use them as custom attributes in your element.
constructor() {
super()
this.customProperty = {
name: "data-custom",
value: "custom value"
}
connectedCallback() {
this.textContent = "hello"
this.setAttribute(this.customAttribute.name, this.customAttribute.value)
}
}
But what if you need to modify the functionality on attribute's value change? That's where attributeChangedCallback()
method comes into action. In order to see it in action, you need to first define a static observedAttributes
class property and set it to an array of all the attributes you want to keep track of.
The attributeChangedCallback()
fires if those attributes mentioned in the static observedAttributes
property change. Note that if the attribute is already present when the custom element loads, this method is fired at that time too.
static observedAttributes = ["data-custom"]
constructor() {
super()
}
attributeChangedCallback(name, old, newValue) {
console.log(name, old, newValue)
}
Once you are done with building a custom element, you must register it by using the define()
method. It's callable on the customElements
global object (window.customElements
) which is a registry of custom elements.
customElements.define("custom-element-name", ClassName, options)
This was all for defining a customized autonomous custom element. What about extending only an anchor HTML element?
For that, instead of extending the HTMLElement
class, extend the HTMLAnchorElement
class. And specify which type of HTML element it extends with the extends
option.
class DemoAnchor extends HTMLAnchorElement {
constructor() {
super()
}
connectedCallback() {
this.textContent = "syntackle.live"
this.href = "https://syntackle.live"
}
}
customElements.define("demo-anchor", DemoAnchor, { extends: "a" })
You can't use this element like <demo-anchor>
because it's not an autonomous element, instead you can use it like this:
<a is="demo-anchor"></a>
Web Components
Web components are more than just custom elements. They sometimes also involve a shadow DOM. A "shadow" DOM, as the name suggests, is a sub-DOM tree for HTML elements. It is mainly used for encapsulation and restricting styles up to the web component only.
Shadow DOM
To create a shadow DOM, attach it to a host, in our case the custom element itself is a host to the shadow DOM. However, the shadow DOM can only be attached to a custom element or these built-in elements mentioned in the HTML spec.
You can access elements outside the shadow DOM from inside the shadow DOM.
class DemoWebC extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
const shadow = this.attachShadow({mode: "open"})
const style = document.createElement("style")
style.textContent = `p { color: blue; }`
shadow.appendChild(style)
const text = document.createElement("p")
text.textContent = "hello"
shadow.appendChild(text)
}
}
customElements.define("demo-webc", DemoWebC)
Shadow DOM has two modes: open
and closed
. Open means external elements in the page can modify the contents of the shadow DOM by using shadowRoot
property. In the closed
mode, the shadow DOM is not accessible from outside using the shadowRoot
property as it is null
in this case.
Try doing this on a closed shadow DOM custom element:
console.log(document.querySelector("demo-webc").shadowRoot)
It returns null
.
Templates and slots are extremely useful when building complex custom elements or web components. Diving deep into them is out of the scope of this article, but here are some good resources for them:
Styling Shadow DOM
The shadow DOM can be styled either by:
- Constructing a
CSSStyleSheet
object, inserting CSS in it usingreplaceSync()
and attaching it to the shadow DOM using theadoptedStyleSheets
property.
const shadowDOM = this.attachShadow({mode: "open"})
const styleSheet = new CSSStyleSheet()
styleSheet.replaceSync(`p { color: blue; }`)
shadowDOM.adoptedStyleSheets = [styleSheet]
- Declaring styles using a
<template>
.
<template id="custom">
<head>
<style>p { color: blue; }</style>
</head>
<p>Web Component</p>
</template>
const shadowDOM = this.attachShadow({ mode: "open" })
const template = document.querySelector("#custom")
shadowDOM.appendChild(template.content.cloneNode(true))
- Simply creating a
style
tag and inserting CSS as text in it.
const style = document.createElement("style")
style.textContent = `p { color: blue; }`
shadow.appendChild(style)
Creating Your Own Web Component
The first web component shown below is a custom button element which opens a dialog
element. And the second web component involves a shadow DOM to pretty print JSON string in HTML.
Similarly, you can create your own custom elements and use them anywhere you want.