2.0.2 • Published 1 year ago

@wesib/wesib v2.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Wesib: Web Components Building Blocks

NPM Build Status Code Quality Coverage GitHub Project API Documentation

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.

2.0.2

1 year ago

2.0.1

3 years ago

2.0.0

3 years ago

2.0.0-dev.0

3 years ago

1.0.0-beta.44

3 years ago

1.0.0

3 years ago

1.0.0-beta.42

3 years ago

1.0.0-beta.43

3 years ago

1.0.0-beta.40

3 years ago

1.0.0-beta.41

3 years ago

1.0.0-beta.39

3 years ago

1.0.0-beta.38

3 years ago

1.0.0-beta.36

3 years ago

1.0.0-beta.33

3 years ago

1.0.0-beta.34

3 years ago

1.0.0-beta.31

3 years ago

1.0.0-beta.32

3 years ago

1.0.0-beta.30

3 years ago

1.0.0-beta.29

3 years ago

1.0.0-beta.28

3 years ago

1.0.0-beta.27

3 years ago

1.0.0-beta.26

3 years ago

1.0.0-beta.25

3 years ago

1.0.0-beta.24

3 years ago

1.0.0-beta.23

3 years ago

1.0.0-beta.22

3 years ago

1.0.0-beta.21

3 years ago

1.0.0-beta.20

3 years ago

1.0.0-beta.19

4 years ago

1.0.0-beta.18

4 years ago

1.0.0-beta.17

4 years ago

1.0.0-beta.16

4 years ago

1.0.0-beta.15

4 years ago

1.0.0-beta.13

4 years ago

1.0.0-beta.14

4 years ago

1.0.0-beta.12

4 years ago

1.0.0-beta.11

4 years ago

1.0.0-beta.10

4 years ago

1.0.0-beta.9

4 years ago

1.0.0-beta.8

4 years ago

1.0.0-beta.7

4 years ago

1.0.0-beta.6

4 years ago

1.0.0-beta.5

4 years ago

1.0.0-beta.4

4 years ago

1.0.0-beta.3

4 years ago

1.0.0-beta.2

4 years ago

1.0.0-beta.1

4 years ago

1.0.0-beta.0

4 years ago

1.0.0-alpna.100

4 years ago

1.0.0-alpha.100

4 years ago

1.0.0-alpha.99

4 years ago

1.0.0-alpha.98

4 years ago

1.0.0-alpha.97

4 years ago

1.0.0-alpha.96

4 years ago

1.0.0-alpha.95

4 years ago

1.0.0-alpha.94

4 years ago

1.0.0-alpha.93

4 years ago

1.0.0-alpha.92

4 years ago

1.0.0-alpha.91

4 years ago

1.0.0-alpha.90

4 years ago

1.0.0-alpha.89

4 years ago

1.0.0-alpha.88

4 years ago

1.0.0-alpha.87

4 years ago

1.0.0-alpha.86

4 years ago

1.0.0-alpha.85

4 years ago

1.0.0-alpha.83

4 years ago

1.0.0-alpha.79

4 years ago

1.0.0-alpha.80

4 years ago

1.0.0-alpha.78

4 years ago

1.0.0-alpha.75

4 years ago

1.0.0-alpha.74

4 years ago

1.0.0-alpha.73

4 years ago

1.0.0-alpha.71

4 years ago

1.0.0-alpha.69

4 years ago

1.0.0-alpha.70

4 years ago

1.0.0-alpha.68

4 years ago

1.0.0-alpha.67

4 years ago

1.0.0-alpha.66

4 years ago

1.0.0-alpha.65

4 years ago

1.0.0-alpha.62

4 years ago

1.0.0-alpha.61

4 years ago

1.0.0-alpha.59

4 years ago

1.0.0-alpha.58

4 years ago

1.0.0-alpha.57

4 years ago

1.0.0-alpha.56

4 years ago

1.0.0-alpha.55

4 years ago

1.0.0-alpha.54

4 years ago

1.0.0-alpha.53

4 years ago

1.0.0-alpha.52

4 years ago

1.0.0-alpha.47

4 years ago

1.0.0-alpha.44

4 years ago

1.0.0-alpha.42

4 years ago

1.0.0-alpha.41

4 years ago

1.0.0-alpha.40

4 years ago

1.0.0-alpha.39

5 years ago

1.0.0-alpha.38

5 years ago

1.0.0-alpha.37

5 years ago

1.0.0-alpha.36

5 years ago

1.0.0-alpha.35

5 years ago

1.0.0-alpha.34

5 years ago

1.0.0-alpha.33

5 years ago

1.0.0-alpha.32

5 years ago

1.0.0-alpha.31

5 years ago

1.0.0-alpha.30

5 years ago

1.0.0-alpha.29

5 years ago

1.0.0-alpha.27

5 years ago

1.0.0-alpha.25

5 years ago

1.0.0-alpha.24

5 years ago

1.0.0-alpha.21

5 years ago

1.0.0-alpha.20

5 years ago

1.0.0-alpha.19

5 years ago

1.0.0-alpha.18

5 years ago

1.0.0-alpha.17

5 years ago

1.0.0-alpha.16

5 years ago

1.0.0-alpha.15

5 years ago

1.0.0-alpha.14

5 years ago

1.0.0-alpha.13

5 years ago

1.0.0-alpha.12

5 years ago

1.0.0-alpha.11

5 years ago

1.0.0-alpha.10

5 years ago

1.0.0-alpha.9

5 years ago

1.0.0-alpha.8

5 years ago

1.0.0-alpha.7

5 years ago

1.0.0-alpha.6

5 years ago

1.0.0-alpha.5

5 years ago

1.0.0-alpha.4

5 years ago

1.0.0-alpha.3

5 years ago

1.0.0-alpha.2

5 years ago

1.0.0-alpha.1

6 years ago

1.0.0-alpha.0

6 years ago