Skip to content

Custom Elements

Ryan Johnson edited this page Apr 24, 2018 · 11 revisions
Audience
Contributors

Getting Started

Please review "Building Components" from Google.

When to create a Custom Element

TODO: Add flowchart

Custom elements should only be created if you need to add behavior to an element (emit events, automatically apply ARIA attributes, etc.). If you do not need to add behavior, use a built-in, semantic HTML element. If no semantic HTML elements fit your need, use <div> for blocks and <span> for inline elements.

Appearance and 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);
}

Build as much as possible in LightDOM

To retain as much built-in support as possible and to get around flash of unstyled content before custom elements upgrade, it's necessary to have LightDOM styling to set default display and arrangement of custom elements. As such, it makes sense to keep all :host styles in the LightDOM to keep CSS as organized as possible (and to reduce JavaScript file size).

Native Form Controls

There will be cases where developers cannot use custom form control elements and have to rely on native HTML functionality. In this scenario, CSS should be provided to apply styles to native form elements so that they are as close as possible to design system specifications.

NOTE: It is unrealistic to expect fully compliant styling.

Placeholder Custom Elements

Because we don't know which elements will need accessibility support added to them, it's beneficial to create placeholder custom elements for the purposes of consumption. This has a few advantages:

  1. It's more flexible.
    • We can always add JavaScript to upgrade behavior of these elements without the need for consumers to change their source code (e.g., automatically adding aria attributes in the future)
  2. It's easier to style.
    • Given the Light DOM CSS strategy above, we don't need to embed styles in the ShadowDOM, so it's easier to theme.
  3. It provides a simpler implementation.
    • Rather than using named slots on random Light DOM elements, we can achieve similar results with more control over the declarative API.

Avoid

<hx-thing>
  <header slot="head">I'm the head</header>
  Blah blah blah
  <footer slot="foot">I'm the foot</footer>
</hx-thing>
  • If we need to add role or aria attributes to the implementation above, it would require consumers to modify their application source to be compliant.
    • This results in a breaking change to the implementation which slows down innovation when you consider our documented release cadence (one major release per year).
  • To style, just the body, you would need to hope that the ShadowDOM implementation has provided you a way to do so.
    • If the implementation doesn't allow it, you'll have to wait for an update.
    • If you are able to successfully style the content, the style is brittle because it is dependent on that specific implementation of hx-thing. It's likely that an update to the ShadowDOM could break consuming apps.
  • This implementation is less intuitive, because it requires memorization of special html attributes (slot="...") and values.
    • These values are dependent on the ShadowDOM implementation, so it's likely that an update to the ShadowDOM could break consuming apps.

Recommended

<hx-thing>
  <hx-thinghead>I'm the head</hx-thinghead>
  <hx-thingbody>Blah blah blah</hx-thingbody>
  <hx-thingfoot>I'm the foot</hx-thingfoot>
</hx-thing>
  • Automatically adding role or aria attributes to any of the elements above is an enhancement that won't break existing APIs.
    • Consuming developers likely would not need to change their markup to be compliant with required functionality.
  • This implementation is similar to how thead, tbody, and tfoot elements behave in relation to HTML tables.

Communicating with Custom Elements

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

TL;DR - Use properties to configure with complex primitives.

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

Use events to announce state change

Events provide "callback-like" logic for application logic to react to changes.

  • An event should be emitted whenever a user triggers a state change.
  • Emit custom events in attributeChangedCallback()
    • This helps to ensure maximum compatibility with client-side frameworks and provides a consistent location for event emission.

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.

Configure State via Properties instead of Methods

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;

JavaScript Methods

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.

Example

elSearch.clear() // same as user clicking "X" to clear the value

Custom Form Controls

Custom form controls are elements that provide consistent behavior and appearance, across supported browsers, for the purposes of submitting form data (e.g. Date Picker, Time Picker, Checkbox, Radio, etc.). Custom form controls are not natively supported by browsers and will rely upon some guidelines to ensure maximum compatibility with consuming applications.

Do not attempt to support native form functionality

TL;DR - It's too complex to tackle, given current browser limitations.

Native <form> elements do not know how to submit data gathered in custom control elements. This is a limitation with how the <form> element is hardwired to work only with native form elements. Currently, no API exists to tap into the hardwired behavior, so the only way to add compatibility is to dynamically inject one of the supported elements into the LightDOM (surrogate control). This should allow the native <form> element to submit the captured value of the custom control along with the rest of the form data.

While this may be sound in theory, there are many variables that add complexity to the equation...

  • How do client-side libraries handle the surrogate control as its custom control is added/removed from the DOM?
  • How do we keep the value of the surrogate control synchronized with the internal state of the custom control in an efficient manner?
  • How do we ensure that only one surrogate control is created?
  • How do we remove the surrogate control if it's no longer needed?
  • How do event listeners affect functionality?
  • etc.

Events and ShadowDOM

As mentioned above in "Communicating with Custom Elements", use events to announce state change within custom elements. This provides a consistent API for client-side libraries to use in synchronizing overall application state. By using native form elements (e.g. <input>, <textarea>, <select>, etc.) in the ShadowDOM, we can piggyback off of existing browser functionality.

  • Events that originate from the native elements will bubble up and retarget the custom control elements (no need to emit custom events).
  • Built-in browser heuristics, accessibility, and functionality can be leveraged to help provide needed functionality

Example: <hx-checkbox>

<!-- Shadow DOM -->
<div id="wrapper">
  <input type="checkbox" />
  <div id="control">
    <hx-icon type="checkmark"></hx-icon>
    <hx-icon type="minus"></hx-icon>
  </div>
</div>

With the above markup, we can consistently style the appearance of the div#control element based on the state of the native checkbox input using sibling selectors (input:checked + #control). Combined with some default styling, the user will only see the styled control, but they'll be interacting with the native checkbox (win-win for accessibility, appearance, and behavior).

Reference

  • Custom Elements Everywhere / Polymer Summit 2017 (youtube)
Clone this wiki locally