Custom Elements - HelixDesignSystem/helix-ui GitHub Wiki

[Add TOC Here]

Best Practices

Please review "Building Components" and "Best Practices" from Google before continuing.

General

Avoid setting attributes on this in constructor

HTML spec prohibits returning an instance containing attributes from constructor().

Recommended

  • Double check that logic in constructor() does not call this.setAttribute().

Avoid using getElementById()

The getElementById function is only available on the document object. If this.getRootElement().getElementById() were called before the element instance has connected to the DOM, this.getRootElement() will return the element instance (not the document) and getElementById will be undefined.

Recommended

  • Use querySelector() instead.

Avoid watching attributes that don't affect behavior

For every watched attribute, a handler should be defined that reacts to changes of the attribute value.

Recommended

  • Remove entries from observedAttributes without corresponding logic in attributeChangedCallback().

Communication

Custom Element Interface (elements & properties IN, events OUT)

Use Properties and Attributes to get/set configurations of a custom element.

  • Do not use Attributes for complex primitives (Object, Array, etc.)
  • Attributes always consumed as Strings in the element definition.
    • You may have to coerce certain values to other native primitives.

Avoid complex primitives in HTML attributes

Attributes are always consumed as strings in the element definition. You'll have to coerce certain values to other native primitives.

While you might be able to serialize certain objects into JSON format, it is highly discouraged because:

  • it can be computationally expensive to serialize/deserialize objects
  • javascript references within objects will be lost in the serialization process

Recommended

  • Use properties to apply complex primitive configuration values.

Use events to communicate state change and/or user interaction

Events provide "callback-like" logic for application logic to react to changes. This provides a consistent API for client-side libraries to use in synchronizing overall application state.

  • Emit an event when a user triggers a significant action
    • e.g., "open", "close", "dismiss", "retry", etc.

JavaScript Properties

  • A property should exist for every HTML attribute.
    • If it can be configured in HTML, it should be configurable via JavaScript.
  • Do not create HTML attributes for every JavaScript property.
    • Not everything that can be configured in JavaScript should be configurable in HTML attributes.
    • Some properties may be impossible to configure via HTML attributes.
  • Properties should be named similar to their HTML attribute
    • e.g., avoid defining a valid property that reflects to the invalid attribute

Configure element state via properties

Using methods to modify state adds unnecessary complexity to both implementation and consumption.

AVOID RECOMMEND
elReveal.open() elReveal.open = true;
elReveal.close() elReveal.open = false;
elReveal.toggle() elReveal.open = !elReveal.open;

Define declarative methods for user interactions

If a user can do it, I should be able to do the same via JavaScript.

Define public methods for any functionality that a user can trigger. This has the added benefit of providing an API that can simplify certain testing strategies.

Example

elSearch.clear() // same as user clicking "X" to clear the value
elDisclosure.click() // same as user triggering a disclosure

Styling

Theming should be handled in helix-ui.css, either via custom properties or direct element styling. This keeps branding in an easily replaceable stylesheet.

Avoid altering document flow

Custom elements should not alter the flow of the document that they are included within. If they are meant to consume a certain geometry after upgrade, they should also consume the same geometry before upgrade.

Avoid styling LightDOM elements from within ShadowDOM

Generally, it is not a good idea to alter the styles of elements in the parent document from within the ShadowDOM of a custom element. This becomes difficult to debug and can be very frustrating for consumers to implement.

The ::slotted() selector is available, but it has limitations on what can be styled and it has lower specificity/priority than LightDOM CSS.

Use custom properties to aid theming

Elements whose appearance are being handled purely within the ShadowDOM should try to implement overridable styles using CSS Variables (i.e., "Custom Properties").

While not all browsers support custom properties, fallback styles can be implemented.

#shadowElement {
  /* Legacy Fallback */
  border-color: #777;
  /* Modern Browsers */
  border-color: var(--border-color, #777);
}

Form Controls

Let's start by defining some terms:

  • Vanilla Control
    • HTML element used to submit data via <form> functionality (e.g., <input>, <select>, <textarea>, etc.)
    • Appearance and behavior exists entirely in LightDOM.
  • Custom Control
    • Custom HTML element that encapsulates all control state within a ShadowDOM
    • Appearance and behavior exists entirely in ShadowDOM.
  • Hybrid Control
    • Custom HTML element that styles a vanilla control, by encapsulating markup and styles within a ShadowDOM.
    • Styling applied in ShadowDOM (maybe some LightDOM, too)
    • Vanilla control behavior remains in LightDOM

Avoid custom controls

Keep all submittable controls in the LightDOM.

Currently, no APIs exist to tap into the hardcoded behavior of <form>.

Native <form> elements are not aware of the vanilla controls contained within the ShadowDOM of a custom control. This is because the <form> element is hard coded to serialize data values of vanilla controls within itself. If a vanilla control isn't present in the LightDOM, the <form> isn't aware of it, and its data will not be included in the payload when the form is submitted.

Events and ShadowDOM

Using vanilla controls in the ShadowDOM will be subject to issues mentioned above, but we can piggyback off of their existing functionality to modify internal state of a custom element.

  • Bubbling events that originate from within the ShadowDOM will retarget the custom element.
    • No need to emit custom events
  • Non-bubbling events will need to be re-emitted from the custom element.
    • e.g. blur, focus, scroll, etc.
  • Built-in browser heuristics, accessibility, and functionality can be leveraged to help provide needed functionality.
    • e.g., tab order, keyboard interaction, etc.

Example: <hx-search>

<!-- Shadow DOM -->
<div id="wrapper">
  <input type="text" />
  <button id="clear">
    <hx-icon type="times"></hx-icon>
  </div>
</div>

With the above markup, we can show or hide the button#clear as a user enters text into the input. By adding a 'click' event listner on the button, we can clear the value and return focus to the input. Combined with some styling, the user will only see the custom element, but they'll be interacting with the vanilla <input> and <button> elements (win-win for accessibility, appearance, and behavior).


When to create a Custom Element

NEEDS REVIEW

[Add flowchart here]

SEEK

  • Use semantic HTML elements as much as possible
    • Most HTML elements have accessibility features baked into their implementation that cannot be imitated by custom elements.
  • Create a custom element if
    • you need to define behavior that isn't available in semantic HTML elements
      • e.g., Tabs, Menus, Popovers, Modals, etc.
    • you need to decorate Light DOM content with static markup
      • e.g., <hx-error>

AVOID

  • Avoid creating empty custom elements
    • (i.e., no JavaScript prototype, no customElements.define()).
    • The lack of documentation for these elements is confusing to consumers.
  • Avoid creating custom elements for form inputs
    • browser APIs currently have no way to hook into form submission logic
      • if you need styled controls:
        • if possible, prioritize styling elements directly with CSS
        • otherwise, wrap native <input>, <select>, and <textarea> elements for styling via Shadow DOM

Reference

⚠️ **GitHub.com Fallback** ⚠️