- Coding conventions
- Linters/formatters
- TSDoc comments
- No kitchen-sink "base" class and using mix-in
- Lifecycle management
- Component styles for different component states/variants
- Customizing components
- Polymorphism with static properties
- Custom events
- Globalization
- Null checks
- CSS considerations with IE11
- Custom element registration
- Propagating misc attributes from shadow host to an element in shadow DOM
carbon-custom-elements uses ESLint with typescript-eslint for linting, and Prettier for code formatting.
Most of ESLint configurations are same as ones in carbon-components.
In addition to using TypeScript, we try to leverage editors' code assistance feature as much as possible.
For that purpose, we add TSDoc comments to the following:
- All classes
- All properties/methods (including private properties), only exception here is one being overriden
- All type definitions (e.g.
interface,enum)
We strive to avoid kitchen-sink "base" class, for the sake of maintenability and avoiding code bloat. Toward that goal, we use mix-in classes. Instead of manipulating prototype, we simply use ECMAScript class feature (Subclass Factory Pattern), which is, something like:
const Mixin = <T extends Constructor<SomeClass>>(Base: T) => class extends Base {
...
someProperty = someValue;
someMethod() { ... }
...
};To avoid memory leaks and zombie event listeners, we ensure the event listeners on custom elements themselves (hosts) and ones on document, etc. are released when they get out of render tree.
For that purpose, carbon-custom-elements uses @HostListener(type, options) decorator. @HostListener(type, options) decorator works with a custom element class inheriting HostListenerMixin() and attaches an event listener using the target method as the listener. The type argument can be something like document:click so the click event listener is attached to document.
Here's an example seen in <bx-modal> code:
...
import HostListener from '../../globals/decorators/HostListener';
import HostListenerMixin from '../../globals/mixins/HostListener';
...
@customElement(`${prefix}-modal` as any)
class BXModal extends HostListenerMixin(LitElement) {
...
@HostListener('click')
// @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
private _handleClick = (event: MouseEvent) => {
...
};
...
}Carbon core CSS uses BEM modifier like bx--btn--danger to style different states/variants of a component.
OTOH carbon-custom-elements uses attributes to represent different states/variants (e.g. <bx-btn type="danger">), in a similar manner as how attributes influence states/variants of native elements (e.g. <input type="hidden">).
If such states/variants should affect the style of custom element (shadow host), we define attribute styles from the following reasons:
- Taking a cue from native elements with user agent shadow DOM (e.g. UA stylesheet for
<input type="hidden">) - Adding CSS classes on our custom elements by ourselves may conflict with CSS classes set by consumers
Like carbon-components library does, carbon-custom-elements ensures components are written in a flexible manner enough to support use cases different applications have.
Component options are defined as static properties of custom element class, instead of in options object seen in carbon-components.
The primary reason for the difference is that there is no support for constructor arguments in Custom Elements and the use case for using constructor for Custom Elements is rare. It makes instance-level configuration unrealistic.
A component variant with different options can be created by creating a derived class which overrides static properties of component options.
| Area | Example of component option (static property) name |
|---|---|
CSS selectors/classes used in imperative DOM API calls (Doing so allows overriding .render() method) |
selectorNonSelectedItem |
| Custom event names | eventBeforeSelect |
- CSS classes used in template (Should be done by overriding
.render()method)
This codebase intends to support the components being inherited, to some extent. e.g. Compoennts with different options described above. To support that, it's easier for all properties/methods exposed as protected, but it exposes a risk of the component internals being poked around. The current guideline for using protected is the following:
- Ones where override happens within this component library (e.g.
<bx-multi-select>inheriting<bx-dropdown>) - Element ID's auto-generation logic
- (Possibly some more, e.g. ones whose API are stable enough)
To support polymorphism with static properties...
We do:
(this.constructor as typeof CustomElementClass).staticPropName;(customElementInstance.constructor as typeof CustomElementClass).staticPropName;We don't:
CustomElementClass.staticPropName;Wherever it makes sense, carbon-custom-elements translates user-initiated events to something that gives event listeners more context of what they mean. For example, <bx-modal> translates click event on <bx-modal-close-button> to bx-modal-beingclosed and bx-modal-closed custom events.
bx-modal-beingclosed is cancelable in a similar manner as how click event on <a href="..."> is cancelable; If bx-modal-beingclosed is canceled, <bx-modal> stops closing itself.
We define custom event names as static properties so derived classes can customize them.
Like what most of native elements do, the primary means to handle translatable strings is let user put them in DOM, e.g. in attributes, child (text) nodes.
The only exception would be date picker (though this repository hasn't got one yet), where there are huge amount of translatable stings.
To avoid problems with collation, the primary means for user to determine order in list item is ordering them in DOM, for example:
<bx-dropdown>
<bx-dropdown-item value="all">Option 1</bx-dropdown-item>
<bx-dropdown-item value="cloudFoundry">Option 2</bx-dropdown-item>
<bx-dropdown-item value="staging">Option 3</bx-dropdown-item>
</bx-dropdown>If you get TypeScript "may be null" errors, think twice to see if there is such edge case:
- If so, do such check to throw more reasonable exception or to make it no-op if the condition is not met.
- If not, you can now add non-null assertion operator (
!) - But again, don't do that blindly.
lit-element observes for changes in declared properties for updating the view. carbon-custom-elements codebase doesn't use this feature simply to get properties observed. Specifically, carbon-custom-elements doesn't set private/protected properties as declared. Whenever change in private/protected should cause update in the view, we take manual approach (.requestUpdate()).
We use ShadyCSS shim as the emulation of scoped CSS in shadow DOM in IE11. There is one notable limitation with that; It appears that :host(bx-foo) ::slotted(bx-bar) selector does not work in ShadyCSS unless <slot> is a direct child of the shadow root. There was an issue in ShadyCSS repo (webcomponents/shadycss#5) that seems to have explained that in detail, but the repository has been deleted somehow.
To make such case work for ShadyCSS, we add a CSS class to an ancestor of <slot> in shadow DOM, and use .bx-ce--some-class ::slotted(bx-bar) selector.
This library registers custom elements to global window automatically upon importing the corresponding modules.
It may not be desirable in two scenarios:
- One is when consumer wants to customize our custom element's behavior before it's registered. In such case, consumer can create a derived class and register it with a different custom element name.
- Another, though the use case is rare, is using our custom element in a different realm. In such case, consumer can re-register the custom element in the realm.
Some components, e.g. <bx-btn>, simply represent the content in shadow DOM, e.g. <button> in it. It's sometimes desiable for applications to have control of attributes in <button>, for example, adding data- attributes there.
In such case, we let consumer create a derived class. For example, its .attributeChangedCallback() can propagate <bx-btn>'s attribute to <button> in it.