Skip to content
This repository has been archived by the owner on Oct 31, 2023. It is now read-only.

Commit

Permalink
feat(ceb-inversion-builder): inject entries in Custom Elements
Browse files Browse the repository at this point in the history
  • Loading branch information
tmorin committed Nov 8, 2021
1 parent 507179c commit 8e4961d
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -66,6 +66,7 @@ A built-in implementation of a templating system:
A built-in implementation of the Inversion of Control principle:

- [ceb-inversion](./packages/ceb-inversion)
- [ceb-inversion-builder](./packages/ceb-inversion-builder)

A built-in implementation of the Event/Message architecture:

Expand Down
1 change: 1 addition & 0 deletions lerna.json
Expand Up @@ -6,6 +6,7 @@
"packages/ceb-core",
"packages/ceb-builders",
"packages/ceb-inversion",
"packages/ceb-inversion-builder",
"packages/ceb-messaging-adapter-electron",
"packages/ceb-messaging-core",
"packages/ceb-messaging-simple",
Expand Down
22 changes: 22 additions & 0 deletions packages/ceb-inversion-builder/README.md
@@ -0,0 +1,22 @@
# @tmorin/ceb-inversion-builder

[![npm version](https://badge.fury.io/js/%40tmorin%2Fceb-inversion-builder.svg)](https://badge.fury.io/js/%40tmorin%2Fceb-inversion-builder)
[![api](https://img.shields.io/badge/-api-informational.svg)](https://tmorin.github.io/ceb/api/modules/_tmorin_ceb_inversion_builder.html)

> The package is part of the `<ceb/>` library.
> It provides a builder to inject entries in Custom Elements.
## Install

The NPM package is compliant [CommonJs](https://flaviocopes.com/commonjs) and [ES Module](https://flaviocopes.com/es-modules).

```bash
npm install @tmorin/ceb-inversion-builder
```

## License

Released under the [MIT license].

[Custom Elements (v1)]: https://html.spec.whatwg.org/multipage/custom-elements.html
[MIT license]: http://opensource.org/licenses/MIT
1 change: 1 addition & 0 deletions packages/ceb-inversion-builder/karma.conf.js
@@ -0,0 +1 @@
module.exports = require('../../karma.conf')
57 changes: 57 additions & 0 deletions packages/ceb-inversion-builder/package.json
@@ -0,0 +1,57 @@
{
"name": "@tmorin/ceb-inversion-builder",
"version": "4.0.3-alpha.12",
"license": "MIT",
"description": "The package is part of the `<ceb/>` library. It provides a builder to inject entries in Custom Elements.",
"keywords": [
"custom-element-builder",
"custom-elements-v1",
"custom-elements",
"custom-element",
"typescript",
"typescript-library",
"javascript-library",
"inversion-of-control",
"ioc",
"injection"
],
"homepage": "https://tmorin.github.io/ceb",
"bugs": {
"url": "https://github.com/tmorin/ceb/issues"
},
"repository": {
"type": "git",
"url": "git@github.com:tmorin/ceb.git"
},
"author": {
"name": "Thibault Morin",
"url": "https://tmorin.github.io"
},
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"import": "./dist/mjs/index.js",
"require": "./dist/cjs/index.js"
}
},
"types": "dist/types/index.d.ts",
"main": "dist/cjs/index.js",
"module": "dist/mjs/index.js",
"files": [
"dist"
],
"scripts": {
"build": "npm run build:lib && npm run build:module && npm run build:fix",
"build:fix": "../../scripts/fix-dist.js",
"build:lib": "tsc -p tsconfig.build.json --module CommonJS --outDir dist/cjs --declaration --declarationDir dist/types",
"build:module": "tsc -p tsconfig.build.json --module ESNext --outDir dist/mjs",
"test": "karma start --single-run --no-auto-watch --browsers FirefoxHeadless,ChromeHeadless",
"test:watch": "karma start --no-single-run --auto-watch --browsers Firefox"
},
"dependencies": {
"@tmorin/ceb-core": "4.0.3-alpha.12",
"@tmorin/ceb-inversion": "4.0.3-alpha.12"
}
}
75 changes: 75 additions & 0 deletions packages/ceb-inversion-builder/src/builder.decorator.spec.ts
@@ -0,0 +1,75 @@
import {assert} from "chai"
import {Container, ContainerBuilder, OnlyConfigureModule} from "@tmorin/ceb-inversion";
import {ElementBuilder} from "@tmorin/ceb-core";
import {InversionBuilder} from "./builder";
import {InversionBuilderModule} from "./module";

class ServiceA {
methodA() {
return "resultA"
}
}

class ServiceB {
constructor(
private readonly serviceA: ServiceA
) {
}

methodA() {
return this.serviceA.methodA()
}
}

describe("inversion/builder/decorator", function () {
let sandbox: HTMLDivElement
let container: Container
let testElement: TestElement
const tagName = "inversion-builder-decorator"

@ElementBuilder.get(TestElement).name(tagName).decorate()
class TestElement extends HTMLElement {
@InversionBuilder.get().decorate()
serviceA?: ServiceA
@InversionBuilder.get().decorate()
serviceB?: ServiceB
@InversionBuilder.get().key("serviceB").decorate()
serviceBbis?: ServiceB
@InversionBuilder.get().key(Symbol.for("serviceBWithSymbol")).decorate()
serviceBWithSymbol?: ServiceB
}

before(async () => {
container = await ContainerBuilder.get()
.module(new InversionBuilderModule())
.module(OnlyConfigureModule.create(async function () {
this.registry.registerFactory<ServiceA>("serviceA", () => new ServiceA())
this.registry.registerFactory<ServiceB>("serviceB", (registry) => new ServiceB(registry.resolve<ServiceA>("serviceA")))
this.registry.registerFactory<ServiceB>(Symbol.for("serviceBWithSymbol"), (registry) => registry.resolve<ServiceB>("serviceB"))
}))
.build()
.initialize()
sandbox = document.body.appendChild(document.createElement('div'))
testElement = sandbox.appendChild(document.createElement(tagName) as TestElement)

})
after(async () => {
// @ts-ignore
InversionBuilder.setDefaultContainer(undefined)
container?.dispose()
})
it("should inject serviceA", function () {
assert.property(testElement, "serviceA")
assert.strictEqual(testElement.serviceA?.methodA(), "resultA")
})
it("should inject serviceB", function () {
assert.property(testElement, "serviceB")
assert.strictEqual(testElement.serviceB?.methodA(), "resultA")
assert.property(testElement, "serviceBbis")
assert.strictEqual(testElement.serviceBbis?.methodA(), "resultA")
})
it("should inject serviceBWithSymbol", function () {
assert.property(testElement, "serviceBWithSymbol")
assert.strictEqual(testElement.serviceBWithSymbol?.methodA(), "resultA")
})
})
108 changes: 108 additions & 0 deletions packages/ceb-inversion-builder/src/builder.spec.ts
@@ -0,0 +1,108 @@
import {assert} from "chai"
import {Container, ContainerBuilder, OnlyConfigureModule} from "@tmorin/ceb-inversion";
import {ElementBuilder} from "@tmorin/ceb-core";
import {InversionBuilder} from "./builder";
import {InversionBuilderModule} from "./module";

class ServiceA {
methodA() {
return "resultA"
}
}

class ServiceB {
constructor(
private readonly serviceA: ServiceA
) {
}

methodA() {
return this.serviceA.methodA()
}
}

describe("inversion/builder", function () {
let sandbox: HTMLDivElement
let container: Container

describe("with InversionBuilderModule", function () {
let testElement: TestElement
const tagName = "inversion-builder-inversion-builder-module"

class TestElement extends HTMLElement {
serviceA?: ServiceA
serviceB?: ServiceB
}

before(async () => {
container = await ContainerBuilder.get()
.module(new InversionBuilderModule())
.module(OnlyConfigureModule.create(async function () {
this.registry.registerFactory<ServiceA>("ServiceA", () => new ServiceA())
this.registry.registerFactory<ServiceB>("ServiceB", (registry) => new ServiceB(registry.resolve<ServiceA>("ServiceA")))
}))
.build()
.initialize()
sandbox = document.body.appendChild(document.createElement('div'))
ElementBuilder.get(TestElement).name(tagName).builder(
InversionBuilder.get("serviceA").key("ServiceA"),
InversionBuilder.get("serviceB").key("ServiceB"),
).register()
testElement = sandbox.appendChild(document.createElement(tagName) as TestElement)

})
after(async () => {
// @ts-ignore
InversionBuilder.setDefaultContainer(undefined)
container?.dispose()
})
it("should inject ServiceA", function () {
assert.property(testElement, "serviceA")
assert.strictEqual(testElement.serviceA?.methodA(), "resultA")
})
it("should inject ServiceB", function () {
assert.property(testElement, "serviceB")
assert.strictEqual(testElement.serviceB?.methodA(), "resultA")
})
})

describe("with a container provider", function () {
let testElement: TestElement
const tagName = "inversion-builder-container-provider"

class TestElement extends HTMLElement {
serviceA?: ServiceA
serviceB?: ServiceB
}

before(async () => {
container = await ContainerBuilder.get()
.module(OnlyConfigureModule.create(async function () {
this.registry.registerFactory<ServiceA>("ServiceA", () => new ServiceA())
this.registry.registerFactory<ServiceB>("ServiceB", (registry) => new ServiceB(registry.resolve<ServiceA>("ServiceA")))
}))
.build()
.initialize()
sandbox = document.body.appendChild(document.createElement('div'))
ElementBuilder.get(TestElement).name(tagName).builder(
InversionBuilder.get("serviceA").key("ServiceA").provider(() => container),
InversionBuilder.get("serviceB").key("ServiceB").provider(() => container),
).register()
testElement = sandbox.appendChild(document.createElement(tagName) as TestElement)

})
after(async () => {
// @ts-ignore
InversionBuilder.setDefaultContainer(undefined)
container?.dispose()
})
it("should inject ServiceA", function () {
assert.property(testElement, "serviceA")
assert.strictEqual(testElement.serviceA?.methodA(), "resultA")
})
it("should inject ServiceB", function () {
assert.property(testElement, "serviceB")
assert.strictEqual(testElement.serviceB?.methodA(), "resultA")
})
})
})
106 changes: 106 additions & 0 deletions packages/ceb-inversion-builder/src/builder.ts
@@ -0,0 +1,106 @@
import {Builder, CustomElementConstructor, ElementBuilder, HooksRegistration} from "@tmorin/ceb-core";
import {Container, RegistryKey} from "@tmorin/ceb-inversion";

/**
* Factory of a bus.
*/
export interface ContainerProvider {
/**
* @return the bus
*/
(): Container
}

/**
* The builder injects an entry from a container into a Custom Element.
*/
export class InversionBuilder<E extends HTMLElement> implements Builder<E> {

private static DEFAULT_CONTAINER: Container

/**
* Set the default {@link Container}.
* @param container the container
* @internal
*/
static setDefaultContainer(container: Container) {
InversionBuilder.DEFAULT_CONTAINER = container
}

private constructor(
private _propName?: string,
private _key?: RegistryKey,
private _provider?: ContainerProvider,
) {
}

/**
* Provides a fresh builder.
* @param propName the property name
* @template E the type of the Custom Element
*/
static get<E extends HTMLElement>(propName?: string) {
return new InversionBuilder<E>(propName)
}

/**
* Set the registry key.
* @param key the registry key
*/
key(key: RegistryKey): InversionBuilder<E> {
this._key = key
return this
}

/**
* Set the Container provider.
* @param provider the provider
*/
provider(provider: ContainerProvider): InversionBuilder<E> {
this._provider = provider
return this
}

/**
* Decorates the property of the bus.
*/
decorate(): PropertyDecorator {
return (target, propName) => {
this._propName = propName.toString()
if (!this._key) {
this._key = this._propName
}
const id = `bus-field-inversion-${this._propName}`
ElementBuilder.getOrSet(target, this, id)
}
}

build(Constructor: CustomElementConstructor<E>, hooks: HooksRegistration<E>): void {
if (!this._propName) {
throw new TypeError("InversionBuilder - the property name is missing")
}
if (!this._key) {
throw new TypeError("InversionBuilder - the registry key is missing")
}
const _propName = this._propName
const _key = this._key
const _provider = this._provider
hooks.before('constructorCallback', el => {
let _container: Container
if (_provider) {
_container = _provider()
} else if (InversionBuilder.DEFAULT_CONTAINER) {
_container = InversionBuilder.DEFAULT_CONTAINER
} else {
throw new TypeError("InversionBuilder - unable to resolve a Container")
}
Object.defineProperty(el, _propName, {
value: _container.registry.resolve(_key),
configurable: false,
writable: false,
enumerable: true
})
})
}

}
Empty file.

0 comments on commit 8e4961d

Please sign in to comment.