Wesib: Web Components Building Blocks

[NPM]npm-url [Build Status]build-status-link [Code Quality]quality-link [Coverage]coverage-link [GitHub Project]github-url [API Documentation]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.

Популярные проекты
Web components building blocks
TypeScript Обновлен 1 год назад
Wesib: Generic Components
TypeScript Обновлен 1 год назад
Wesib: Navigation support
Markdown Обновлен 1 год назад
Wesib: Forms
TypeScript Обновлен 1 год назад
https://github.com/wesib/css
Markdown Обновлен 1 год назад
https://github.com/wesib/examples.git
TypeScript Обновлен 1 год назад