<ceb/> ~ Custom Element Builder

<ceb/> was initially a library dedicated for the authoring of Custom Elements (v0) then Custom Elements (v1). However, the library is now providing building blocks going beyond the topic of composable UI elements. It's about fundamental design principles, messaging, functional rendering and obviously composition of UI elements ;).

<ceb/> is released under the MIT license.

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

Packages

The library is composed of many packages published on npmjs.com. All packages are compliant CommonJs and ES Module. That means, they can be directly used in almost all JavaScript runtimes.

Inversion Of Control:

Event/Message Architecture:

Custom Element Authoring:

Templating:

Support:

Universal Module Definition

The bundle package @tmorin/ceb-bundle-web is also available as a UMD module from cdn.skypack.dev.

<!-- the optimized Universal Module Definition -->
<script src="https://cdn.skypack.dev/@tmorin/ceb-bundle-web/dist/umd/ceb.min.js"></script>
<!-- the not optimized Universal Module Definition -->
<script src="https://cdn.skypack.dev/@tmorin/ceb-bundle-web/dist/umd/ceb-bundle-web.js"></script>

Inversion

<ceb/> provides a built-in implementation of the Inversion Of Control principle. Its purpose is to emphasize loose coupling between programing artifacts like classes, modules ... But also to improve the modularity and extensibility of the overall software architecture like the Hexagonal Architecture.

Hexagonal Architecture

Despite both following facts, the IoC container can be applied to other architecture styles and the Hexagonal Architecture can be implemented without IoC, at the end, IoC and Hexagonal Architecture just work well together :).

This architecture helps to refine a model to separate the functional to the technical. The technical concern is pushed to the boundary of the model within contracts (i.e. interfaces), identified as ports. The contacts are implemented within the infrastructure by adapters

Port and adapter

Obviously, a port can be satisfied by many adapters. Therefore, it becomes easy to configure a setup by execution contexts: development, integration testing, functional testing, production ...

One port, many adapters

The relationship between two models can be handled with a regular Facade.

The model to model relationship

However, the relationship can be handled using the port/adapter approach. The introduction of a component which expose only the API of a model leads to definition of contextual adapters. For instance, the component API can be satisfied by an adapter dedicated for testing purpose which is just a mock of the core implementation.

Modularity of the model's Facade

IoC implementation

The IOC implementation is bundled in the NPM package @tmorin/ceb-inversion-core.

This IoC implementation relies on three main concepts. The first one is the registry. Its purpose is to provide services to register and resolve items. The second concept, the module, interacts with the registry in order to configure items to register. Finally, the last one, the container, manages the lifecycle of the modules and, therefore, the lifecycle of the registry's items too.

The Container, the Registry and the Modules

The Container

The Container lifecycle

A Container is created using the ContainerBuilder, and especially its build() method. Once built, the Container can be initialized, so that Modules and Components will be configured. When a Container becomes useless, it has to be disposed, so that Modules and Components will be disposed too. Once disposed, a container cannot be used anymore.

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"

ContainerBuilder.get()
.build() // from `void` to `Built`
.initialize() // from `Built` to `Alive`
.then((container) => {
  return container.dispose() // from `Alive` to `Disposed`
})

The Registry

The Registry handles only two kinds of registration modes: by value and by factory.

With the by value mode, the value must exist at the time of the registration, and the registry will always resolve it.

import { DefaultRegistry } from "@tmorin/ceb-inversion-core"

// create a fresh registry
const registry = new DefaultRegistry()

// register a Value
registry.registerValue("key", "the value")

// resolve the entry
const a = registry.resolve("key")

// resolve again the entry
const b = registry.resolve("key")

// assert instances are the same
console.assert(a === b)

However, with the by factory mode, the value is created at the resolution time. Moreover, its creation can be based on entries available in the registry. By default, the factory is invoked for each resolution.

import { DefaultRegistry } from "@tmorin/ceb-inversion-core"

// create a fresh registry
const registry = new DefaultRegistry()

// register a Factory
registry.registerFactory("key", () => ({ k: "v" }))

// resolve the entry
const a = registry.resolve("key")

// resolve again the entry
const b = registry.resolve("key")

// assert instances are not the same
console.assert(a !== b)

The option singleton prevents this invocation of the factory at each resolution. That means the value is created only once, at its first resolution.

import { DefaultRegistry } from "@tmorin/ceb-inversion-core"

// create a fresh registry
const registry = new DefaultRegistry()

// register a Factory acting as a singleton
registry.registerFactory("key", () => ({ k: "v" }), { singleton: true })

// resolve the entry
const a = registry.resolve("key")

// resolve again the entry
const b = registry.resolve("key")

// assert instances are the same
console.assert(a === b)

The Modules

The modules hierarchy

A Module helps to bundle entry registrations to the registry. It's an implementation of the interface Module. However, a convenient abstracted implementation, AbstractModule, handles already the boilerplate stuff.

import { AbstractModule } from "@tmorin/ceb-inversion-core"

export class WorldModule extends AbstractModule {
async configure(): Promise<void> {
  // register a name
  this.registry.registerValue("name", "World")
}
}

export class GreetingModule extends AbstractModule {
async configure(): Promise<void> {
  // register a factory
  this.registry.registerFactory(
    "greeting",
    (registry) => `Hello, ${registry.resolve("name")}!`
  )
}
}

The modules are used during the creation of a container and must be provided to the ContainerBuilder.

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"
import { GreetingModule, WorldModule } from "./ioc-container-module-class"

ContainerBuilder.get()
.module(new GreetingModule())
.module(new WorldModule())
.build()
.initialize()
.then((container) => {
  // resolve the greeting entry
  const greeting = container.registry.resolve("greeting")
  // assert the greeting text match the expected one
  console.assert(greeting === "Hello, World!")
  // release the stateful stuff
  return container.dispose()
})
.catch((e) => console.error(e))

Sometime, the class usage can be a bit too much. Therefore, an inline way to available using OnlyConfigureModule.create(...).

import { ContainerBuilder, ModuleBuilder } from "@tmorin/ceb-inversion-core"

ContainerBuilder.get()
.module(
  ModuleBuilder.get()
    .configure(function (registry) {
      // register a name
      registry.registerValue("name", "John Doe")
      // register a factory
      registry.registerFactory(
        "greeting",
        (registry) => `Hello, ${registry.resolve("name")}!`
      )
    })
    .build()
)
.build()
.initialize()
.then((container) => {
  // resolve the greeting entry
  const greeting = container.registry.resolve("greeting")
  // assert the greeting text match the expected one
  console.assert(greeting === "Hello, John Doe!")
  // release the stateful stuff
  return container.dispose()
})
.catch((e) => console.error(e))

The Components

The Component

Components follow the lifecycle of Modules, i.e. configure then dispose. To be discovered and managed by the Container, they must be registered with the ComponentSymbol Registry Key.

Components are the right places to implement Process Manager or other reactive programing artifacts.

import { Component } from "@tmorin/ceb-inversion-core"

// define a component
export class MyModule extends Component {
async configure(): Promise<void> {
  // execute things when container starts
}

async dispose(): Promise<void> {
  // execute things when container stops
}
}

Hexagonal testing

The testing model is bundled in the NPM package @tmorin/ceb-inversion-testing-core.

The isolation, provided by the Hexagonal Architecture, makes the testing of models easier. However, it leads to an annoying side effect: some tests, especially the functional ones, are duplicated among the adapters.

<ceb/> provides a testing library to prevent test duplication. The purpose is to define tests in the model which have to be executed in the adapters. So that, an adapter can validate the port implementation satisfy the model. The library leverages on the IoC container to let the adapter binds its implementation to the port.

The testing model

From the model point of view, i.e. the supplier, the suites are created using the TestSuiteBuilder and the scenarios with the TestScenarioBuilder.

import { assert } from "chai"
import { ModuleBuilder } from "@tmorin/ceb-inversion-core"
import {
  TestScenarioBuilder,
  TestSuiteBuilder,
} from "@tmorin/ceb-inversion-testing-core"

export const SuiteA = TestSuiteBuilder.get("ToEmphasize Port")
  .scenario(
    TestScenarioBuilder.get("Greeting target is emphasized")
      .configure((containerBuilder) => {
        containerBuilder.module(
          ModuleBuilder.get()
            .configure(function (registry) {
              registry.registerFactory("Greeting", (registry) => {
                // expect an adapter of the port "ToEmphasize"
                const toEmphasize =
                  registry.resolve<(value: string) => string>("ToEmphasize")
                // use the adapter to emphasize the target name
                return `Hello, ${toEmphasize("john doe")}`
              })
            })
            .build()
        )
      })
      .execute((container) => {
        // resolve the model artifact
        const text = container.registry.resolve<string>("Greeting")
        // validate the model artifact works as expected with the tested adapter
        assert.equal(text, "Hello, JOHN DOE!")
      })
      .build()
  )
  .build()

Mocha Implementation

The Mocha implementation of the testing model is bundled in the NPM package @tmorin/ceb-inversion-testing-mocha.

An implementation of the testing model is provided for Mocha.

From the provider point of view, the suites are executed using the MochaTestSuiteExecutorBuilder.

import { MochaTestSuiteExecutorBuilder } from "@tmorin/ceb-inversion-testing-mocha"
import { SuiteA } from "./hexagonal_testing-suite"
import { ModuleBuilder } from "@tmorin/ceb-inversion-core"

describe("ToEmphasize Adapter", function () {
  MochaTestSuiteExecutorBuilder.get(SuiteA)
    .configure((containerBuilder) => {
      containerBuilder.module(
        ModuleBuilder.get()
          .configure((registry) => {
            registry.registerValue("ToEmphasize", (value: string) =>
              value.toUpperCase()
            )
          })
          .build()
      )
    })
    .build()
    .execute()
})

Jest Implementation

The Jest implementation of the testing model is bundled in the NPM package @tmorin/ceb-inversion-testing-jest.

An implementation of the testing model is provided for Jest.

From the provider point of view, the suites are executed using the JestTestSuiteExecutorBuilder.

import { JestTestSuiteExecutorBuilder } from "@tmorin/ceb-inversion-testing-jest"
import { OnlyConfigureModule } from "@tmorin/ceb-inversion-core"
import { SuiteA } from "./hexagonal_testing-suite"

describe("ToEmphasize Adapter", function () {
  JestTestSuiteExecutorBuilder.get(SuiteA)
    .configure((containerBuilder) => {
      containerBuilder.module(
        OnlyConfigureModule.create(async function () {
          this.registry.registerValue("ToEmphasize", (value: string) =>
            value.toUpperCase()
          )
        })
      )
    })
    .build()
    .execute()
})

Messaging

<ceb/> provides a built-in solution for the implementation of the application logic which relies on an Event/Message Architecture.

The original purpose is to strictly separate the interaction logic (i.e. the Custom Elements) and the application logic. The communication between both worlds is handled by an asynchronous API relying on messages transiting within buses. A message is a simple data structure expressing an action to do, its result or a notification about one of its side effect. A bus is a component providing a set of commands to emit and receive messages.

The core of the solution is composed of a model and a set of building blocks implementing an Event/Message Architecture. The reference implementation is a vanilla TypeScript/JavaScript flavor working in almost all JavaScript context: Browser, Node, Electron ... Additional packages provide support for side concerns like integration, testing ...

Concepts

All Event/Message Architectures rely on common concepts.

Messages

A message is a structured piece of information which contains a kind, a set of headers and a body.

<ceb/> recognizes three main kinds of messages:

  • A Command expresses the intent of the producer to change the state of the system.
  • A Query expresses the intent of the producer to know the state of the system.
  • An Event expresses that something has happened in the past.

An additional one, the Result, is used to provide a feedback to the producer within a Request-Reply scenario.

The hierarchy of the messages

Message Buses

A Message Bus is a component able to handle the sending and reception of messages.

The described messages are transmitted in buses from their producers to eventual receivers.

<ceb/> recognizes three kind of buses:

  • The Command Bus dedicated for the Command messages.
  • The Query Bus dedicated for the Query messages.
  • The Event Bus dedicated for the Event messages.

The Command Bus and Query Bus embraces the Point-to-Point Channel characteristics. So that, Request-Reply scenario can be implemented, especially for the Query Bus channel where the purpose is get a Result from the query handler.

On the other hand, the Event Bus relies on the Publish-Subscribe Channel characteristics.

The command bus

The query bus

The event bus

Gateway

From a user point of view, a gateway is an entry point to produce and consume messages. It exposes the three message buses: the Command Bus, the Query Bus and the Event Bus. Additionally, an Observer is also available to track some messaging side cases like error handling.

The gateway

Command And Query Separation

The CQS design pattern emphases a strict separation of the APIs about read and write operations. At the code level, its implementation is the expressiveness of dedicated programming artifacts for each concern: wrote operations vs read operations.

The base at the code level

On an upper level, at the Bounded Context one, the CQS design pattern can be implemented leveraging on messaging systems.

  1. The Bounded Context's Facade creates and dispatches Commands and Queries to the Gateway.
  2. Then, the Gateway delegates to the right handler the execution of the action.
  3. Finally, the implementations of the handlers leverage on the model artifacts (Entity, Service ...) to fulfill the requests.

The base at the bounded context level

The logic of the command handler produces an output which is composed of two items:

  • a result which will be sent back to the requester
  • as well as an additional set of events which will be published on the event bus

The command handling

On the other side, the logic of the query handler produces a result which will be sent back to the requester

The query handling

Command And Query Responsibility Segregation

When CQS is applied at the software architecture level, an adaptation of the command handling and query handling can be done to strictly segregate the model for the commands and the model for the queries. There are many strategies to feed the query model based on the operations made by the command one. One of them is to leverage on the events published by the command handling part. The ways to feed the query model by events depends on the nature of the application. Some can just work with transient events, whereas others may require a more robust approach based on queues or streams of events potentially persisted.

From CQS to CQRS at the bounded context level

Additionally, the usage of Events can lead to a model driven by the Event Sourcing approach where the produced Event are not just a side effect of the command handling but also its source of truth.

Location Transparency

Location Transparency is the ability to share or get resources without the knowledge of their localizations.

Because of the introduction of the messages and their underlying buses provided by the Gateway, the communication between Bounded Contexts is natively transparent. Therefore, only infrastructural components will have to be adapted to move a Bound Context from its original Monolith to a dedicated side Microlith.

The original Monolith

The introduction of a Microlith

Additionally, scaling of Bounded Contexts can be done leveraging only on infrastructural components.

The scaling of a Bounded Context

Messages

The definition of the messages is part of the NPM package @tmorin/ceb-messaging-core.

Anatomy of a message

A message is a data structure composed of three properties: kind, headers and body.

PropertyDescription
kindProvide the intention of the message producer.
headersProvide information to the messaging system to handle the message. The headers are a set of arbitrary key/value entries. Two entries are expected for each message: the messageType and the messageId.
bodyAn arbitrary set of information expected by the receiver.

The Message

The commands

A Command is a data structure matching the Message one.

PropertyDescription
kindThe value is always command.
headers.messageTypeHelp the messaging system to route the message to the right command handler.

The Command

Commands are handled by Command Handlers. By convention, a command type should only be processed by one Command Handler implementation and process only once. The main purpose of a command handler is to interact with model artifacts to mutate the state of a system. The underlying side effects can be described by a Result and/or a set of Events.

The queries

A Query is a data structure matching the Message one.

PropertyDescription
kindThe value is always query.
headers.messageTypeHelp the messaging system to route the message to the right query handler.

The Query

Queries are handled by Query Handlers. By convention, a query type can be processed by different Command Handlers implementations and process more than once. The main purpose of a query handler is to interact with model artifacts to gather data and build a view of the system's state. The view is finally sent back to the requester within a Result.

The results

A Result is a data structure matching the Message one.

PropertyDescription
kindThe value is always result.
headers.messageTypeHelp requester to handle the result, i.e. success vs failure.
headers.originalMessageIdIt's the messageId of the related command or query. It helps the requester to distinguish to which command or query a result belongs to.

The Result

The events

An Event is a data structure matching the Message one.

PropertyDescription
kindThe value is always event.
headers.messageTypeHelp the messaging system to route the message to the right event listeners.

The Event

Events are listened by Event Listeners. By convention, an event type can be listened by many Event listeners. The main purpose of an event listener is to react on the event.

Message Construction

Messages are simple vanilla JavaScript objects. For instance to create a command Greeting, the following snippet is enough.

const greetingCmd = {
  kind: "command",
  headers: {
    messageType: "Greeting",
    messageId: "command-0"
  },
  body: "Hello, World!"
}

However, manual creation of messages is error-prone and not really DRY. Therefore, a builder is provided to, at least, build messages which are technically valid.

Creation of commands using the MessageBuilder:

import { MessageBuilder } from "@tmorin/ceb-messaging-core"

// create a command
const aCommandA = MessageBuilder.command("CommandA")
  // set a custom identifier
  .identifier("command-1")
  // add an header entry
  .headers({ k1: "v1" })
  // set a body
  .body("a body")
  // build the result
  .build()

Creation of queries using the MessageBuilder:

import { MessageBuilder } from "@tmorin/ceb-messaging-core"

// create a query
const aQueryA = MessageBuilder.query("QueryA")
  // set a custom identifier
  .identifier("query-1")
  // add an header entry
  .headers({ k1: "v1" })
  // set a body
  .body("a body")
  // build the result
  .build()

Creation of events using the MessageBuilder:

import { MessageBuilder } from "@tmorin/ceb-messaging-core"

// create an event
const anEventA = MessageBuilder.event("EventA")
  // set a custom identifier
  .identifier("event-1")
  // add an header entry
  .headers({ k1: "v1" })
  // set a body
  .body("a body")
  // build the result
  .build()

Creation of results using the MessageBuilder:

import { MessageBuilder } from "@tmorin/ceb-messaging-core"

// create a basic command
const commandA = MessageBuilder.command("CommandA").build()

// create a result
const resultA = MessageBuilder.result(commandA)
  // override the message type, by default it's `result`
  .type("custom-type")
  // set a custom identifier
  .identifier("result-1")
  // add an header entry
  .headers({ k1: "v1" })
  // set a body
  .body("a body")
  // build the result
  .build()

Gateway

The definition of the Gateway is part of the NPM package @tmorin/ceb-messaging-core.

The Gateway is a set of interfaces acting as an entry point to the messaging world. It can be used to send or handle messages but also to observe the implementations' behavior.

A Gateway provides accessors to the main buses: the CommandBus, the QueryBus and the EventBus. A remaining accessor provides an observable view point of the Gateway.

A Gateway should be ready to used once created. Its end-of-life is triggered with its method dispose(). Its purpose is to release all stateful stuff. So that, once invoked, the Gateway cannot be used anymore.

The Gateway

Commands

The CommandBus provides methods to interact with the Point-to-Point Channel which handles the commands. The CommandBus supports the one-way or two-way conversations.

On the receiver side, the handled commands are registered using the method handle(). The method expects the type of the command to handle as well as its handler. The handler is a callback function which will be invoked once a command is received. A handler may return an output which may be composed of an optional Result and/or an optional set of Events. If expected, the Result will be sent back to the sender. About the set of Events, they will be published on the EventBus. At any time, a handler can be unregistered using the Removable object returned by the method handler().

On the sender side, those requiring a one-way conversation must use the method executeAndForget(). Within this conversation style, the command sender doesn't expect a Result from the receiver side. So that, the receiver and server sides don't need to handle the Request-Reply pattern.

However, senders requiring a two-way conversation must use the method execute().

The CommandBus

Queries

The QueryBus provides methods to interact with the Point-to-Point Channel which handles the queries. The QueryBus supports only the two-way conversations.

On the receiver side, the handled queries are registered using the method handle(). At any time, a handler can be unregistered using the Removable object returned by the method handler().

On the sender side, the queries are sent to the bus and the replies received using the method execute().

The QueryBus

Events

The EventBus provides methods to interact with the Publish-Subscribe Channel which handles the events.

The publisher must use the method publish() to publish events. On the other side, the subscribers must use the method subscribe() to subscribe on published events. At any time, a subscription can be disposed using the Removable object returned by the method subscribe().

The EventBus

Observer

The Observer is a view side of the Control Bus. It provides an entry point to observe the main buses: the CommandBus, the QueryBus and the EventBus.

The GatewayObserver

Inversion

The definition of the Inversion module is part of the NPM package @tmorin/ceb-messaging-inversion.

Command and Query handlers as well as Event listeners can be discovered and managed by Inversion on the container initialization. The module MessagingModule takes care of the discovery, registration and also disposition of handlers and listeners at the container end of life.

Discoverable Command handlers must match the DiscoverableCommandHandler interface.

import {
  Command,
  Event,
  MessageBuilder,
  Result,
} from "@tmorin/ceb-messaging-core"
import { DiscoverableCommandHandler } from "@tmorin/ceb-messaging-inversion"

// create the handler using the "implementation" way
export class GreetSomebodyHandler
  implements
    DiscoverableCommandHandler<
      Command<string>,
      Result<string>,
      Array<Event<string>>
    >
{
  // the type of the Command to handle
  type = "GreetSomebody"

  // the handler
  handler(command: Command<string>) {
    // create the greeting text
    const result = MessageBuilder.result(command)
      .body(`Hello, ${command.body}!`)
      .build()
    // create the event
    const events = [
      MessageBuilder.event("SomeoneHasBeenGreeted").body(command.body).build(),
    ]
    // return the output
    return { result, events }
  }
}

Discoverable Query handlers must match the DiscoverableQueryHandler interface.

import { MessageBuilder, Query, Result } from "@tmorin/ceb-messaging-core"
import { DiscoverableQueryHandler } from "@tmorin/ceb-messaging-inversion"

// create the handler using the "implementation" way
export class WhatTimeIsItHandler
  implements DiscoverableQueryHandler<Query<void>, Result<string>>
{
  // the type of the Query to handle
  type = "WhatTimeIsIt"

  // the handler
  handler(query: Query<void>) {
    return MessageBuilder.result(query).body(new Date().toISOString()).build()
  }
}

Discoverable Event listeners must match the DiscoverableEventListener interface.

import { Event } from "@tmorin/ceb-messaging-core"
import { DiscoverableEventListener } from "@tmorin/ceb-messaging-inversion"

// create the listener using the "object" way
export const SOMEONE_HAS_BEEN_GREETED_LISTENER: DiscoverableEventListener<
  Event<string>
> = {
  // the type of the Event to handle
  type: "SomeoneHasBeenGreeted",
  // the handler
  listener: (event: Event<string>) => {
    console.info(`${event.body} has been greeted`)
  },
}

To be discoverable the handlers and listeners must be registered in the Container's Registry with the right Registry Key.

  • DiscoverableCommandHandlerSymbol for the Command handlers
  • DiscoverableQueryHandlerSymbol for the Query handlers
  • DiscoverableEventListenerSymbol for the Event listeners
import { AbstractModule } from "@tmorin/ceb-inversion-core"
import { GreetSomebodyHandler } from "./inversion-discovery-command"
import { WhatTimeIsItHandler } from "./inversion-discovery-query"
import { SOMEONE_HAS_BEEN_GREETED_LISTENER } from "./inversion-discovery-event"
import {
  DiscoverableCommandHandlerSymbol,
  DiscoverableEventListenerSymbol,
  DiscoverableQueryHandlerSymbol,
} from "@tmorin/ceb-messaging-inversion"

// define a "regular" Module
export class DiscoverableStuffModule extends AbstractModule {
  async configure() {
    // register the command handler
    this.registry.registerValue(
      DiscoverableCommandHandlerSymbol,
      new GreetSomebodyHandler()
    )
    // register the query handler
    this.registry.registerFactory(
      DiscoverableQueryHandlerSymbol,
      () => new WhatTimeIsItHandler()
    )
    // register the event listener
    this.registry.registerValue(
      DiscoverableEventListenerSymbol,
      SOMEONE_HAS_BEEN_GREETED_LISTENER
    )
  }
}

Finally, the module MessagingModule and those registering discoverable handlers and listeners must be registered as other modules.

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"
import {
  Gateway,
  GatewaySymbol,
  MessageBuilder,
} from "@tmorin/ceb-messaging-core"
import { DiscoverableStuffModule } from "./inversion-discovery-module"
import { MessagingModule } from "@tmorin/ceb-messaging-inversion"
import { SimpleModule } from "@tmorin/ceb-messaging-simple-inversion"

ContainerBuilder.get()
  // register the module which discovers the handlers and listeners
  .module(new MessagingModule())
  // register the module which provide a Gateway instance
  .module(new SimpleModule())
  // register the module which contain the discoverable handlers and listeners
  .module(new DiscoverableStuffModule())
  .build()
  .initialize()
  .then(async (container) => {
    // resolve the gateway
    const gateway = container.registry.resolve<Gateway>(GatewaySymbol)

    // register an event listener
    gateway.events.subscribe("SomeoneHasBeenGreeted", (event) => {
      console.info(`${event.body} has been greeted`)
    })

    // create and execute the GreetSomebody command
    const greetSomebody = MessageBuilder.command("GreetSomebody")
      .body("World")
      .build()
    const cmdResult = await gateway.commands.execute(greetSomebody)
    console.info(`the greeting text: ${cmdResult.body}`)

    // create and execute the WhatTimeIsIt query
    const whatTimeIsIt = MessageBuilder.query("WhatTimeIsIt")
      .body("World")
      .build()
    const qryResult = await gateway.queries.execute(whatTimeIsIt)
    console.info(`the time: ${qryResult.body}`)

    return container.dispose()
  })
  .catch((e) => console.error(e))

The reference implementation

The reference implementation is defined in the NPM package @tmorin/ceb-messaging-simple.

The reference implementation relies on an in-memory and single process approach. So that, the implementation is free of network or any other concerns related to distributed systems.

The SimpleGateway

A SimpleGateway instance can be got from the following three approaches: the global instance, the factory method or the constructor.

The global instance

A global instance of the SimpleGateway is available from the static field SimpleGateway.GOBAL. It's a lazy property, in fact the instance is only created once at its first get.

import { MessageBuilder } from "@tmorin/ceb-messaging-core"
import { SimpleGateway } from "@tmorin/ceb-messaging-simple"

// create an event and publish it using the global SimpleGateway instance
const event = MessageBuilder.event("EventA").build()
SimpleGateway.GLOBAL.events.publish(event)

The factory method

A SimpleGateway instance can be easily created using the factory method, i.e. the static method SimpleGateway.create(). The method returns a fresh new SimpleGateway instance at each call.

import { Gateway, MessageBuilder } from "@tmorin/ceb-messaging-core"
import { SimpleGateway } from "@tmorin/ceb-messaging-simple"

// create a SimpleGateway instance
const gateway: Gateway = SimpleGateway.create()

// create an event and publish it
const event = MessageBuilder.event("EventA").build()
gateway.events.publish(event)

// dispose the created SimpleGateway
gateway.dispose().catch((e) => console.error(e))

The constructor

The constructor approach provides a fine grain control of the Gateway dependencies: the CommandBus, the QueryBus, the EventBus and the GatewayObserver.

import { GatewayEmitter, MessageBuilder } from "@tmorin/ceb-messaging-core"
import {
  SimpleCommandBus,
  SimpleEventBus,
  SimpleGateway,
  SimpleQueryBus,
} from "@tmorin/ceb-messaging-simple"

// create the SimpleGateway dependencies
const emitter = new GatewayEmitter()
const events = new SimpleEventBus(emitter)
const commands = new SimpleCommandBus(events, emitter)
const queries = new SimpleQueryBus(emitter)

// create a SimpleGateway instance
const gateway = new SimpleGateway(events, commands, queries, emitter)

// create an event and publish it
const event = MessageBuilder.event("EventA").build()
gateway.events.publish(event)

// dispose the created SimpleGateway
gateway.dispose().catch((e) => console.error(e))

The Inversion Module

The package provides an Inversion Module which can be used to create (optionally) and register it on the registry.

Create a container with the default module behavior, i.e. the SimpleGateway will be created from scratch automatically:

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"
import {
  Gateway,
  GatewaySymbol,
  MessageBuilder,
} from "@tmorin/ceb-messaging-core"
import { SimpleModule } from "@tmorin/ceb-messaging-simple-inversion"

ContainerBuilder.get()
  // let the module created it-self the SimpleGateway instance
  .module(new SimpleModule())
  .build()
  .initialize()
  .then((container) => {
    // resolve the gateway
    const gateway = container.registry.resolve<Gateway>(GatewaySymbol)
    // publish a simple message
    gateway.events.publish(MessageBuilder.event("Hello").build())
  })
  .catch((e) => console.error(e))

Create a container with a provided SimpleGateway instance:

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"
import {
  Gateway,
  GatewaySymbol,
  MessageBuilder,
} from "@tmorin/ceb-messaging-core"
import { SimpleGateway } from "@tmorin/ceb-messaging-simple"
import { SimpleModule } from "@tmorin/ceb-messaging-simple-inversion"

ContainerBuilder.get()
  .module(
    new SimpleModule({
      // the provided instance, there the GLOBAL one
      gateway: SimpleGateway.GLOBAL,
    })
  )
  .build()
  .initialize()
  .then((container) => {
    // resolve the gateway
    const gateway = container.registry.resolve<Gateway>(GatewaySymbol)
    // publish a simple message
    gateway.events.publish(MessageBuilder.event("Hello").build())
  })
  .catch((e) => console.error(e))

The Moleculer implementation

The Moleculer implementation is defined in the NPM package @tmorin/ceb-messaging-moleculer.

The Moleculer implementation leverages on the features provided by the microservices framework.

Management of Commands and Queries

There is one Moleculer service per command or query types. For instance, the command type CommandA will be managed by the service CommandA.

About commands, each service provides two Moleculer actions: execute and executeAndForget. The first one executes the command handler and return the result. The second one just executes the command handler at the next clock thick. For instance, the command type CommandA can be executed within the Moleculer world with the actions CommandA.execute and CommandA.executeAndForget. Each action accepts only one parameter: the command.

About queries, each service provides only one action: execute. The action executes the query handler and return the result. For instance, the query type QueryA can be executed within the Moleculer world with the action QueryA.execute. The action accepts only one parameter: the query.

Management of Events

The Events are managed by a single Moleculer service: EventBus. Each time an Event is published, the type of the Moleculer event is EventBus.<MESSAGE_TYPE>. For instance, when the Event EventA is published, the Moleculer event name is EventBus.EventA.

By default, the implementation publish messages using the balanced mode. That means, each balanced event will be processed by only one EventBus Moleculer service instance within the cluster.

The default mode works well for monoliths where the logic is replicated in each node of the cluster.

Event Balancer and Monolith

However, for Microliths or even Microservices, the EventBus instances have to arrange in logical groups. The group name of MoleculerEventBus instances can be provided by constructor.

Event Balancer and Microlith

The Inversion Module

The package provides an Inversion Module which can be used to create the MoleculerGateway instance and register it on the registry.

Create a container with the default module behavior, i.e. a ServiceBroker is expected in the registry:

import { ContainerBuilder, ModuleBuilder } from "@tmorin/ceb-inversion-core"
import {
  Gateway,
  GatewaySymbol,
  MessageBuilder,
} from "@tmorin/ceb-messaging-core"
import {
  MoleculerModule,
  ServiceBrokerSymbol,
} from "@tmorin/ceb-messaging-moleculer-inversion"
import { ServiceBroker } from "moleculer"

ContainerBuilder.get()
  // create a service broker
  .module(
    ModuleBuilder.get()
      .configure(function (registry) {
        registry.registerFactory<ServiceBroker>(
          ServiceBrokerSymbol,
          () => new ServiceBroker()
        )
      })
      .build()
  )
  // register the module which build the Moleculer Gateway
  .module(new MoleculerModule())
  .build()
  .initialize()
  .then((container) => {
    // resolve the gateway
    const gateway = container.registry.resolve<Gateway>(GatewaySymbol)
    // publish a moleculer message
    gateway.events.publish(MessageBuilder.event("Hello").build())
  })
  .catch((e) => console.error(e))

Create a container with a provided ServiceBroker instance. In that case, the provided ServiceBroker will be registered in the registry.

import { ContainerBuilder } from "@tmorin/ceb-inversion-core"
import {
  Gateway,
  GatewaySymbol,
  MessageBuilder,
} from "@tmorin/ceb-messaging-core"
import { MoleculerModule } from "@tmorin/ceb-messaging-moleculer-inversion"
import { ServiceBroker } from "moleculer"

ContainerBuilder.get()
  // register the module which build the Moleculer Gateway
  .module(
    new MoleculerModule({
      broker: new ServiceBroker(),
    })
  )
  .build()
  .initialize()
  .then((container) => {
    // resolve the gateway
    const gateway = container.registry.resolve<Gateway>(GatewaySymbol)
    // publish a moleculer message
    gateway.events.publish(MessageBuilder.event("Hello").build())
  })
  .catch((e) => console.error(e))

Adapters

The DOM Adapter

The adapter is bundled in the NPM package @tmorin/ceb-messaging-adapter-dom.

The DOM adapter provides a bridges between the DOM event handling system and a Gateway. The main purpose is to drive the development of UI components with an Event/Message Architecture approach without dependencies or additional library.

Handling of Command messages

Handling of Query messages

Handling of Event messages

The Electron Adapter

The adapter is bundled in the NPM package @tmorin/ceb-messaging-adapter-electron.

The Electron adapter provides a bridges between the Electron IPC Event Emitter and a Gateway. The main purpose is to standardize the communication flows between the main context and the renderer contexts based on the Location Transparency pattern.

The bridge is bidirectional for all message kinds. However, messages dispatched from a Renderer context are only forwarded to the Main context. That means sibling Renderer contexts won't get the messages.

From Renderer to Main

However, on the other side, messages dispatched from the Main Context are forwarded to all Renderer contexts.

From Main to Renderer

The Purify Adapter

The adapter is bundled in the NPM package @tmorin/ceb-messaging-adapter-purify.

The Purify adapter provides an adapter of Gateways which integrates Purify types. The main purpose is to provide a hint of functional programing style for the processing of commands, queries and results.

The buses hierarchy

Elements

<ceb/> relies on the Builder Pattern and also Decorators to define, enhance and finally register Custom Elements.

The main builder ElementBuilder handles the definition and registration of Custom Elements.

Then, other builders can be used to enhance it:

For convenience, the package @tmorin/ceb-bundle-web provides all built-in artifacts for the Custom Elements authoring.

ElementBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-core.

The class ElementBuilder provides services to define and register a Custom Element.

Challenge yourself

Will you be able to ...

  1. change the tag name to <my-greeting></my-greeting> without changing the class name?
  2. transform SimpleGreeting as an extension of h1, so that can be created with <h1 is="my-greeting"></h1>? the class of h1 is HTMLHeadingElement

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

Define a regular Custom Element

import { ElementBuilder } from "@tmorin/ceb-elements-core"

// defines and register the custom element class
@ElementBuilder.get().decorate()
class SimpleGreeting extends HTMLElement {
  constructor(public name = "World") {
    super()
  }

  connectedCallback() {
    this.textContent = `Hello, ${this.name}!`
  }
}

Once registered, the Custom Element can be created with three different styles: markup, Object-Oriented and, hybrid.

The first one relies on the tag name of the Custom Element within the markup of an HTML document.

document.body.innerHTML = `<simple-greeting></simple-greeting>`

The second one relies on the Object-Oriented nature of the Custom Element. Basically, the class can be instantiated, and the created object can be then append to the DOM.

const helloJohn: SimpleGreeting = new SimpleGreeting("John")
document.body.appendChild(helloJohn)

The last one lies between the markup and OO style.

const helloDoe: SimpleGreeting = document.createElement("simple-greeting")
helloDoe.name = "Doe"
document.body.appendChild(helloDoe)

Define an extension of a native Element

import { ElementBuilder } from "@tmorin/ceb-elements-core"

// defines and register the custom element class
@ElementBuilder.get().extends("p").decorate()
class SimpleGreetingParagraph extends HTMLParagraphElement {
  constructor(public name = "World") {
    super()
  }

  connectedCallback() {
    this.textContent = `Hello, ${this.name}!`
  }
}

Once registered, the Custom Element can be created like the regular one. However, because of the extension of a native Element, the creation expects additional information.

The creation with the markup style:

document.body.innerHTML = `<p is="simple-greeting-paragraph"></p>`

The creation with the Object-Oriented style:

const helloJohn: SimpleGreetingParagraph = new SimpleGreeting("John")
document.body.appendChild(helloJohn)

The creation with the hybrid style:

const helloDoe: SimpleGreetingParagraph = document.createElement("p", {
    extends: "is"
})
helloDoe.name = "Doe"
document.body.appendChild(helloDoe)

AttributeBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder handles the initialization of an attribute as well as the registration of its listeners.

Challenge yourself

Will you be able to ...

  1. display Hello, John Doe! just setting the attribute name?
  2. change the attribute name to alt-name without changing the method name?

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

FieldBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder binds a property to an attribute. So that, the value is available and mutable from both sides.

Challenge yourself

Will you be able to ...

  1. display Hello, John Doe! just setting the property name?
  2. change the attribute name to alt-name without changing the property name?

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

OnBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder handles the addition and removal of DOM event listeners.

Challenge yourself

Will you be able to ...

  1. display Hello, World! only when the click event comes from a button? c.f. Event Delegation ;)
  2. display Hello, World! only on double click? there is a native event for that ;)

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

ContentBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder handles the initialization of the HTML content of the Custom Element.

Challenge yourself

Will you be able ...

  1. to display Hello, John Doe!?
  2. to render into a Shadow DOM?

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

TemplateBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder handles the integration of a templating solution to update the content of the Custom Element.

The builder doesn't provide the templating solution out of the box. However, the library provides a built-in solution described later, c.f. Templating .

Challenge yourself

Will you be able to ...

  1. display Hello, John Doe! calling the render method?
  2. render into a Shadow DOM?
  3. render into a Grey DOM?

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

ReferenceBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder enhances a readonly property to execute a CSS Selector once the property is get. So that, a property of the Custom Element can always be related to a child element or a set of child elements.

Challenge yourself

Will you be able to ...

  1. implement the method sayHelloJohnDoe() to display Hello, John Doe!?

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

AttributePropagationBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder handles the propagation of an attribute's values to embedded elements. That means, each time the attribute is mutated, the mutation is propagated to selected child nodes.

Challenge yourself

Will you be able to ...

  1. propagate the value of the attribute value to the property placeholder of the input?
  2. propagate the value of the attribute frozen to the attribute disabled of the input?

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

PropertyDelegationBuilder

The builder is bundled in the NPM package @tmorin/ceb-elements-builders.

The builder delegates the accesses of a property to an embedded element.

Challenge yourself

Will you be able to ...

  1. delegate the property value to the attribute placeholder of the input?
  2. delegate the property frozen to the property disabled of the input?

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

Templating

<ceb/> provides a built-in solution for templating which relies on three main components.

The first component is the template engine. Its purpose is to patch the DOM incrementally. The implementation and the underlying API is similar to incremental-dom. The main difference is the full support of Custom Elements, especially the handling of a Grey DOM, or scope.

The engine is part of the NPM package @tmorin/ceb-templating-engine.

The second component is a user-friendly interface which operates the command. Presently, the library provides an interface leveraging on the Tagged Templates. Its usage is cover in the Template literal section.

The user-friendly interface is part of the NPM package @tmorin/ceb-templating-literal.

Finally, the third component is a builder which enhances a method of the Custom Element: TemplateBuilder. So that, when the enhanced method is invoked, the Custom Element's content is dynamically updated.

The builder is part of the NPM package @tmorin/ceb-templating-builder.

The Light, Grey and Shadow DOMs

When a Custom Element is responsible for a part of its child nodes, the usage of Shadow DOM is welcoming. Shadow DOM handles the HTMLSlotElement elements which can be used as placeholders. However, Shadow DOM brings a level of isolation which is not always welcome. Especially for the shadowified markup which relies on common stylesheets.

The built-in template engine provides a solution to handle Grey DOM. The purpose is to keep the concept of slot coming from Shadow DOM but in the Light DOM. Therefore, the DOM tree between the Custom Element node, and the slot node becomes a Grey DOM.

Basically, a Grey DOM can only be mutated from its Custom Element and, the Custom Element can only mutate its Grey DOM. Moreover, like for the Shadow DOM, the Grey DOM handles kind of <slot> elements to manage the placeholders. However, the Grey DOM is not isolated from the Light DOM context (javascript, styles ...). For senior JS developers :), it is similar to the transclude concept implemented in AngularJS.

Template literal

The template solution is part of the NPM package @tmorin/ceb-templating-literal.

The built-in template solution provides an API to express templates based on Template literal. The API is the Tagged Templates html.

Common usages

Text

Write the content Hello, World! in the <p> element:

import { Template } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

const name = "World"

const template: Template = html`<p>Hello, ${name}!</p>`

template.render(document.body)

Attribute

Set the value foo to the attribute bar:

import { Template } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

const foo = "bar"

const template: Template = html`<input class="${foo}" />`

template.render(document.body)

Set boolean values, the checked attribute won't be rendered because its value is false:

import { Template } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

const checked = false

const template: Template = html`<input
  required
  disabled=""
  checked="${checked}" />`

template.render(document.body)

Property

Set the value foo to the property bar:

import { Template } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

const value = "Foo"

const template: Template = html`<input p:bar="${value}" />`

template.render(document.body)

Prevent extra processing

The special attribute o:skip, notifies the template engine that the children of the element should not be processed.

import { Template } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

const template: Template = html`<div><ul o:skip></ul></div>`

template.render(document.body)

When rendering within a Shadow DOM, the usage of the element <slot> have the same effect: the children of the slot element won't be processed.

Optimize patch activities

The special attribute o:key, notifies the template engine that the current node can be identified by a key. The key can be of any types.

The feature should be used when rendering a dynamic list where the items can be added/removed/shift. For each item, the o:key should be provided. So that, the engine will be able to efficiently discover the related DOM nodes.

import { Template } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

const lis = ["item A", "item B"].map(
  (item) => html`<li o:key="${item}">${item}</li>`
)

const template: Template = html`<div>
  <ul>
    ${lis}
  </ul>
</div>`

template.render(document.body)

When rendering within a Shadow DOM, the usage of the element <slot> have the same effect: the children of the slot element won't be processed.

Grey DOM

The special element <ceb-sot></ceb-slot> is the marker of the placeholder.

Given the following Custom Element with template expressed using the literal approach:

import { ElementBuilder } from "@tmorin/ceb-elements-core"
import { Template, TemplateBuilder } from "@tmorin/ceb-templating-builder"
import { html } from "@tmorin/ceb-templating-literal"

class HelloWorld extends HTMLElement {
  render(): Template {
    return html`<p>Hello, <ceb-slot></ceb-slot>!</p>`
  }
}

ElementBuilder.get().builder(TemplateBuilder.get().grey()).register()

When the following statement is created and rendered:

<hello-worlder>John Doe</hello-worlder>

Then the Light DOM becomes:

<hello-worlder>
  Hello, <ceb-slot>John Doe<ceb-slot>!
</hello-worlder>

Examples

Some examples demonstrate the usage of <ceb/> features:

  • TodoMVC : an implementation of the TodoMVC application
  • ex-greeting : a simple Custom Element to display a greeting message

TodoMVC

This example is an implementation of the TodoMVC application with <ceb/>.

Introduction

The implementation embraces a kind of Hexagonal Architecture to provide a loose coupling integration between: the UI, the application logic (i.e. the model) and, the adapters (i.e. the persistence system ...). The communication between both concerns the UI and the application logic is managed by a Message/Event Driven approach which emphasizes the CQRS pattern.

Codebase

The codebase is composed of four Bounded Contexts organized in modules.

The Bounded Contexts

NameDescription
TodoManage the todos of the list.
FilterManage the filtering of the list.
AppManage the application lifecycle.
User InterfaceManage the User Interface.

The Bounded Contexts

The Module Types

The modules are qualified by types. The purpose of the types is to organize the source code according to the Hexagonal Architecture.

Each bounded context may have modules of the following types: core, api, infra, ui and e2e.

The core modules contain the implementation of the Bounded Context's functionalities. They contain also the ports as defined by the Hexagonal Architecture.

The api modules provides resources to interact with the Bounded Context's functionalities. They are mainly composed of message definitions (commands, events, queries and results) and other data structures.

The infra modules contain the implementations of ports defined in the core modules, i.e. adapters as defined by the Hexagonal Architecture.

The ui modules contain resources handling the interactions with humans.

Finally, the e2e (i.e. end-to-end ) modules contain test suites which can be used by infra modules to validate the implementation of ports coming from core modules.

Todo

This bounded context manages the todos which composes the todo list.

The modules of Todo

Filter

This bounded context manages the filtering of the todo list.

The modules of Filter

App

This bounded context manages the application lifecycle. The main purpose of the Bounded Context is to produce and publish the application state.

The modules of App

User Interface

This bounded context manages the user interactions.

It is composed of two exclusive modules, ui-elements and ui-frp.

The first one provides an implementation based on Custom Elements and leveraging on the <ceb/> library exclusively.

The ui-elements modules

The second one provides a Functional Reactive Programing implementation which leverages on RxJS for the functional domain and lit-html for the rendering side.

The ui-frp modules

Collaboration between Bounded Contexts

AddTodo

The command AddTodo adds a new Todo to the todo list. The command is triggered by the User Interface then once the causality chain is done, the Integration Event TodosUpdated is published. Therefore, the UI can react.

The AddTodo flow

The flow is similar for the ChangeFilter command.

The ChangeFilter flow

ex-greeting

This example demonstrates how to leverage on some builders and decorators to create a Custom Element which displays a greeting message.

The example is available on codepen.io!

Initiate the Custom Element class

The Custom Element ex-greeting is a regular ES6 class which extends HTMLElement :

export class ExGreeting extends HTMLElement {}

Register the Custom Element

To register ex-greeting, the decorator of @ElementBuilder is used:

import { ElementBuilder } from "@tmorin/ceb-elements-core"

@ElementBuilder.get().decorate()
export class ExGreeting extends HTMLElement {}

Initialize the Shadow DOM

The Shadow DOM of ex-greeting is initialized with the decorator of @ContentBuilder :

import { ElementBuilder } from "@tmorin/ceb-elements-core"
import { ContentBuilder } from "@tmorin/ceb-elements-builders"

@ElementBuilder.get().decorate()
@ContentBuilder.get(`<p>Hello, <span id="name"></span>!</p>`)
  .shadow()
  .decorate()
export class ExGreeting extends HTMLElement {}

Capture the name

The target of the greeting is captured with the field name using the decorator of FieldBuilder :

import { ElementBuilder } from "@tmorin/ceb-elements-core"
import { ContentBuilder, FieldBuilder } from "@tmorin/ceb-elements-builders"

@ElementBuilder.get().decorate()
@ContentBuilder.get(`<p>Hello, <span id="name"></span>!</p>`)
  .shadow()
  .decorate()
export class ExGreeting extends HTMLElement {
  @FieldBuilder.get().decorate()
  name: string = "World"
}

Update the Shadow DOM with the captured name

Each time the field name mutates, the element selected by span#name has to be updated with the new value. There are two ways to handle it with the built-in <ceb/> builders : the craft style and the propagation way.

The craft style

The decorator of ReferenceBuilder retrieves the reference of the element span#name.

import { ElementBuilder } from "@tmorin/ceb-elements-core"
import {
  ContentBuilder,
  FieldBuilder,
  ReferenceBuilder,
} from "@tmorin/ceb-elements-builders"

@ElementBuilder.get().decorate()
@ContentBuilder.get(`<p>Hello, <span id="name"></span>!</p>`)
  .shadow()
  .decorate()
export class ExGreeting extends HTMLElement {
  @FieldBuilder.get().decorate()
  name: string = "World"

  @ReferenceBuilder.get().shadow().selector("span#name").decorate()
  span?: HTMLSpanElement
}

Finally, the decorator of FieldBuilder handles the mutation of the field name.

import { FieldListenerData } from "@tmorin/ceb-elements-builders"
import { ElementBuilder } from "@tmorin/ceb-elements-core"
import {
  ContentBuilder,
  FieldBuilder,
  ReferenceBuilder,
} from "@tmorin/ceb-elements-builders"

@ElementBuilder.get().decorate()
@ContentBuilder.get(`<p>Hello, <span id="name"></span>!</p>`)
  .shadow()
  .decorate()
export class ExGreeting extends HTMLElement {
  @FieldBuilder.get().decorate()
  name: string = "World"

  @ReferenceBuilder.get().shadow().selector("span#name").decorate()
  span?: HTMLSpanElement

  @FieldBuilder.get().decorate()
  private onName(data: FieldListenerData<string>) {
    if (this.span) {
      this.span.textContent = data.newVal
    }
  }
}

The propagation way

Alternatively, the decorator of AttributePropagationBuilder can be used to automatically binds the mutation of the field name to the property textContent of the selected element span#name :

import { ElementBuilder } from "@tmorin/ceb-elements-core"
import { ContentBuilder, FieldBuilder, FieldListenerData, ReferenceBuilder } from "@tmorin/ceb-elements-builders"

@ElementBuilder.get<ExGreeting>().decorate()
@ContentBuilder.get(`<p>Hello, <span id="name"></span>!</p>`).shadow().decorate()
export class ExGreeting extends HTMLElement {
  @ReferenceBuilder.get().shadow().selector("span#name").decorate()
  span?: HTMLSpanElement

  @FieldBuilder.get().decorate()
  name = "World"

  @FieldBuilder.get().decorate()
  private onName(data: FieldListenerData<string>) {
    if (this.span) {
      this.span.textContent = data.newVal
    }
  }
}

References

Books

  • Domain-Driven Design: Tackling Complexity in the Heart of Software, by Eric Evans, 2004, www.dddcommunity.org
  • Clean Architecture: A Craftsman's Guide to Software Structure and Design, by Robert C. Martin, 2018, www.pearson.com
  • Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions, by Gregor Hohpe and Bobby Woolf, 2004, www.enterpriseintegrationpatterns.com

Articles

Specifications

Libraries