Custom Elements are really useful! They make reusing markup for functionality across your site as easy as adding an element to a page. Most of the controls I list on this site (and I think all of them at time of writing) are built with custom elements. Over time I've developed a template for new custom elements that contains all of the boilerplate structure and functionality I find generally useful, which is what I'm sharing here. 🙂
Once you've filled in its name, to use the custom element you'll just need to add the code to a JavaScript file loaded onto your page. I recommend putting it in its own file so it can be loaded specifically as-needed.
After you've copied the code, you'll want to find and replace all instances of TEMPLATE
with a name (I recommend Title Case for this, like SwitchControl or TabsElement). You'll also want to set the static Name
variable to the actual name of your element - (something like gw-switch or gw-check-listbox).
/**
* @file
* @author
*/
(function CustomElements(ns) {
ns.TEMPLATE = class TEMPLATE extends HTMLElement {
static InstanceCount = 0; // Global count of instances created
static InstanceMap = {}; // Dynamic map of IDs to instances of the element currently attached
//Element name (see MDN)
static Name = "";
// Element CSS rules
static Style = `${TEMPLATE.Name} {
}`;
InstanceId; // Identifier for this instance of the element
IsInitialized; // Whether the element has rendered its content
/** Creates an instance */
constructor() {
super();
if(!this.getId) {
// We're not initialized correctly. Attempting to fix:
Object.setPrototypeOf(this, customElements.get(TEMPLATE.Name).prototype);
}
this.InstanceId = TEMPLATE.InstanceCount++;
}
/** Shortcut for the root node of the element */
get Root() {
return this.getRootNode();
}
/** Looks up the <head> element (or a fascimile thereof in the shadow DOM) for the element's root */
get Head() {
if(this.Root.head) {
return this.Root.head;
}
if(this.Root.getElementById("gw-head")) {
return this.Root.getElementById("gw-head");
}
const head = document.createElement("div");
head.setAttribute("id", "gw-head");
this.Root.prepend(head);
return head;
}
/**
* Generates a globally unique ID for a key unique to the custom element instance
* @param {String} key Unique key within the custom element
* @returns A globally unique ID
*/
getId(key) {
return `${TEMPLATE.Name}-${this.InstanceId}-${key}`;
}
/**
* Finds an element within the custom element created with an ID from getId
* @param {String} key Unique key within the custom element
* @returns The element associated with the key
*/
getRef(key) {
return this.querySelector(`#${this.getId(key)}`);
}
/** Handler invoked when the element is attached to the page */
connectedCallback() {
this.onAttached();
}
/** Handler invoked when the element is moved to a new document via adoptNode() */
adoptedCallback() {
this.onAttached();
}
/** Handler invoked when the element is disconnected from the document */
disconnectedCallback() {
delete TEMPLATE.InstanceMap[this.InstanceId];
}
/** Performs setup when the element has been sited */
onAttached() {
if(!this.Root.querySelector(`style.${TEMPLATE.Name}`)) {
this.Head.insertAdjacentHTML(
"beforeend",
`<style class=${TEMPLATE.Name}>${TEMPLATE.Style}</style>`
);
}
TEMPLATE.InstanceMap[this.InstanceId] = this;
if(!this.IsInitialized) {
if(document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", this.renderContent);
}
else {
this.renderContent();
}
}
}
/** Handler invoked when the element is disconnected from the document */
renderContent = () => {
// DOM manipulation here
this.IsInitialized = true;
};
}
if(!customElements.get(ns.TEMPLATE.Name)) {
customElements.define(ns.TEMPLATE.Name, ns.TEMPLATE);
}
}) (window.CustomElements = window.CustomElements || {});