@joist/di 4.0.0-next.4 → 4.0.0-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +4 -14
  2. package/src/lib/dom-injector.test.ts +16 -18
  3. package/src/lib/dom-injector.ts +3 -3
  4. package/src/lib/inject.test.ts +32 -55
  5. package/src/lib/inject.ts +2 -2
  6. package/src/lib/injectable-el.test.ts +130 -0
  7. package/src/lib/injectable-el.ts +63 -0
  8. package/src/lib/injectable.test.ts +19 -114
  9. package/src/lib/injectable.ts +16 -58
  10. package/src/lib/injector.test.ts +132 -130
  11. package/src/lib/injector.ts +10 -9
  12. package/src/lib/lifecycle.test.ts +56 -58
  13. package/src/lib/lifecycle.ts +13 -2
  14. package/src/lib/provider.ts +0 -2
  15. package/target/lib/dom-injector.js +3 -3
  16. package/target/lib/dom-injector.test.js +16 -18
  17. package/target/lib/dom-injector.test.js.map +1 -1
  18. package/target/lib/inject.js +2 -2
  19. package/target/lib/inject.test.js +57 -89
  20. package/target/lib/inject.test.js.map +1 -1
  21. package/target/lib/injectable-el.d.ts +334 -0
  22. package/target/lib/injectable-el.js +40 -0
  23. package/target/lib/injectable-el.js.map +1 -0
  24. package/target/lib/injectable-el.test.js +238 -0
  25. package/target/lib/injectable-el.test.js.map +1 -0
  26. package/target/lib/injectable.d.ts +2 -5
  27. package/target/lib/injectable.js +11 -41
  28. package/target/lib/injectable.js.map +1 -1
  29. package/target/lib/injectable.test.js +50 -233
  30. package/target/lib/injectable.test.js.map +1 -1
  31. package/target/lib/injector.d.ts +1 -1
  32. package/target/lib/injector.js +5 -7
  33. package/target/lib/injector.js.map +1 -1
  34. package/target/lib/injector.test.js +215 -216
  35. package/target/lib/injector.test.js.map +1 -1
  36. package/target/lib/lifecycle.d.ts +11 -2
  37. package/target/lib/lifecycle.js +4 -2
  38. package/target/lib/lifecycle.js.map +1 -1
  39. package/target/lib/lifecycle.test.js +120 -122
  40. package/target/lib/lifecycle.test.js.map +1 -1
  41. package/target/lib/provider.d.ts +0 -1
  42. package/src/lib/injector.test-node.ts +0 -187
  43. package/target/lib/injector.test-node.js +0 -231
  44. package/target/lib/injector.test-node.js.map +0 -1
  45. /package/target/lib/{injector.test-node.d.ts → injectable-el.test.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/di",
3
- "version": "4.0.0-next.4",
3
+ "version": "4.0.0-next.6",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -20,7 +20,7 @@
20
20
  "description": "Dependency Injection for Vanilla JS classes",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/deebloo/joist.git"
23
+ "url": "git+https://github.com/joist-framework/joist.git"
24
24
  },
25
25
  "keywords": [
26
26
  "TypeScript",
@@ -31,7 +31,7 @@
31
31
  "author": "deebloo",
32
32
  "license": "MIT",
33
33
  "bugs": {
34
- "url": "https://github.com/deebloo/joist/issues"
34
+ "url": "https://github.com/joist-framework/joist/issues"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -57,17 +57,7 @@
57
57
  "test": {
58
58
  "command": "wtr --config wtr.config.mjs",
59
59
  "files": [
60
- "target/**"
61
- ],
62
- "output": [],
63
- "dependencies": [
64
- "build",
65
- "test:node"
66
- ]
67
- },
68
- "test:node": {
69
- "command": "node --test target/**/*.test-node.js",
70
- "files": [
60
+ "wtr.config.mjs",
71
61
  "target/**"
72
62
  ],
73
63
  "output": [],
@@ -1,30 +1,28 @@
1
- import { expect } from '@open-wc/testing';
1
+ import { assert } from 'chai';
2
2
 
3
3
  import { DOMInjector } from './dom-injector.js';
4
- import { Injectables } from './injector.js';
4
+ import { injectables } from './injector.js';
5
5
 
6
- describe('DOMInjector', () => {
7
- it('should attach an injector to a dom element', () => {
8
- const root = document.createElement('div');
9
- const app = new DOMInjector();
6
+ it('should attach an injector to a dom element', () => {
7
+ const root = document.createElement('div');
8
+ const app = new DOMInjector();
10
9
 
11
- app.attach(root);
10
+ app.attach(root);
12
11
 
13
- const injector = Injectables.get(root);
12
+ const injector = injectables.get(root);
14
13
 
15
- expect(injector).to.equal(app);
16
- });
14
+ assert.strictEqual(injector, app);
15
+ });
17
16
 
18
- it('should remove an injector associated with a dom element', () => {
19
- const root = document.createElement('div');
20
- const app = new DOMInjector();
17
+ it('should remove an injector associated with a dom element', () => {
18
+ const root = document.createElement('div');
19
+ const app = new DOMInjector();
21
20
 
22
- app.attach(root);
21
+ app.attach(root);
23
22
 
24
- expect(Injectables.get(root)).to.equal(app);
23
+ assert.strictEqual(injectables.get(root), app);
25
24
 
26
- app.detach(root);
25
+ app.detach(root);
27
26
 
28
- expect(Injectables.get(root)).to.equal(undefined);
29
- });
27
+ assert.strictEqual(injectables.get(root), undefined);
30
28
  });
@@ -1,11 +1,11 @@
1
- import { Injectables, Injector } from './injector.js';
1
+ import { injectables, Injector } from './injector.js';
2
2
 
3
3
  export class DOMInjector extends Injector {
4
4
  attach(root: HTMLElement) {
5
- Injectables.set(root, this);
5
+ injectables.set(root, this);
6
6
  }
7
7
 
8
8
  detach(root: HTMLElement) {
9
- Injectables.delete(root);
9
+ injectables.delete(root);
10
10
  }
11
11
  }
@@ -1,25 +1,12 @@
1
- import { expect } from '@open-wc/testing';
1
+ import { assert } from 'chai';
2
2
 
3
3
  import { inject } from './inject.js';
4
4
  import { injectable } from './injectable.js';
5
5
  import { Injector } from './injector.js';
6
6
  import { StaticToken } from './provider.js';
7
7
 
8
- describe('inject', () => {
9
- it('should work', () => {
10
- class HelloService {}
11
-
12
- @injectable()
13
- class HelloWorld extends HTMLElement {
14
- hello = inject(HelloService);
15
- }
16
-
17
- customElements.define('inject-1', HelloWorld);
18
-
19
- expect(new HelloWorld().hello()).to.be.instanceOf(HelloService);
20
- });
21
-
22
- it('should throw error if called in constructor', () => {
8
+ it('should throw error if called in constructor', () => {
9
+ assert.throws(() => {
23
10
  class FooService {
24
11
  value = '1';
25
12
  }
@@ -35,49 +22,39 @@ describe('inject', () => {
35
22
 
36
23
  const parent = new Injector();
37
24
 
38
- try {
39
- parent.inject(BarService);
40
-
41
- throw new Error('Should not succeed');
42
- } catch (err) {
43
- const error = err as Error;
44
-
45
- expect(error.message).to.equal(
46
- `BarService is either not injectable or a service is being called in the constructor. \n Either add the @injectable() to your class or use the [LifeCycle.onInject] callback method.`
47
- );
48
- }
49
- });
50
-
51
- it('should use the calling injector as parent', () => {
52
- class FooService {
53
- value = '1';
54
- }
55
-
56
- @injectable()
57
- class BarService {
58
- foo = inject(FooService);
59
- }
25
+ parent.inject(BarService);
26
+ }, 'BarService is either not injectable or a service is being called in the constructor.');
27
+ });
60
28
 
61
- const parent = new Injector([
62
- {
63
- provide: FooService,
64
- use: class extends FooService {
65
- value = '100';
66
- }
29
+ it('should use the calling injector as parent', () => {
30
+ class FooService {
31
+ value = '1';
32
+ }
33
+
34
+ @injectable()
35
+ class BarService {
36
+ foo = inject(FooService);
37
+ }
38
+
39
+ const parent = new Injector([
40
+ {
41
+ provide: FooService,
42
+ use: class extends FooService {
43
+ value = '100';
67
44
  }
68
- ]);
45
+ }
46
+ ]);
69
47
 
70
- expect(parent.inject(BarService).foo().value).to.equal('100');
71
- });
48
+ assert.strictEqual(parent.inject(BarService).foo().value, '100');
49
+ });
72
50
 
73
- it('should inject a static token', () => {
74
- const TOKEN = new StaticToken('test', () => 'Hello World');
51
+ it('should inject a static token', () => {
52
+ const TOKEN = new StaticToken('test', () => 'Hello World');
75
53
 
76
- @injectable()
77
- class HelloWorld {
78
- hello = inject(TOKEN);
79
- }
54
+ @injectable()
55
+ class HelloWorld {
56
+ hello = inject(TOKEN);
57
+ }
80
58
 
81
- expect(new HelloWorld().hello()).to.equal('Hello World');
82
- });
59
+ assert.strictEqual(new HelloWorld().hello(), 'Hello World');
83
60
  });
package/src/lib/inject.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { InjectionToken } from './provider.js';
2
2
 
3
- import { Injectables } from './injector.js';
3
+ import { injectables } from './injector.js';
4
4
 
5
5
  export type Injected<T> = () => T;
6
6
 
7
7
  export function inject<This extends object, T>(token: InjectionToken<T>): Injected<T> {
8
8
  return function (this: This) {
9
- const injector = Injectables.get(this);
9
+ const injector = injectables.get(this);
10
10
 
11
11
  if (injector === undefined) {
12
12
  const name = Object.getPrototypeOf(this.constructor).name;
@@ -0,0 +1,130 @@
1
+ import { assert } from 'chai';
2
+
3
+ import { inject } from './inject.js';
4
+ import { injectable } from './injectable.js';
5
+
6
+ it('should allow services to be injected into custom element', () => {
7
+ class Foo {}
8
+
9
+ @injectable()
10
+ class MyElement extends HTMLElement {
11
+ foo = inject(Foo);
12
+ }
13
+
14
+ customElements.define('injectable-1', MyElement);
15
+
16
+ const el = new MyElement();
17
+
18
+ assert.instanceOf(el.foo(), Foo);
19
+ });
20
+
21
+ it('should allow services to be injected into custom elements that has been extended', () => {
22
+ class Foo {}
23
+
24
+ class MyBaseElement extends HTMLElement {}
25
+
26
+ @injectable()
27
+ class MyElement extends MyBaseElement {
28
+ foo = inject(Foo);
29
+ }
30
+
31
+ customElements.define('injectable-2', MyElement);
32
+
33
+ const el = new MyElement();
34
+
35
+ assert.instanceOf(el.foo(), Foo);
36
+ });
37
+
38
+ it('should handle parent HTML Injectors', async () => {
39
+ @injectable()
40
+ class A {}
41
+
42
+ @injectable()
43
+ class B {
44
+ a = inject(A);
45
+ }
46
+
47
+ class AltA implements A {}
48
+
49
+ @injectable({
50
+ providers: [
51
+ { provide: B, use: B },
52
+ { provide: A, use: AltA }
53
+ ]
54
+ })
55
+ class Parent extends HTMLElement {}
56
+
57
+ @injectable()
58
+ class Child extends HTMLElement {
59
+ b = inject(B);
60
+ }
61
+
62
+ customElements.define('injectable-parent-1', Parent);
63
+ customElements.define('injectable-child-1', Child);
64
+
65
+ const el = document.createElement('div');
66
+ el.innerHTML = /*html*/ `
67
+ <injectable-parent-1>
68
+ <injectable-child-1></injectable-child-1>
69
+ </injectable-parent-1>
70
+ `;
71
+
72
+ document.body.append(el);
73
+
74
+ const child = el.querySelector<Child>('injectable-child-1')!;
75
+
76
+ assert.instanceOf(child.b().a(), AltA);
77
+
78
+ el.remove();
79
+ });
80
+
81
+ it('should handle changing contexts', async () => {
82
+ class A {}
83
+ class AltA implements A {}
84
+
85
+ @injectable({
86
+ providers: [{ provide: A, use: A }]
87
+ })
88
+ class Ctx1 extends HTMLElement {}
89
+
90
+ @injectable({
91
+ providers: [{ provide: A, use: AltA }]
92
+ })
93
+ class Ctx2 extends HTMLElement {}
94
+
95
+ @injectable()
96
+ class Child extends HTMLElement {
97
+ a = inject(A);
98
+ }
99
+
100
+ customElements.define('ctx-1', Ctx1);
101
+ customElements.define('ctx-2', Ctx2);
102
+ customElements.define('ctx-child', Child);
103
+
104
+ const el = document.createElement('div');
105
+ el.innerHTML = /*html*/ `
106
+ <div>
107
+ <ctx-1>
108
+ <ctx-child></ctx-child>
109
+ </ctx-1>
110
+
111
+ <ctx-2></ctx-2>
112
+ </div>
113
+ `;
114
+
115
+ document.body.append(el);
116
+
117
+ const ctx2 = el.querySelector('ctx-2')!;
118
+
119
+ let child = el.querySelector<Child>('ctx-child')!;
120
+
121
+ assert.instanceOf(child.a(), A);
122
+
123
+ child.remove();
124
+
125
+ ctx2.append(child);
126
+
127
+ child = el.querySelector<Child>('ctx-child')!;
128
+
129
+ assert.instanceOf(child.a(), AltA);
130
+ });
@@ -0,0 +1,63 @@
1
+ import { injectables, Injector } from './injector.js';
2
+ import { ConstructableToken } from './provider.js';
3
+
4
+ export function injectableEl<T extends ConstructableToken<HTMLElement>>(
5
+ Base: T,
6
+ _ctx: ClassDecoratorContext
7
+ ) {
8
+ return class InjectablElement extends Base {
9
+ constructor(..._: any[]) {
10
+ super();
11
+
12
+ /**
13
+ * Listen for the finddiroot event.
14
+ * This is event is triggered when the element is connected to the dom
15
+ * This event will bubble up until it finds a parent injector which is then attached
16
+ * This will also work through shadow roots (that are not "closed")
17
+ */
18
+ this.addEventListener('finddiroot', (e) => {
19
+ const parentInjector = findInjectorRoot(e);
20
+
21
+ if (parentInjector) {
22
+ injectables.get(this)?.setParent(parentInjector);
23
+ }
24
+ });
25
+ }
26
+
27
+ connectedCallback() {
28
+ if (this.isConnected) {
29
+ this.dispatchEvent(new Event('finddiroot', { bubbles: true }));
30
+
31
+ if (super.connectedCallback) {
32
+ super.connectedCallback();
33
+ }
34
+ }
35
+ }
36
+
37
+ disconnectedCallback() {
38
+ injectables.get(this)?.setParent(undefined);
39
+
40
+ if (super.disconnectedCallback) {
41
+ super.disconnectedCallback();
42
+ }
43
+ }
44
+ };
45
+ }
46
+
47
+ function findInjectorRoot(e: Event): Injector | null {
48
+ const path = e.composedPath();
49
+
50
+ // find firt parent
51
+ // skips the first item which is the target
52
+ for (let i = 1; i < path.length; i++) {
53
+ const part = path[i];
54
+
55
+ const injector = injectables.get(part);
56
+
57
+ if (injector) {
58
+ return injector;
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
@@ -1,126 +1,31 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { assert } from 'chai';
2
2
 
3
3
  import { injectable } from './injectable.js';
4
4
  import { inject } from './inject.js';
5
+ import { injectables } from './injector.js';
5
6
 
6
- describe('@injectable()', () => {
7
- it('should allow a custom element to be injected with deps', () => {
8
- class Foo {}
9
- class Bar {}
7
+ it('should locally override a provider', () => {
8
+ class Foo {}
10
9
 
11
- @injectable()
12
- class MyElement extends HTMLElement {
13
- foo = inject(Foo);
14
- bar = inject(Bar);
15
- }
10
+ class Bar extends Foo {}
16
11
 
17
- customElements.define('injectable-1', MyElement);
12
+ @injectable({
13
+ providers: [{ provide: Foo, use: Bar }]
14
+ })
15
+ class MyService {
16
+ foo = inject(Foo);
17
+ }
18
18
 
19
- const el = document.createElement('injectable-1') as MyElement;
19
+ const el = new MyService();
20
20
 
21
- expect(el.foo()).to.be.instanceOf(Foo);
22
- });
23
-
24
- it('should locally override a provider', () => {
25
- class Foo {}
26
-
27
- class Bar extends Foo {}
28
-
29
- @injectable({
30
- providers: [{ provide: Foo, use: Bar }]
31
- })
32
- class MyElement {
33
- foo = inject(Foo);
34
- }
35
-
36
- const el = new MyElement();
37
-
38
- expect(el.foo()).to.be.instanceOf(Bar);
39
- });
40
-
41
- it('should handle parent HTML Injectors', async () => {
42
- @injectable()
43
- class A {}
44
-
45
- @injectable()
46
- class B {
47
- a = inject(A);
48
- }
49
-
50
- class AltA implements A {}
51
-
52
- @injectable({
53
- providers: [
54
- { provide: B, use: B },
55
- { provide: A, use: AltA }
56
- ]
57
- })
58
- class Parent extends HTMLElement {}
59
-
60
- @injectable()
61
- class Child extends HTMLElement {
62
- b = inject(B);
63
- }
64
-
65
- customElements.define('injectable-parent-1', Parent);
66
- customElements.define('injectable-child-1', Child);
67
-
68
- const el = await fixture(html`
69
- <injectable-parent-1>
70
- <injectable-child-1></injectable-child-1>
71
- </injectable-parent-1>
72
- `);
73
-
74
- const child = el.querySelector<Child>('injectable-child-1')!;
75
-
76
- expect(child.b().a()).to.be.instanceOf(AltA);
77
- });
78
-
79
- it('should handle changing contexts', async () => {
80
- class A {}
81
- class AltA implements A {}
82
-
83
- @injectable({
84
- providers: [{ provide: A, use: A }]
85
- })
86
- class Ctx1 extends HTMLElement {}
87
-
88
- @injectable({
89
- providers: [{ provide: A, use: AltA }]
90
- })
91
- class Ctx2 extends HTMLElement {}
92
-
93
- @injectable()
94
- class Child extends HTMLElement {
95
- a = inject(A);
96
- }
97
-
98
- customElements.define('ctx-1', Ctx1);
99
- customElements.define('ctx-2', Ctx2);
100
- customElements.define('ctx-child', Child);
101
-
102
- const el = await fixture(html`
103
- <div>
104
- <ctx-1>
105
- <ctx-child></ctx-child>
106
- </ctx-1>
107
-
108
- <ctx-2></ctx-2>
109
- </div>
110
- `);
111
-
112
- const ctx2 = el.querySelector('ctx-2')!;
113
-
114
- let child = el.querySelector<Child>('ctx-child')!;
115
-
116
- expect(child.a()).to.be.instanceOf(A);
117
-
118
- child.remove();
21
+ assert.instanceOf(el.foo(), Bar);
22
+ });
119
23
 
120
- ctx2.append(child);
24
+ it('should define an injector for a service instance', () => {
25
+ @injectable()
26
+ class MyService {}
121
27
 
122
- child = el.querySelector<Child>('ctx-child')!;
28
+ const instance = new MyService();
123
29
 
124
- expect(child.a()).to.be.instanceOf(AltA);
125
- });
30
+ assert.ok(injectables.has(instance));
126
31
  });
@@ -1,73 +1,31 @@
1
1
  import { ConstructableToken, Provider } from './provider.js';
2
- import { Injectables, Injector } from './injector.js';
2
+ import { injectables, Injector } from './injector.js';
3
+ import { injectableEl } from './injectable-el.js';
3
4
 
4
5
  export interface InjectableOpts {
5
- providers: Provider<unknown>[];
6
+ providers?: Provider<unknown>[];
6
7
  }
7
8
 
8
9
  export function injectable(opts?: InjectableOpts) {
9
- return function injectableDecorator<T extends ConstructableToken<any>>(Base: T, _?: unknown) {
10
- return class InjectableNode extends Base {
10
+ return function injectableDecorator<T extends ConstructableToken<any>>(
11
+ Base: T,
12
+ ctx: ClassDecoratorContext
13
+ ) {
14
+ class Injectable extends Base {
11
15
  constructor(..._: any[]) {
12
16
  super();
13
17
 
14
- // Define a new Injector and assiciate it with this instance of the service
15
- const injector = new Injector(opts?.providers);
16
-
17
- Injectables.set(this, injector);
18
-
19
- // If the current injectable instance is a HTMLElement preform additional startup logic
20
- // this will find and attach parent injectors
21
- if ('HTMLElement' in globalThis && this instanceof HTMLElement) {
22
- this.addEventListener('finddiroot', (e) => {
23
- const parentInjector = findInjectorRoot(e);
24
-
25
- if (parentInjector !== null) {
26
- injector.setParent(parentInjector);
27
- }
28
- });
29
- }
30
- }
31
-
32
- connectedCallback() {
33
- if ('HTMLElement' in globalThis && this instanceof HTMLElement) {
34
- this.dispatchEvent(new Event('finddiroot'));
35
-
36
- if (super.connectedCallback) {
37
- super.connectedCallback();
38
- }
39
- }
18
+ injectables.set(this, new Injector(opts?.providers));
40
19
  }
20
+ }
41
21
 
42
- disconnectedCallback() {
43
- const injector = Injectables.get(this);
44
-
45
- if (injector) {
46
- injector.setParent(undefined);
47
- }
48
-
49
- if (super.disconnectedCallback) {
50
- super.disconnectedCallback();
51
- }
22
+ // Only apply custom element bootstrap logic if the decorated class is an HTMLElement
23
+ if ('HTMLElement' in globalThis) {
24
+ if (HTMLElement.prototype.isPrototypeOf(Base.prototype)) {
25
+ return injectableEl(Injectable, ctx);
52
26
  }
53
- };
54
- };
55
- }
56
-
57
- function findInjectorRoot(e: Event): Injector | null {
58
- const path = e.composedPath();
59
-
60
- // find firt parent
61
- // skips the first item which is the target
62
- for (let i = 1; i < path.length; i++) {
63
- const part = path[i];
64
-
65
- const injector = Injectables.get(part);
66
-
67
- if (injector) {
68
- return injector;
69
27
  }
70
- }
71
28
 
72
- return null;
29
+ return Injectable;
30
+ };
73
31
  }