Revision control

Copy as Markdown

Other Tools

# 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 in
`my-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 example
```js
customElements.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:
- For events the `handleEvents` method callback should be used, and just the
- This 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 a `hasConnected` 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 the
`connectedCallback`.
- XPCOM Observers registered with the `nsIObserverService` should generally be
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 the
`nsISupportsWeakReference` and `nsIObserver` interfaces in the `QueryInterface` method.
The observer should of course also be unregistered in the
`disconnectedCallback`.
- Unless key repeats are desired, consider using `keyup` listeners for keyboard
event handling.
## Custom Element Boilerplate
### my-element.mjs
```js
/* 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";
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
```html
<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:
```js
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:
```js
async function createElementWithImport() {
if (hasElement) {
return;
}
document.createElement("other-element");
}
function createElementSynchronously() {
if (hasElement) {
return;
}
ChromeUtils.importESModule(
{ global: "current" }
);
document.createElement("other-element");
}
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{ global: "current" }
);
function createLazyConstructedElement() {
new lazy.OtherElement();
}
```
Yet another option is to use `defineLazyCustomElement` from
where the previous options are hard to use.
```js
defineLazyCustomElement(
"other-element",
);
function createLazyElement() {
document.createElement("other-element");
}
```