UNPKG

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
# 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`