An over-simplified, basic definition of what Web Components are:
- Custom Elements (node names containing a dash)
- Shadow DOM
- HTML Templates (using the
<template>
tag) - HTML Imports
- Best Practices
Browser's native support for each of these things varies, but polyfills help.
Is it a toy? No, its a versatile interlocking brick system for the web.
If you don't want to read the spec, they are essentially nodes with names that contain a dash:
<super-duper-app>
(valid)<superduperapp>
(invalid)<toggle-button>
(valid)<toggleButton>
(invalid)
Before your custom elements are registered using document.registerElement they will be :unresolved and receive that style. Unknown elements will not be styled with the :unresolved pseudoclass. Invalid elements will be treated as unknown. Try the following style and HTML:
/* Style Unresolved (as opposed to Unknown) elements */
:unresolved { border: 5px dashed red; display: block; margin: 2em; }
<toggle-button>Toggle-Button</toggle-button>
<toggleButton>ToggleButton</toggleButton>
When you register your element you are able to define the prototype, which includes some event callbacks, and also define whether it extends another element. The prototype that the custom element inherits from defaults to HTMLElement, but can be virtualy any element.
- createdCallback = { property: value OR f(x) } an instance of the element is created
- attachedCallback = { property: value OR f(x) } an instance was inserted into the document
- detachedCallback = { property: value OR f(x) } an instance was removed from the document
- attributeChangedCallback(attrName, oldVal, newVal) an attribute was added, removed, or updated
// Custom Element Registration with document.registerElement
document.registerElement('toggle-button', {
prototype: Object.create(HTMLElement.prototype, {
createdCallback: {
// This would normally set the value attribute of the custom element
value: function() {
this.innerHTML = '<a class="btn">I am Groot!</a>';
}
}
})
});
Who knows what evil lurks in the hearts of custom elements? The Shadow DOM knows! In this case, that's a good thing though.
The Shadow DOM can utilize the <content>
tag to drop the innards of the web component into your element.
<welcome-msg>Star Lord</welcome-msg>
// Custom Element Registration with Shadow DOM
document.registerElement('welcome-msg', {
prototype: Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
// Adds the #shadow-root to the element
var root = this.createShadowRoot();
// <content> represents the non-Shadow innards of the element
root.innerHTML = 'Welcome to MMMBOP, <content></content>!';
}
}
})
});
Utilizing the <template>
tag for a custom element enables you to do a little separation of concerns.
<cool-menu>
<ul>
<li>First Item</li>
<li>Second Item</li>
</ul>
</cool-menu>
<!-- A template to be used by a custom element -->
<template id="menu-template">
<h1>Menu</h1>
<content></content>
</template>
// Custom Element Registration using a <template>
document.registerElement('cool-menu', {
prototype: Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
// The <template> tag is not visible in the browser, but can still be used
var t = document.querySelector('#menu-template');
var clone = document.importNode(t.content, true);
this.createShadowRoot().appendChild(clone);
}
}
})
});
Use <link rel="import" href="">
to pull in the component dependencies and have even more separation.
<link rel="import" href="menu-item.html">
<style> a { text-decoration: none; font-weight: bold; }</style>
<cool-menu>
<ul>
<menu-item>First Menu Item</menu-item>
<menu-item>Second Menu Item</menu-item>
</ul>
</cool-menu>
menu-item.html
<!-- Everything is completely encapsulated in the Shadow DOM -->
<template id="menu-item-template">
<style>
a { color: orange; }
</style>
<a href="#" class="underline"><content></content></a>
</template>
// There may be a better way to reference the included document versus the including document
// I couldn't get it to work inline with the registration though, so this is how I did it
var thisDoc = document.currentScript.ownerDocument;
var thatDoc = document;
// Register the element to the including page
thatDoc.registerElement('menu-item', {
prototype: Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
// Find the template within this document (and not the including document)
var t = thisDoc.querySelector('#menu-item-template');
var clone = thatDoc.importNode(t.content, true);
this.createShadowRoot().appendChild(clone);
}
}
})
});