Custom Elements
Web Components: Custom Elements
What are Custom Elements in JavaScript?
View Answer:
How are Custom Elements defined in JavaScript?
View Answer:
// Define your custom element class
class MyCustomElement extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// Write element functionality in here
const shadow = this.attachShadow({mode: 'open'});
const wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
const info = document.createElement('span');
info.setAttribute('class', 'info');
info.textContent = "Hello, I'm a custom element!";
shadow.appendChild(wrapper);
wrapper.appendChild(info);
}
}
// Define the new element
customElements.define('my-custom-element', MyCustomElement);
To use this element in HTML, you would simply include <my-custom-element></my-custom-element>
somewhere in your HTML source.
Please keep in mind that not all browsers support Custom Elements and you might need a polyfill for unsupported browsers. As of March 2021, the latest versions of Firefox, Chrome, Safari, and Edge all support Custom Elements.
What are the two types of custom elements in web development?
View Answer:
Here is an example of an autonomous custom element:
class MyElement extends HTMLElement {
constructor() {
super();
this.innerHTML = "<p>I'm an autonomous custom element!</p>";
}
}
customElements.define('my-element', MyElement);
You would use this in your HTML like so:
<my-element></my-element>
Here is an example of a customized built-in element:
class MyParagraph extends HTMLParagraphElement {
constructor() {
super();
this.style.color = 'blue';
}
}
customElements.define('my-paragraph', MyParagraph, { extends: 'p' });
You would use this in your HTML like so:
<p is="my-paragraph">Hello, world!</p>
Please note that as of March 2021, customized built-in elements are not as widely supported as autonomous custom elements. For example, they are not supported in the standard configuration of the Apple Safari browser. Always check the current compatibility status before using this feature.
What are the requirements needed to create a custom element?
View Answer:
class MyElement extends HTMLElement {
constructor() {
super();
// element created
}
}
// let the browser know that <my-element> is served by our new class
customElements.define('my-element', MyElement);
What naming standard should be used for custom elements in web development?
View Answer:
What are the five methods in the lifecycle callbacks?
View Answer:
The constructor is used to set up element properties. However, connectedCallback is preferred for setup work because certain attributes may not be defined at construction.
Can you describe the connectedCallBack() method's purpose?
View Answer:
class MyElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
console.log('Custom element added to page.');
this.innerHTML = "<p>Hello, world!</p>";
}
}
customElements.define('my-element', MyElement);
In the browser console, you'll see the message "Custom element added to page." printed each time a my-element is attached to the DOM. The my-element will also display "Hello, world!" on the webpage.
What is the purpose of the "attributeChangedCallback()" in Custom Elements?
View Answer:
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['my-attribute'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`The attribute ${name} has changed from ${oldValue} to ${newValue}!`);
}
}
customElements.define('my-element', MyElement);
In this example, every time the my-attribute
attribute of a my-element
custom element changes, the attributeChangedCallback
method will be invoked, logging a message to the console about this change.
You can use it in your HTML like so:
<my-element my-attribute="foo"></my-element>
Then, if you were to later change the attribute in JavaScript like so:
document.querySelector('my-element').setAttribute('my-attribute', 'bar');
You would see a message in your console that says: "The attribute my-attribute has changed from foo to bar!"
Remember, attributeChangedCallback
will only monitor changes for attributes that are included in the array returned by the observedAttributes
method. If you want to monitor multiple attributes, you can include all of them in this array.
What is the "observedAttributes" method in Custom Elements?
View Answer:
class MyElement extends HTMLElement {
// Specify observed attributes so that attributeChangedCallback will work
static get observedAttributes() {
return ['my-attribute', 'my-other-attribute'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`The attribute ${name} has changed from ${oldValue} to ${newValue}!`);
}
}
customElements.define('my-element', MyElement);
You can use this in HTML like so:
<my-element my-attribute="foo" my-other-attribute="bar"></my-element>
Then, if you were to later change these attributes in JavaScript:
let element = document.querySelector('my-element');
element.setAttribute('my-attribute', 'newFoo');
element.setAttribute('my-other-attribute', 'newBar');
You would see messages in your console saying: "The attribute my-attribute has changed from foo to newFoo!" and "The attribute my-other-attribute has changed from bar to newBar!"
Note that if you add, remove, or change an attribute that is not included in the observedAttributes
array, the attributeChangedCallback
will not be invoked.
What's the purpose of "adoptedCallback" in Custom Elements?
View Answer:
class MyElement extends HTMLElement {
constructor() {
super();
}
adoptedCallback(oldDocument, newDocument) {
console.log('Custom element has been moved to a new document.');
}
}
customElements.define('my-element', MyElement);
In this example, every time a my-element
custom element is moved from one document to another, the adoptedCallback
method will be invoked, and it will log a message to the console.
To demonstrate adoptedCallback
, you need two documents: the main document and an iframe. You can create the custom element in the main document and then move it to the iframe document:
<iframe id="my-iframe"></iframe>
<my-element id="my-element"></my-element>
<script>
let iframe = document.querySelector('#my-iframe');
let myElement = document.querySelector('#my-element');
// When the iframe is loaded, move myElement to the iframe's document
iframe.addEventListener('load', () => {
iframe.contentWindow.document.body.appendChild(myElement);
});
</script>
In this scenario, when the custom element is moved to the iframe's document, the adoptedCallback
will be called and you will see the message "Custom element has been moved to a new document." in the console.
Can Custom Elements extend built-in elements?
View Answer:
Can Custom Elements be used with older browsers?
View Answer:
Can we nest Custom Elements?
View Answer:
Here's an example of nested custom elements in JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>Nested Custom Elements Example</title>
</head>
<body>
<my-container>
<my-heading>Hello, world!</my-heading>
<my-list>
<my-list-item>Item 1</my-list-item>
<my-list-item>Item 2</my-list-item>
<my-list-item>Item 3</my-list-item>
</my-list>
</my-container>
<script>
// Define the custom elements
class MyContainer extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = '<div style="border: 1px solid black; padding: 10px;">' + this.innerHTML + '</div>';
}
}
class MyHeading extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = '<h1>' + this.innerHTML + '</h1>';
}
}
class MyList extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = '<ul>' + this.innerHTML + '</ul>';
}
}
class MyListItem extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = '<li>' + this.innerHTML + '</li>';
}
}
// Register the custom elements
customElements.define('my-container', MyContainer);
customElements.define('my-heading', MyHeading);
customElements.define('my-list', MyList);
customElements.define('my-list-item', MyListItem);
</script>
</body>
</html>
In this example, we have four custom elements: my-container
, my-heading
, my-list
, and my-list-item
. The my-container
element serves as a container and adds a border and padding to its content. The my-heading
element wraps its content in an <h1>
tag, making it a heading. The my-list
element wraps its content in <ul>
tags, creating an unordered list, and the my-list-item
element wraps its content in <li>
tags, representing each list item.
By nesting these custom elements within each other, you can create a structured hierarchy of elements with customized behavior and appearance.
What happens when a base element, the one we are customizing, loads before the customized element?
View Answer:
What is the reasoning for not utilizing the constructor and instead relying on connectedCallBack?
View Answer:
When the element gets added to the document, the connectedCallback is triggered. It is not just attached to another element as a child but instead becomes a part of the page. As a result, we may construct detached DOM, create elements, and prepare them for subsequent usage. They do not render until they get included on the page.
class MyElement extends HTMLElement {
constructor() {
super();
// Trying to manipulate attributes or children here may not work as expected
}
connectedCallback() {
// It's more reliable to perform setup work here
this.innerHTML = "<p>I'm a custom element!</p>";
}
}
customElements.define('my-element', MyElement);
You use the custom element in your HTML like so:
<my-element></my-element>
In this example, if you tried to set innerHTML
in the constructor
, it might not work as expected because the element might not be fully ready and its context might not be completely defined. However, if you set innerHTML
in connectedCallback
, it's more likely to work reliably because the element is already in the DOM.
Can you explain how observedAttribute works in conjunction with attributeChangedCallback?
View Answer:
<script>
class TimeFormatted extends HTMLElement {
render() {
// (1)
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat('default', {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
connectedCallback() {
// (2)
if (!this.rendered) {
this.render();
this.rendered = true;
}
}
static get observedAttributes() {
// (3)
return [
'datetime',
'year',
'month',
'day',
'hour',
'minute',
'second',
'time-zone-name',
];
}
attributeChangedCallback(name, oldValue, newValue) {
// (4)
this.render();
}
}
customElements.define('time-formatted', TimeFormatted);
</script>
<time-formatted id="elem" hour="numeric" minute="numeric" second="numeric">
</time-formatted>
<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
It does not trigger unlisted properties (for performance reasons).
Can you explain the rendering order when the HTML parser builds the DOM?
View Answer:
<script>
customElements.define(
'user-info',
class extends HTMLElement {
connectedCallback() {
console.log(this.innerHTML); // console.log is empty (*)
}
}
);
</script>
<user-info>John</user-info>
Is there a way to ensure that custom-element returns a value on a nested element?
View Answer:
<script>
customElements.define(
'user-info',
class extends HTMLElement {
connectedCallback() {
setTimeout(() => console.log(this.innerHTML)); // John (*)
}
}
);
</script>
<user-info>John</user-info>
Are there any issues with new or autonomous elements relative to search engines?
View Answer:
<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => console.log('Hello!'));
}
}
customElements.define('hello-button', HelloButton, { extends: 'button' });
</script>
<button is="hello-button">Click me</button>
<button is="hello-button" disabled>Disabled</button>