Wesib: Web Components Building Blocks
[]npm-url []build-status-link []quality-link []coverage-link []github-url []api-docs-url
Wesib is a base for web components definition.
It provides a way to define custom elements. But instead of extending HTMLElement
, it supports arbitrary component classes, and defines custom elements for them programmatically.
Wesib provides an IoC container, a component definition and lifecycle callbacks, and an infrastructure for opt-in features that can involve in component definition process and thus alter the resulting components in very flexible way.
This package provides a core API.
The @wesib/generic package provides generic web components and features.
The examples can be found in @wesib/examples.
Components
Wesib allows defining custom element by decorating a component class with @Component
decorator:
import { Component } from '@wesib/wesib';
@Component('my-component') // Custom element name
export class MyComponent {
// ...component definition
}
No need to extend HTMLElement
or any other class. Instead, Wesib creates a custom element accordingly to its definition built either programmatically or using component decorators.
To register custom component(s) call bootstrapComponents()
function like this:
import { bootstrapComponents } from '@wesib/wesib';
bootstrapComponents(MyComponent);
After that the custom element can be used anywhere in the document:
<my-component></my-component>
The component instance created along with a custom element and bound to it. All the logic of custom element delegated to the bound component instance.
Element Attributes
To define custom element attributes use @Attribute
or @AttributeChanged
component property decorators, or @Attributes
component class decorator.
import { Attribute, AttributeChanged, Attributes, Component } from '@wesib/wesib';
@Component('my-component') // Custom element name
@Attributes('attribute-one', 'another-attribute')
export class MyComponent {
@Attribute('attribute-two') // Attribute name. When omitted the property name is used
attribute2!: string | null; // Attribute value is accessed instead.
@AttributeChanged('attribute-three') // Attribute name. When omitted the method name is used
setAttribute3(newValue: string, oldValue: string | null) {
// This is called on attribute value modification with new and old values
}
}
<my-component
attribute-one="1" <!-- Can be accessed with element's `element.getAttribute("attribute-one")` -->
attribute-two="2" <!-- Can be accessed as `attribute2` property of `MyComponent` -->
attribute-three"3" <!-- Triggers `setAttribute3()` method call -->
></my-component>
Element Properties
To define the properties of custom element use a @DomProperty
component property decorator.
import { Component, DomProperty } from '@wesib/wesib';
@Component('my-component') // Custom element name
export class MyComponent {
@DomProperty('elementProperty') // Element property name. The decorated property name is used if omitted.
customProperty = 12; // Element's `elementProperty` is backed by this one.
}
The same can be done for element methods with @DomMethod
decorator, which is just a convenient alias for @DomProperty
.
IoC Container
Wesib provides contexts for each component and feature (see below). This context can be used to access provided values.
For example, each component class constructor accepts a ComponentContext
instance as its only argument.
import { Component, ComponentContext } from '@wesib/wesib';
@Component('my-component') // Custom element name
export class MyComponent {
private readonly _service: MyService;
constructor(context: ComponentContext) {
this._service = context.get(MyService); // Obtain a `MyService` instance provided by some feature elsewhere.
}
}
IoC container implementation is based on @proc7ts/context-values.
Features
Apart from custom elements definition and IoC container, everything in Wesib is an opt-in feature.
It is possible to define custom features to extend Wesib. E.g. to define or augment existing components, extend custom elements (like @Attribute
or @DomProperty
decorators do), or provide some context values.
The feature is a class decorated with @Feature
decorator:
import { cxBuildAsset } from '@proc7ts/context-builder';
import { ComponentContext, DefinitionContext, Feature, FeatureContext, FeatureSetup } from '@wesib/wesib';
@Feature({
needs: [
OtherFeature1, // Requires other features to be enabled.
MyComponent, // The required component will be defined too.
],
setup(setup: FeatureSetup) {
setup.provide(
cxBuildAsset(
GlobalService, // Provide a `GlobalService` available globally
() => new GlobalService(), // in all IoC contexts
),
);
setup.perDefinition(
cxBuildAsset(DefinitionService, ({ context: definitionContext }) => {
// Provide a `DefinitionService` available during component definition.
// Such service will be provided per component class
// and will be available during custom element construction,
// e.g. to `onDefinition()` listeners.
return new DefinitionService(definitionContext);
}),
);
setup.perComponent(
cxBuildAsset(MyService, ({ context: componentContext }) => {
// Provide a `MyService` available to components.
// Such service will be provided per component instance
// and will be available to component instance and `onComponent()` listeners.
return new MyService(componentContext.component);
}),
);
},
init(context: FeatureContext) {
// Bootstrap the feature by calling methods of provided context.
context.onDefinition((definitionContext: DefinitionContext) => {
// Notified on each component definition.
// The service provided with `perDefinition()` method above is available here
const definitionService = definitionContext.get(DefinitionService);
definitionContext.whenReady(() => {
// This is called when element class is defined.
console.log(
`Define element class ${definitionContext.elementType.name}` +
` for component of ${definitionContext.componentType.name} type`,
);
});
});
context.onComponent((componentContext: ComponentContext) => {
// Notified on each component instantiation.
// The service provided with `perComponent()` method above is available here
const myService = componentContext.get(MyService);
componentContext.whenReady(() => {
// This is called when component is instantiated,
// which happens right after custom element instantiation.
console.log(componentContext.element, ` is instantiated for`, componentContext.component);
});
});
},
})
export class MyFeature {}
To enable a custom feature just pass it to bootstrapComponents()
like this:
import { bootstrapComponents } from '@wesib/wesib';
bootstrapComponents(MyFeature);
Note that components are kind of features that, when passed to this function (or enabled with needs
option), register themselves as components.
Component State
Whenever a component state changes, e.g. when element attribute or property value changes, a state update notification issued.
A state update notification can also be issued by calling a ComponentContext.updateState()
method:
import { Component, ComponentContext } from '@wesib/wesib';
@Component('my-component') // Custom element name
export class MyComponent {
data: any;
constructor(private readonly _context: ComponentContext) {}
async loadData() {
const newData = await fetch('/api/data').then(response => response.json());
const oldData = this.data;
this.data = newData;
this._context.updateState('data', newData, oldData); // Update the state
}
}
A ComponentState
instance available in component context allows to track the component state updates.
Shadow DOM
It is possible to attach shadow root to custom element by decorating the component with @AttachShadow
decorator.
If shadow DOM is supported, then a shadow root will be attached to element. Otherwise, an element itself will be used as shadow root. In both cases the shadow root will be available in component context under [ShadowContentRoot]
key.
A ComponentContext.contentRoot
property is always available. It either contains a shadow root, or element itself. This is a root DOM node component element contents.
Rendering
Wesib core does not provide any mechanics for component rendering. It is completely up to the developer which rendering mechanics to use: direct DOM manipulations, template processing, virtual DOM, etc.
However, Wesib is able to notify the renderer on component state updates and trigger its rendering. For that a @Render
decorator can be applied to component renderer method:
import { Attribute, Component, ComponentContext, Render } from '@wesib/wesib';
@Component('greet-text')
export class GreetTextComponent {
@Attribute()
name: string | null;
constructor(private readonly _context: ComponentContext) {}
@Render()
render() {
this._context.contentRoot.innerText = `Hello, ${this.name}!`;
}
}
The @Render
-decorated method will be called from requestAnimationFrame()
callback by default. So, it won’t be called too frequently.