Create standard and reliable custom elements

The custom elements created can be used in any framework and can be created in a variety of ways. When you want to expose your custom elements, you need to design carefully.

Element name

The first thing to consider when creating a custom element is to define a suitable element name, because duplicate element names are not allowed in the entire document. So you should define a clear naming method in your project:

<!-- Library-Component --> <gem-link></gem-link> <gem-route></gem-route> <!-- Application-Type-Component --> <portal-page-user></portal-page-user> <portal-module-profile></portal-module-profile> <portal-ui-checkbox></portal-ui-checkbox>

Their class name should correspond to the element name, because directly using the constructor is also a way to create elements:

new GemLinkElement(); new PortalPageUserElement(); new PortalModuleProfileElement();

Constructable element

In imperative calls, it is common to configure properties in constructor arguments:

const img = new MyImgElement({ width: 100, height: 100 });

To avoid static attribute defined in the declarative form being overwritten in the constructor, you should only set the passed attributes in the constructor:

@customElement('my-img') class MyImgElement extends GemElement { @numattribute width: number; @numattribute height: number; @property srcObject?: MediaStream | MediaSource | Blob | File; constructor(options = {}) { super(); if (options.width) this.width = options.width; if (options.height) this.height = options.height; this.srcObject = options.srcObject; } }

Attribute or Property

When using Gem to create a custom element, you can define Attribute and Property. Both of them can pass data to the element, and both can make them "observable", that is, when their values are changed, they will trigger element updates. But Attribute can be expressed by Markup, machine readable, and can be directly edited in the browser DevTools, and Attribute has default values, which is very convenient when used inside the element, so if you can use the data represented by Attribute, try to use Attribute. Property is only used for data types not supported by Attribute.

@customElement('portal-module-profile') class PortalModuleProfileElement extends GemElement { @attribute name: string; @numattribute age: number; @boolattribute vip: boolean; @property data?: Data; }

When a property needs to support templates, <slot> (ShadowDOM) or non-responsive Property can be used:

@shadow() @customElement('portal-module-profile') class PortalModuleProfileElement extends GemElement { @slot static name: string; @attribute name: string; get #name() { return html`<slot name=${PortalModuleProfileElement.name}>${this.name}</slot>`; } }@customElement('portal-module-profile') class PortalModuleProfileElement extends GemElement { @attribute name: string; nameSlot?: TemplateResult; get #name() { return this.nameSlot || this.name; } }

TIP

Backwards compatibility can be ensured in the same way when deprecating properties:

@customElement('portal-module-profile') class PortalModuleProfileElement extends GemElement { /**@deprecated */ @property data?: Item[]; @property items?: Item[]; get #items() { return this.items || this.data; } }

Public or Private

When using TypeScript to write Gem elements, their fields and methods are all public by default. Although you can use the private modifier to mark them as private, they are still public in JavaScript and can be accessed outside the element. In order to prevent users from accidentally using these fields and methods, you should use Private Fields in JavaScript:

@customElement('my-element') class MyElement extends GemElement { #valid = false; #process = () => { // }; }

Another advantage of using private fields is that they won't have the same name as GemElement/HTMLelement attributes or methods, which has high benefits when developing complex elements.

addEventListener or onclick

You can use event handler property when adding native DOM event listeners inside elements:

@customElement('my-element') class MyElement extends GemElement { onclick = console.log; }

Never use this method, because they have many disadvantages:

  • According to ES Semantics, it will not work
  • Can be overridden and cancelled outside the element

So you should use addEventListener to register event handlers:

@customElement('my-element') class MyElement extends GemElement { constructor() { super(); this.addEventListener('click', console.log); } }

Handling element errors

When an error occurs in an element, the error should be propagated in an event mode, so that the external event listener can be used to handle the error:

@customElement('my-element') class MyElement extends GemElement { @emitter error: Emitter<string>; async #fetchData = () => { try { //... } catch { this.error('fetch fail...'); } } }

Performance

When writing an element template, can add inline styles, which works in Shadow DOM(not in Light DOM):

@customElement('my-element') @shadow() class MyElement extends GemElement { render = () => { return html` <style> :host { display: contents; } </style> `; } }

This is equivalent to creating a <style> element in each <my-element>. If it is a static style, you should try to use Constructable Stylesheet , It has better performance and lower memory usage:

const styles = css` :host { display: contents; } `; @customElement('my-element') @adoptedStyle(styles) @shadow() class MyElement extends GemElement {}

If you need to render many instances at once, you can use @async to create asynchronous rendering elements, which can avoid blocking the main thread during rendering and guarantee 60fps:

@customElement('my-element') @async() class MyElement extends GemElement {}

Styling

Suppose you use the <my-element> element defined above somewhere else, and for some reason add the hidden attribute in the hope of temporarily hiding it:

html`<my-element hidden>My content</my-element>`;

You will look that the hidden attribute does not take effect because the custom element's style display: contents will override the browser style display: none, so the :host style should be defined carefully to avoid making it difficult for external use, such as using :where:

:host(:where(:not([hidden]))) { display: contents; }

In addition, use @layer to solve the problem of multi-state style coverage of elements; use CSS Nesting simplified stylesheets.

Accessibility

When users use custom elements, they can use the role,aria-* attributes to specify the semantics of the element:

html`<my-element role="region" aria-label="my profile"></my-element>`;

Use ElementInternals to define the default semantics of custom elements, use delegatesFocus or @aria focusable:

@customElement('my-element') @aria({ focusable: true, role: 'region', ariaLabel: 'my profile' }) class MyElement extends GemElement { @boolattribute disabled: boolean; render = () => { return html`<div>Focusable</div>`; } }

NOTE

delegatesFocus or @aria.focusable elements with the disabled attribute will not trigger the click event just like native elements.

Resources: