<ceb/> ~ Custom Element Builder

<ceb/> is a library helping to develop Custom Elements (v1). Its core is a builder which executes others builders. By this way, <ceb/> is natively opened to extensions and builders easily sharable.

<ceb/> is released under the MIT license.

The source code is available on GitHub: github.com/tmorin/ceb.

Installation

From npm or yarn or ... from npm what?

npm install @tmorin/ceb

And also directly in the browser via unpkg.com

<!-- the optimized Universal Module Definition -->
<script src="https://unpkg.com/@tmorin/ceb/dist/ceb.min.js"></script>
<!-- the not optimized Universal Module Definition -->
<script src="https://unpkg.com/@tmorin/ceb/dist/ceb.js"></script>

The builders and decorators

<ceb/> provides several built-in builders handling the common requirements. For each builder, decorators counter-parts are available.

First, the custom element has to be registered using the builder ElementBuilder.

Then, other builders can be used to enhance it:

ElementBuilder

The class ElementBuilder provides services to enhance and register a custom element. It's the main builder, the entry point of the library.

The static method CustomElement.get(constructor) returns a fresh builder. The method expects the constructor of the custom element.

import {ElementBuilder} from '@tmorin/ceb'
// defines the custom element class
class MyCustomElement extends HTMLElement {
    constructor() {
        super()
    }
}
// creates the builder
const builder = ElementBuilder.get(MyCustomElement)

The builder and underlying decorators are also technically documented: ElementBuilder.

Registering a new custom element

A custom element is registered with the method ElementBuilder#register().

import {ElementBuilder} from '@tmorin/ceb'
// defines the custom element class
class MyCustomElement extends HTMLElement {
    constructor() {
        super();
    }
}
// creates the builder
const builder = ElementBuilder.get(MyCustomElement)
// register the custom element
builder.register()

The custom element can also be registered by a decorator.

import {ElementBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>()
// defines the custom element class
class MyCustomElement extends HTMLElement {
    constructor() {
        super()
    }
}

By default, the name of the custom element is the kebab case of the class name. So, MyCustomElement becomes my-custom-element.

Once registered, the custom element can be created as any other HTML elements.

<!-- creates the custom element in HTML as any other HTML elements -->
<my-custom-element></my-custom-element>
// creates the custom element from its tag name
const myCustomElement = document.createElement('my-custom-element')
// appends the custom element as any other regular HTML element
document.body.append(myCustomElement)

Extending a built-in element

To register custom element which extends a built-in HTML elements, the tag name of the extended element has to be provided using the method ElementBuilder#extends().

import {ElementBuilder} from '@tmorin/ceb'
// defines the custom element class
class MyCustomButton extends HTMLButtonElement {
    constructor() {
        super()
    }
}
// creates the builder
const builder = ElementBuilder.get(MyCustomButton)
// provides the tag name of HTMLButtonElement
builder.extends('button')
// register the custom element
builder.register()

The extended HTML element can also be provided with the decorator.

import {ElementBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomButton>({
    // provides the tag name of HTMLButtonElement
    extends: 'button'
})
// defines the custom element class
class MyCustomButton extends HTMLButtonElement {
    constructor() {
        super()
    }
}

Once registered, the custom element can be created.

<!-- creates the extended HTML element using the `is` attribute -->
<button is="my-custom-button"></button>
// creates the extended HTML element
const myCustomElement = document.createElement('button', {is: 'my-custom-button'})
// appends the extended HTML element as any other regular HTML element
document.body.append(myCustomElement)

Overriding the name of the custom element

The name of the custom element can be overridden using the method ElementBuilder#name(tagName).

import {ElementBuilder} from '@tmorin/ceb'
// defines the custom element class
class MyCustomElementBis extends HTMLElement {
    constructor() {
        super()
    }
}
// creates the builder
const builder = ElementBuilder.get(MyCustomElementBis)
// overrides the default tag name
builder.name('another-name')
// register the custom element
builder.register()

The name of the custom element can also be provided with the decorator.

import {ElementBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElementBis>({
    // overrides the default tag name
    name: 'another-name'
})
// defines the custom element class
class MyCustomElementBis extends HTMLButtonElement {
    constructor() {
        super()
    }
}

In this case, the name of the custom element is another-name.

<!-- creates the custom element in HTML as any other HTML elements -->
<another-name></another-name>

An example

The registered custom element is a simple element having the text content Hello! I'm <the element's name>..

See the Pen ceb - ElementBuilder by Thibault Morin (@tmorin) on CodePen.

AttributeBuilder

The class AttributeBuilder provides services to initialize an attribute and react on changes.

The static method AttributeBuilder.get(attrName) returns a fresh builder. The builder expects the name of the attribute in kebab case.

import {AttributeBuilder} from '@tmorin/ceb'
// creates the builder
const builder = AttributeBuilder.get('an-attribute')

The builder and underlying decorators are also technically documented: AttributeBuilder.

Boolean value

By default an attribute is a string value. The method AttributeBuilder#boolean() can be used to force a boolean one.

import {AttributeBuilder} from '@tmorin/ceb'
// creates the builder
const builder = AttributeBuilder.get('a-boolean-attribute').boolean()

The value true means the attribute exists: element.hasAttribute('a-boolean-value') === true. When true, the value of the attribute is an empty string.

The value false means the attribute doesn't exist: element.hasAttribute('a-boolean-value') === false.

Default value

Once instantiated, an attribute can have a default value. The method AttributeBuilder#default(value) can be used to set the default value.

import {AttributeBuilder} from '@tmorin/ceb'
// creates the builder and set the default value `a default value`
const builder = AttributeBuilder.get('an-attribute').default('a default value')

An attribute of type boolean can also have a default value.

import {AttributeBuilder} from '@tmorin/ceb'
// creates the builder and set the default value `false`
const builder = AttributeBuilder.get('an-attribute').boolean().default(true)

Reacting on changes

Listeners can be registered in order to react on attribute changes. The method AttributeBuilder#listener(listener) can be used to set the default value.

import {AttributeBuilder} from '@tmorin/ceb'
// creates the builder and add a listener
const builder = AttributeBuilder.get('an-attribute').listener((el, data) => {
    console.log(el.tagName, data.attrName, data.oldVal, data.newVal);
})

The decorator

Attributes can also be defined using a decorator.

import {ElementBuilder, AttributeBuilder, AttributeListenerData} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>()
// defines the custom element class
class MyCustomElement extends HTMLElement {
    // bind the method to the attribute 'an-attribute'
    @AttributeBuilder.listen()
    onAnAttribute(data: AttributeListenerData) {
        console.log(data);
    }
}

An example

The registered custom element is the item of a todo list. Its API is two attributes. The first one, content, is the description of the task. The second one, done, is a boolean saying if the task is done or not.

See the Pen ceb - AttributeBuilder by Thibault Morin (@tmorin) on CodePen.

FieldBuilder

The class FieldBuilder provides services to define fields. A field is an attribute bound to a property. The value is hosted by the attribute but it can be mutated using the bound property.

The static method FieldBuilder.get(attrName) returns a fresh builder. The builder expects the name of the property in camel case.

import {FieldBuilder} from '@tmorin/ceb'
// creates the builder
const builder = FieldBuilder.get('aField')

The builder and underlying decorators are also technically documented: FieldBuilder.

Boolean value

By default a field is a string value. The method FieldBuilder#boolean() can be used to force a boolean one.

import {FieldBuilder} from '@tmorin/ceb'
// creates the builder
const builder = FieldBuilder.get('aBooleanField').boolean()

The value true means the attribute exists: element.hasAttribute('a-boolean-value') === true. When true, the value of the attribute is an empty string.

The value false means the attribute doesn't exist: element.hasAttribute('a-boolean-value') === false.

Attribute name

By default, the attribute name is the kebab case of the property name. It can be overridden using the method FieldBuilder#attribute(attrName).

import {FieldBuilder} from '@tmorin/ceb'
// creates the builder and overrides the attribute name
const builder = FieldBuilder.get('aField').attribute('another-attribute-name')

Reacting on changes

Listeners can be registered in order to react on field changes. The method FieldBuilder#listener(listener) can be used to set the default value.

import {FieldBuilder} from '@tmorin/ceb'
// creates the builder and add a listener
const builder = FieldBuilder.get('aField').listener((el, data) => {
    console.log(el.tagName, data.propName, data.attrName, data.oldVal, data.newVal);
})

The decorators

Fields can also be defined using decorators.

import {ElementBuilder, FieldBuilder, FieldListenerData} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>();
// defines the custom element class
class MyCustomElement extends HTMLElement {
    // defines the field
    @FieldBuilder.field()
    altName = 'a field';
    // defines the listener
    @FieldBuilder.listen()
    onAltName(data: FieldListenerData) {
        console.log(data);
    }
}

An example

The registered custom element is the item of a todo list. Its API is two fields. The first one, content, is the description of the task. The second one, done, is a boolean saying if the task is done or not.

See the Pen ceb - FieldBuilder by Thibault Morin (@tmorin) on CodePen.

OnBuilder

The class OnBuilder provides services to listen to DOM events. Listeners are added on connectedCallback and removed on disconnectedCallback.

The static method OnBuilder.get(clauses) returns a fresh builder. The builder expects the clauses defining the event types to listen to and eventually a query selector. Clauses have to be separated by comas.

import {OnBuilder} from '@tmorin/ceb'
// creates the builder
const builder = OnBuilder.get('click, dblclick')

The builder and underlying decorators are also technically documented: OnBuilder.

Listening to events from the custom element

By default, the listeners are added to the custom element it-self.

import {OnBuilder} from '@tmorin/ceb'
// add a listener to the custom element listening to `click` events
const builder = OnBuilder.get('click').invoke((el, evt, target) => {
    console.log(el.tagName, evt.type, target.tagName);
    console.assert(el.tagName === target.tagName)
})

Listening to events from a child node

The listeners can also be added to a child node. The query selector targeting the child node has to be given next to the DOM event type.

import {OnBuilder} from '@tmorin/ceb'
// add a listener to the first child button listening to `click` events
const builder = OnBuilder.get('click button').invoke((el, evt, target) => {
    console.log(el.tagName, evt.type, target.tagName);
    console.assert(el.tagName !== target.tagName);
    console.assert('BUTTON' === target.tagName)
})

Bubbling and capture phase

By default, the listeners are invoked on the bubbling phase. The method OnBuilder#capture() can be used to force the capture phase.

import {OnBuilder} from '@tmorin/ceb'
// add a listener listening `click` events on the capture phase
const builder = OnBuilder.get('click button').capture()

More information are available on developer.mozilla.org about event bubbling and capture.

event.preventDefault() and event.stopPropagation()

The method Event#preventDefault() and Event#stopPropagation() can be automatically called.

import {OnBuilder} from '@tmorin/ceb'
// add a listener listening `submit` events and preventing the default behavior
const builder = OnBuilder.get('submit').prevent();
// add a listener listening `submit` events and stopping the event propagation
const builder = OnBuilder.get('button').stop();
// add a listener listening `submit` events and preventing the default behavior as well as stopping the event propagation
const builder = OnBuilder.get('button').skip()

Event delegation

Event delegation allows us to attach a single event listener, to a parent element, that will fire for all descendants matching a selector, whether those descendants exist now or are added in the future. c.f. JQuery doc

The method OnBuilder#delegate(selector) is used to define the selector.

import {OnBuilder} from '@tmorin/ceb'
// add a listener listening `click` events on children matching the selector `li button.delete`
const builder = OnBuilder.get('click').delegate('li button.delete')

Shadow DOM

By default, the listeners are listening events coming from the light DOM.

The method OnBuilder#shadow() adds the listener to the shadowRoot property. So that, the listener only listen to events coming from the shadow DOM.

import {OnBuilder} from '@tmorin/ceb'
// add a listener listening `click` events coming from the shadow DOM
const builder = OnBuilder.get('click').shadow()

The decorator

On listeners can also be defined using decorator.

import {ElementBuilder, OnBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>();
// defines the custom element class
class MyCustomElement extends HTMLElement {
    // defines the listener
    @OnBuilder.listen('event-name')
    on(data: Event) {
        console.log(data);
    }
}

Example

The registered custom element is an extension of the ul element. It reacts on click events coming from button. When a button is clicked, its parent li is removed from the DOM.

See the Pen ceb ~ OnBuilder by Thibault Morin (@tmorin) on CodePen.

TemplateBuilder

The class TemplateBuilder provides service to initialize the HTML content of the custom element.

The static method TemplateBuilder.get(content) returns a fresh builder. The builder expects a content in string or a function providing it.

import {TemplateBuilder} from '@tmorin/ceb'
// creates the builder
const builder = TemplateBuilder.get('<strong>the content</strong>')

The builder and underlying decorators are also technically documented: TemplateBuilder.

Initialize the shadow DOM

By default, the builder initializes the light DOM of the custom element. The method TemplateBuilder#shadow(focus) can be used to force the initialization of a shadow DOM.

import {TemplateBuilder} from '@tmorin/ceb'
// initializes the shadow DOM of the custom element
const builder = TemplateBuilder.get('a content').shadow()

The decorator

Templates can also be defined using decorators.

import {ElementBuilder, TemplateBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>()
// define the template
@TemplateBuilder.template({content: '<p><input></p>', isShadow: true})
// defines the custom element class
class MyCustomElement extends HTMLElement {
}

Example

The registered custom element is initialized with a shadow DOM wrapping its light DOM.

See the Pen </ceb> ~ TemplateBuilder by Thibault Morin (@tmorin) on CodePen.

ReferenceBuilder

The class ReferenceBuilder provides services to bind a property to a embedded DOM element.

The static method ReferenceBuilder.get(propName) returns a fresh builder. The builder expects the name of the property in camel case.

import {ReferenceBuilder} from '@tmorin/ceb'
// creates the builder
const builder = ReferenceBuilder.get('myInput')

The builder and underlying decorators are also technically documented: ReferenceBuilder.

Default selector

By default, the builder binds the property to a child having the same id. For instance, the property propName is bound to the selector #propName.

The method ReferenceBuilder#selector(selector) can be used to override the default selector.

import {ReferenceBuilder} from '@tmorin/ceb'
// initializes the shadow DOM of the custom element
const builder = ReferenceBuilder.get('myInput').selector('input.my-input')

Bind to list of elements

By default, the builder binds the property to a single element.

The method ReferenceBuilder#array() can be used to bind the property to a list of matching elements.

import {ReferenceBuilder} from '@tmorin/ceb'
// initializes the shadow DOM of the custom element
const builder = ReferenceBuilder.get('activeLiList').selector('li.active').array()

Bind relative to the shadow DOM

By default, the builder binds the property relative to the light DOM.

The method ReferenceBuilder#shadow() can be used to bind the property relative to the shadow DOM.

import {ReferenceBuilder} from '@tmorin/ceb'
// initializes the shadow DOM of the custom element
const builder = ReferenceBuilder.get('button').shadow()

The decorator

References can also be defined using decorators.

import {ElementBuilder, ReferenceBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>()
// defines the custom element class
class MyCustomElement extends HTMLElement {
    // define the reference
    @ReferenceBuilder.reference({isShadow: true, selector: 'ul'})
    readonly ul: HTMLUListElement;
}

Example

The registered custom element counts the number of selected li and displays it.

See the Pen </ceb> ~ ReferenceBuilder by Thibault Morin (@tmorin) on CodePen.

AttributeDelegateBuilder

The class AttributeDelegateBuilder provides services to delegate the mutations of an attribute to targets (i.e. a child nodes).

The static method AttributeDelegateBuilder.get(attributeBuilder) returns a fresh builder. The builder expects the instance of an AttributeBuilder.

import {AttributeDelegateBuilder, AttributeBuilder} from '@tmorin/ceb'
// creates the builder
const builder = AttributeDelegateBuilder.get(AttributeBuilder.get('an-attribute'))

Alternatively, DelegateBuilder.attribute(attributeBuilder) can also be used.

import {DelegateBuilder, AttributeBuilder} from '@tmorin/ceb'
// creates the builder
const builder = DelegateBuilder.attribute(AttributeBuilder.get('an-attribute'))

The builder and underlying decorators are also technically documented: AttributeDelegateBuilder.

Set the selector

The method AttributeDelegateBuilder#to(selector) has to be used to define the selector. The selector is mandatory otherwise the builder won't be able to identify the targets.

import {AttributeDelegateBuilder, AttributeBuilder} from '@tmorin/ceb'
// delegate the accesses to the attribute 'an-attribute'
const builder = AttributeDelegateBuilder.get(AttributeBuilder.get('an-attribute'))
    .to('button');

Shadow DOM

By default, the builder selects targets relative to the light DOM.

The method AttributeDelegateBuilder#shadow() can be used to select targets relative to the shadow DOM.

import {AttributeDelegateBuilder, AttributeBuilder} from '@tmorin/ceb'
// delegate the accesses to the attribute 'an-attribute'
const builder = AttributeDelegateBuilder.get(AttributeBuilder.get('an-attribute'))
    .to('button')
    .shadow();

Bind to another attribute

By default, the builder mutates the same targets' attribute.

The method AttributeDelegateBuilder#attribute(toAttrName) can be used to force another attribute name.

import {AttributeDelegateBuilder, AttributeBuilder} from '@tmorin/ceb'
// delegate the accesses to the attribute 'an-attribute'
const builder = AttributeDelegateBuilder.get(AttributeBuilder.get('an-attribute'))
    .to('button')
    .attribute('another-attribute');

Bind to a property

The method AttributeDelegateBuilder#property(toPropName) can be used to force the mutation of a property.

import {AttributeDelegateBuilder, AttributeBuilder} from '@tmorin/ceb'
// delegate the accesses to the attribute 'an-attribute'
const builder = AttributeDelegateBuilder.get(AttributeBuilder.get('an-attribute'))
    .to('button')
    .property('aProperty');

The decorator

Attribute delegations can also be defined using a decorator.

import {ElementBuilder, AttributeDelegateBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>()
// define an attribute delegation
@AttributeDelegateBuilder.delegate('value', 'input')
// defines the custom element class
class MyCustomElement extends HTMLElement {
}

Example

The registered custom element is composed of an input and a button button. The boolean attribute disabled is delegated to both input and button. The attribute placeolder is delegated to the input. The attribute label is delegated to the textContent property of the button.

See the Pen </ceb> ~ AttributeDelegateBuilder by Thibault Morin (@tmorin) on CodePen.

PropertyDelegateBuilder

The class PropertyDelegateBuilder provides services to delegate the accesses of a property to a single target (i.e. a child node).

The static method PropertyDelegateBuilder.get(propName) returns a fresh builder. The builder expects the name of a property.

import {PropertyDelegateBuilder} from '@tmorin/ceb'
// creates the builder
const builder = PropertyDelegateBuilder.get('aProperty')

Alternatively, DelegateBuilder.property(propName) can also be used.

import {DelegateBuilder} from '@tmorin/ceb'
// creates the builder
const builder = DelegateBuilder.property('aProperty')

The builder and underlying decorators are also technically documented: PropertyDelegateBuilder.

Set the selector

The method PropertyDelegateBuilder#to(selector) has to be used to define the selector. The selector is mandatory otherwise the builder won't be able to identify the targets.

import {PropertyDelegateBuilder} from '@tmorin/ceb'
// delegate the accesses to the property 'aProperty'
const builder = PropertyDelegateBuilder.get('aProperty')
    .to('button');

Shadow DOM

By default, the builder selects targets relative to the light DOM.

The method PropertyDelegateBuilder#shadow() can be used to select targets relative to the shadow DOM.

import {PropertyDelegateBuilder} from '@tmorin/ceb'
// delegate the accesses to the property 'aProperty'
const builder = PropertyDelegateBuilder.get('aProperty')
    .to('button')
    .shadow();

Bind to another property

By default, the builder mutates the same targets' property.

The method PropertyDelegateBuilder#property(toPropName) can be used to force another property name.

import {PropertyDelegateBuilder} from '@tmorin/ceb'
// delegate the accesses to the property 'aProperty'
const builder = PropertyDelegateBuilder.get('aProperty')
    .to('button')
    .property('anotherProperty');

Bind to an attribute

The method PropertyDelegateBuilder#attribute(toAttrName) can be used to force the mutation of a attribute.

import {PropertyDelegateBuilder} from '@tmorin/ceb'
// delegate the accesses to the property 'aProperty'
const builder = PropertyDelegateBuilder.get('aProperty')
    .to('button')
    .attribute('an-attribute');

The option PropertyDelegateBuilder#boolean() can be used if the attribute is a boolean.

import {PropertyDelegateBuilder} from '@tmorin/ceb'
// delegate the accesses to the property 'aProperty'
const builder = PropertyDelegateBuilder.get('aProperty')
    .to('button')
    .attribute('a-boolean-attribute')
    .boolean();

The decorator

Property delegations can also be defined using a decorator.

import {ElementBuilder, PropertyDelegateBuilder} from '@tmorin/ceb'
// register the custom element
@ElementBuilder.element<MyCustomElement>()
// defines the custom element class
class MyCustomElement extends HTMLElement {
    // define an attribute delegation
    @PropertyDelegateBuilder.delegate('input')
    aProperty = 'a value'
}

Example

The registered custom element is composed of an input. The boolean property disabled is delegated to both input. The property placeholder is delegated to the placeholder attribute of the input.

See the Pen </ceb> ~ PropertyDelegateBuilder by Thibault Morin (@tmorin) on CodePen.