-
Notifications
You must be signed in to change notification settings - Fork 26
Custom Elements
Audience |
---|
Contributors |
Please review "Building Components" from Google.
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.
Theming should be handled in helix-ui.css
, either via custom properties or direct element styling. This keeps branding in an easily replaceable stylesheet.
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.
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.
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);
}
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).
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.
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:
-
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)
-
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.
-
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
, andtfoot
elements behave in relation to HTML tables.
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.
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
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.
- 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.
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; |
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 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.
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.
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).
- Custom Elements Everywhere / Polymer Summit 2017 (youtube)