carbon-components
Version:
The Carbon Design System is IBM’s open-source design system for products and experiences.
359 lines (266 loc) • 15.3 kB
Markdown
# Carbon component model
Carbon has a very simple component model for vanilla JavaScript, like below,
covering basic lifecycle of [creation](#creation)/[clean-up](#clean-up). Most of
the interface is implemented in [`create-component.js`](./create-component.js)
mixin, explained [later](#component-lifecycle-create-componentjs).
```typescript
interface Handle {
// Clean things up, e.g. event handlers
release(): null;
}
// No mandatory properties in component options at bare-bone component model
// (But mix-ins define ones, e.g. `.selectorInit` used for most components)
interface ComponentOptions {}
interface Component extends Handle {
// The constructor takes the DOM element to work with, and instance-specific options
new (element: Element, options: ComponentOptions = {});
// List of components instantiated by this component
// `.release()` in this component should release them
children: Component[];
// Factory method, checks for existing instance before calling the constructor
static create(element: Element, options: ComponentOptions = {}): Component;
// Registry of component instances
static WeakMap<Element, Component> components;
// Default options
static ComponentOptions options;
}
```
## Example
```javascript
import { Loading } from `carbon-components`;
// Where HTML snippet like one in http://carbondesignsystem.com/components/loading/code is
const element = document.querySelector('[data-loading]');
// Instantiates `Loading` (spinner) without making it spinning
const loading = Loading.create(element, { active: false });
loading.set(true); // Starts the spinner
loading.set(false); // Stops the spinner
// Returns an existing instance if there is one, creates a new instance otherwise
console.log(Loading.create(element) === loading); // `true`
// Looks for an existing instance
console.log(Loading.components.get(element) === loading); // `true`
```
# Carbon component mixins
Carbon component mixins, based on
[Subclass Factory Pattern](https://github.com/justinfagnani/proposal-mixins#subclass-factory-pattern),
provides the basis for Carbon component classes by allowing component
implementation to compose small pieces of functionalities to base them on,
instead of introducing "fat base class".
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Component lifecycle (`create-component.js`)](#component-lifecycle-create-componentjs)
- [Creation](#creation)
- [Clean-up](#clean-up)
- [Registry of component instances](#registry-of-component-instances)
- [Sugar layers for component instantiation](#sugar-layers-for-component-instantiation)
- [Searching for DOM nodes to instantiate components on (`init-component-by-search.js`)](#searching-for-dom-nodes-to-instantiate-components-on-init-component-by-searchjs)
- [Lazily instantiating a component upon an event on a root element (`init-component-by-event.js`)](#lazily-instantiating-a-component-upon-an-event-on-a-root-element-init-component-by-eventjs)
- [Lazily instantiating a component upon an event on a launcher button (`init-component-by-launcher.js`)](#lazily-instantiating-a-component-upon-an-event-on-a-launcher-button-init-component-by-launcherjs)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Component lifecycle ([`create-component.js`](./create-component.js))
[`create-component.js`](./create-component.js) mixin covers component lifecycle,
which is, [creation](#creation) and [clean-up](#clean-up).
### Creation
[Static `.create(element)` method](https://github.com/IBM/carbon-components/blob/0336425/src/globals/js/mixins/create-component.js#L44-L46)
covers the creation part. It first checks if there is an instance of the same
component class with by looking up static `components` property for one
associated with the given `element`, and simply returns it if there is one.
Otherwise, a new instance of the component is created by calling the constructor
with the given `element` and returns the new instance.
A Carbon component works with the given `element` and its descendants to hook
event handlers on, change DOM properties of, mangle styles of, and so on. We
call it the _root element_ of the component.
```javascript
import mixin from 'carbon-components/src/globals/js/misc/mixin.js';
import createComponent from 'carbon-components/src/globals/js/mixins/create-component.js';
class MyClass extends mixin(createComponent) {
…
// Every component must define static `components` property
static components = new WeakMap();
// Every component must define static `options` property
static options = { foo: 'foo0', bar: 'bar0' };
}
const div = document.body.appendChild(document.createElement('div'));
const options = { foo: 'foo1', baz: 'baz1' };
const myClassInstance = MyClass.create(div, options);
```
The constructor in `create-component.js` mixin sets the following properties:
| Name | Description |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `element` | The root element of the component (see above). |
| `options` | The component options. |
| `children` | The array of Carbon component instances that has to be released along with this component. An example is with [overflow menu](http://www.carbondesignsystem.com/components/overflow-menu/code) component which creates another Carbon component called [floating menu](../../../components/floating-menu/floating-menu.js), and the floating menu has to be released along with the overflow menu. |
The `.options` property inherits static `.options` property and merges in the
2nd argument of static `.create()` method. In above example, `.options` will be
equal to `{ foo: 'foo1', bar: 'bar0', baz: 'baz1' }`.
### Clean-up
[`.release()`](https://github.com/IBM/carbon-components/blob/0336425/src/globals/js/mixins/create-component.js#L51-L57)
method covers the clean-up part. It takes care of cleaning-up Carbon components
in `.children` property as well as one in static `.components` property.
If a Carbon component has other things to clean-up (e.g. event listeners), it
can override the `.release()` method and write code there to clean things up.
`.release()` method should return `null` to allow the caller of `.release()`
e.g. to assign the return value to a variable referring to Carbon component
instance, marking that the instance is gone.
### Registry of component instances
Every component must define static `components` property, which is an instance
of
[`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap).
The constructor in `create-component.js`
[sets the component instance to static `.components` property](https://github.com/IBM/carbon-components/blob/0336425/src/globals/js/mixins/create-component.js#L37),
mapped with the DOM element the component is instantiated on. This allows
application code to grab a Carbon component instance associated with the root
element.
## Sugar layers for component instantiation
Carbon defines three types of mixins to allow applications to instantiate
components in batch. A component can inherit one of those three mixins to
support batch instantiation.
### Searching for DOM nodes to instantiate components on ([`init-component-by-search.js`](./init-component-by-search.js))
The most basic one is
[`init-component-by-search.js`](./init-component-by-search.js), which searches
for DOM elements matching component option's `.selectorInit` property.
For example, given the following HTML:
```html
<main>
<div data-my-component>The content of component 0</div>
<div data-my-component>The content of component 1</div>
</main>
```
The call of static `.init()` method below instantiates components on the two
`<div>`s above:
```javascript
import mixin from 'carbon-components/src/globals/js/misc/mixin.js';
import createComponent from 'carbon-components/src/globals/js/mixins/create-component.js';
import initComponentBySearch from 'carbon-components/src/globals/js/mixins/init-component-by-search.js';
class MyClass extends mixin(createComponent, initComponentBySearch) {
…
static components = new WeakMap();
static options = {
selectorInit: '[data-my-component]',
};
}
const main = document.querySelector('main');
// Instantiates components on two `<div>`s above
MyClass.init(main);
```
### Lazily instantiating a component upon an event on a root element ([`init-component-by-event.js`](./init-component-by-event.js))
[`init-component-by-event.js`](./init-component-by-event.js) mixin allows
delaying component instantiation until an event happens on a DOM element that
will be the root element. For example, the static `.init()` method of Carbon
[`Tooltip` component](http://www.carbondesignsystem.com/components/tooltip/code)
delays instantiating the components until user hovers the mouse over a trigger
button or user puts the keyboard focus on a trigger button.
Given the same example HTML as the one for `init-component-by-search.js` above,
the call of static `.init()` method below detects `click` events on DOM elements
with `data-my-component` attribute, and instantiates the component when such
event happens:
```javascript
import mixin from 'carbon-components/src/globals/js/misc/mixin.js';
import createComponent from 'carbon-components/src/globals/js/mixins/create-component.js';
import initComponentByEvent from 'carbon-components/src/globals/js/mixins/init-component-by-event.js';
class MyClass extends mixin(createComponent, initComponentByEvent) {
constructor(element) {
super(element);
// Side note: Make sure calling `.removeEventListener()` in the `.release()` method
element.addEventListener('click', this._handleClick);
}
// Called when this component is instantiated upon an event
createdByEvent(evt) {
this._handleClick(evt);
}
…
_handleClick = evt => {
alert('clicked!');
};
…
static components = new WeakMap();
static options = {
selectorInit: '[data-my-component]',
// Clicking on a DOM element with `data-my-component` attribute will instantiate this component
initEventNames: ['click'],
};
}
const main = document.querySelector('main');
MyClass.init(main);
```
Changing `initEventNames` option above allows you to hook more/different event
types to instantiate components upon.
`.createdByEvent()` method allows you to run an event handler of the event that
caused instantiating the component.
### Lazily instantiating a component upon an event on a launcher button ([`init-component-by-launcher.js`](./init-component-by-launcher.js))
[`init-component-by-launcher.js`](./init-component-by-launcher.js) mix-in allows
delaying component instantiation until an event happens on an element that has
semantic association to the component. For example, the static `.init()` method
of Carbon
[`Modal` component](http://www.carbondesignsystem.com/components/modal/code)
delays instantiating the components until user clicks on a launcher button.
```html
<main>
<button
class="bx--btn bx--btn--secondary"
type="button"
data-my-component-target="#id-of-my-component"
>
Launch
</button>
<div data-my-component id="id-of-my-component">
The content of my component
</div>
</main>
```
Given the example HTML above, the call of static `.init()` method below detects
`click` events on DOM elements with `data-my-component-target` attribute, and
when one happens, looks for the DOM element the element's
`data-my-component-target` attribute points to as a selector, and instantiates a
component on the DOM element if found.
```javascript
import mixin from 'carbon-components/src/globals/js/misc/mixin.js';
import createComponent from 'carbon-components/src/globals/js/mixins/create-component.js';
import initComponentByLauncher from 'carbon-components/src/globals/js/mixins/init-component-by-launcher.js';
class MyClass extends mixin(createComponent, initComponentByLauncher) {
…
// Called when user clicks on the launcher button
createdByLauncher(evt) {
alert('launched!');
}
…
static components = new WeakMap();
static options = {
selectorInit: '[data-my-component]',
// Clicking on DOM elements with `data-my-component-target` attribute
// will look for a DOM element `data-my-component-target` points to,
// and will instantiate this component on the DOM element found
attribInitTarget: 'data-my-component-target',
initEventNames: ['click'],
};
}
const main = document.querySelector('main');
MyClass.init(main);
```
Changing `attribInitTarget` option above allows you to change the attribute name
to associate a trigger button with a component's root element.
Changing `initEventNames` option above allows you to hook more/different event
types to instantiate components upon.
`.createdByLauncher()` method allows you to run an event handler of the event
that caused instantiating the component.
# Other mixins
## `evented-state.js`
In our components, oftentimes clicking a UI element implies a certain state
change. Examples are things like closing menus, opening modals, or changing a
page. In our vanilla library, we try to emit CustomEvents for these actions to
let the consuming developer respond to them with a callback function.
_Public Methods added_
- `changeState`
**\*Required** Private Methods\*
- `_changeState`
## `evented-show-hide-state.js`
This one adds hide/show methods to your component and kicks off the change
state - you're expected to add in the element that triggers the action, as well
as a callback to do something with the state change.
_Public Methods Added_
- `show`
- `hide`
## `track-blur.js`
Adds a blur handler to your component - expects a handleBlur method to be added
into the consuming component
**Required Public Methods added**
- `handleBlur`