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 || {});