Custom Element Conventions
Whenever possible custom elements are defined each in their own ES module
The file name should match the element tag name, so
<my-element>
is inmy-element.mjs
The class name should match the custom element tag name, just in PascalCase.
The custom element registers itself in the
customElements
registry at the end of the file. For examplecustomElements.define("my-element", MyElement);
If the element should be extended by other elements, it should be exported as a named export in the module.
Any other custom elements the element depends on or uses in its template should be imported in the element’s module.
External features of the custom element (attributes, slots, parts, css custom properties, events etc.) are documented using these jsdoc tags: https://custom-elements-manifest.open-wc.org/analyzer/getting-started/#supported-jsdoc
For events the
handleEvents
method callback should be used, and just thethis
reference is passed toaddEventListener
. See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#the_event_listener_callbackThis also lets us modify event handling through inheritance.
Any events attached to elements that aren’t within the custom element need to be removed in the
disconnectedCallback
(and should be registered again if the element gets re-attached).
If the element doesn’t attach a shadow root in its
connectedCallback
, consider using ahasConnected
class field to detect if the main setup logic has already happened.Templates are currently managed in HTML markup and we just document the ID of the template that the custom element expects as
Template ID: #myElementTemplate
. Often the template is in an include file we include anywhere the custom element is used.Styles for the custom element are usually in a separate css file with the same name as the module. If the element has a shadow root, the stylesheet will be loaded into it.
When interacting with other custom elements, you should follow these methods:
To send (or request) information to a child element, call a method on it.
To send information to a parent element (without the parent requesting it in a method call), emit the information in a custom event.
If the state needs to be updated from a parent with an explicit function call, consider using an
initialize
method that is also called from theconnectedCallback
.XPCOM Observers registered with the
nsIObserverService
should generally be weak observers. This is safe, because the custom element (and thus the observer) will usually be owned by a DOM scope. The observing element needs to explicitly declare thensISupportsWeakReference
andnsIObserver
interfaces in theQueryInterface
method. The observer should of course also be unregistered in thedisconnectedCallback
.Unless key repeats are desired, consider using
keyup
listeners for keyboard event handling.
Custom Element Boilerplate
my-element.mjs
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Example component serving as a boilerplate demonstration.
* You should never see this description in the wild.
* Template ID: #myElementTemplate
*
* @attribute {string} label - Label of the button. Observed for changes.
* @tagname my-element
*/
export class MyElement extends HTMLElement {
static get observedAttributes() {
return ["label"];
}
QueryInterface = ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference"
]);
connectedCallback() {
window.addEventListener("resize", this);
Services.obs.addObserver(this, "some-topic", true);
if (this.shadowRoot) {
return;
}
const shadowRoot = this.attachShadow({ mode: "open" });
const styles = document.createElement("link");
styles.rel = "stylesheet";
styles.href = "chrome://messenger/skin/shared/my-element.css";
const template = document
.getElementById("myElementTemplate")
.content.cloneNode(true);
template.querySelector("button").addEventListener("click", this);
template.querySelector("button").textContent = this.getAttribute("label");
shadowRoot.append(styles, template);
document.l10n.connectRoot(shadowRoot);
}
disconnectedCallback() {
window.removeEventListener("resize", this);
Services.obs.removeObserver(this, "some-topic");
document.l10n.disconnectRoot(this.shadowRoot);
}
attributeChangedCallback(attribute) {
switch (attribute) {
case "label":
this.shadowRoot.querySelector("button").textContent = this.getAttribute("label");
break;
}
}
handleEvent(event) {
switch (event.type) {
case "click":
console.log("clicked!");
break;
case "resize":
console.warn("Why would you want a resize listener?");
break;
}
}
observe(subject, topic, data) {
switch(topic) {
case "some-topic":
console.log("observer notification");
break;
}
}
}
customElements.define("my-element", MyElement);
myElementTemplate.inc.xhtml
<template id="myElementTemplate">
<span data-l10n-id="my-element-string"></span>
<button class="button" data-l10n-id="my-element-button"></button>
</template>
Loading Custom Elements
If the template of a custom element relies on another custom element, we will generally directly import that other custom element at the top of the module with a synchronous import statement:
import "chrome://messenger/content/other-element.mjs"; // eslint-disable-line import/no-unassigned-import
If we’re dynamically instantiating a custom element, if we can control it only
happening once consider either using an import()
call, ChromeUtils.importESModule
or
ChromeUtils.defineESModuleGetters()
to only load the module when used:
async function createElementWithImport() {
if (hasElement) {
return;
}
await import("chrome://messenger/content/other-element.mjs");
document.createElement("other-element");
}
function createElementSynchronously() {
if (hasElement) {
return;
}
ChromeUtils.importESModule(
"chrome://messenger/content/other-element.mjs",
{ global: "current" }
);
document.createElement("other-element");
}
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{ OtherElement: "chrome://messenger/content/other-element.mjs" },
{ global: "current" }
);
function createLazyConstructedElement() {
new lazy.OtherElement();
}
Yet another option is to use defineLazyCustomElement
from
chrome://messenger/content/CustomElementUtils.mjs
. It is useful in situations
where the previous options are hard to use.
defineLazyCustomElement(
"other-element",
"chrome://messenger/content/other-element.mjs"
);
function createLazyElement() {
document.createElement("other-element");
}