<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:
- AttributeBuilder: to define attributes and react on changes
- FieldBuilder: to define fields (property/attribute) and react on changes
- OnBuilder: to listen to DOM events
- TemplateBuilder: to initialize the light or shadow DOM
- ReferenceBuilder: to get reference of children nodes
- AttributeDelegateBuilder: to delegate attribute mutations to child nodes
- PropertyDelegateBuilder: to delegate the property accesses to a single child node
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.