@joist/di 4.0.0-next.2 → 4.0.0-next.21

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 (56) hide show
  1. package/README.md +37 -7
  2. package/package.json +4 -14
  3. package/src/lib/dom-injector.test.ts +16 -18
  4. package/src/lib/dom-injector.ts +3 -3
  5. package/src/lib/inject.test.ts +32 -55
  6. package/src/lib/inject.ts +2 -3
  7. package/src/lib/injectable-el.test.ts +130 -0
  8. package/src/lib/injectable-el.ts +63 -0
  9. package/src/lib/injectable.test.ts +22 -114
  10. package/src/lib/injectable.ts +21 -61
  11. package/src/lib/injector.test.ts +132 -130
  12. package/src/lib/injector.ts +28 -14
  13. package/src/lib/lifecycle.test.ts +68 -64
  14. package/src/lib/lifecycle.ts +19 -4
  15. package/src/lib/metadata.ts +12 -0
  16. package/src/lib/provider.ts +4 -4
  17. package/src/lib.ts +1 -1
  18. package/target/lib/dom-injector.js +3 -3
  19. package/target/lib/dom-injector.test.js +16 -18
  20. package/target/lib/dom-injector.test.js.map +1 -1
  21. package/target/lib/inject.js +2 -2
  22. package/target/lib/inject.js.map +1 -1
  23. package/target/lib/inject.test.js +57 -89
  24. package/target/lib/inject.test.js.map +1 -1
  25. package/target/lib/injectable-el.d.ts +341 -0
  26. package/target/lib/injectable-el.js +40 -0
  27. package/target/lib/injectable-el.js.map +1 -0
  28. package/target/lib/injectable-el.test.js +238 -0
  29. package/target/lib/injectable-el.test.js.map +1 -0
  30. package/target/lib/injectable.d.ts +3 -6
  31. package/target/lib/injectable.js +14 -43
  32. package/target/lib/injectable.js.map +1 -1
  33. package/target/lib/injectable.test.js +55 -233
  34. package/target/lib/injectable.test.js.map +1 -1
  35. package/target/lib/injector.d.ts +1 -1
  36. package/target/lib/injector.js +18 -11
  37. package/target/lib/injector.js.map +1 -1
  38. package/target/lib/injector.test.js +215 -216
  39. package/target/lib/injector.test.js.map +1 -1
  40. package/target/lib/lifecycle.d.ts +2 -4
  41. package/target/lib/lifecycle.js +15 -4
  42. package/target/lib/lifecycle.js.map +1 -1
  43. package/target/lib/lifecycle.test.js +142 -123
  44. package/target/lib/lifecycle.test.js.map +1 -1
  45. package/target/lib/metadata.d.ts +6 -0
  46. package/target/lib/metadata.js +5 -0
  47. package/target/lib/metadata.js.map +1 -0
  48. package/target/lib/provider.d.ts +4 -4
  49. package/target/lib/provider.js.map +1 -1
  50. package/target/lib.d.ts +1 -1
  51. package/target/lib.js +1 -1
  52. package/target/lib.js.map +1 -1
  53. package/src/lib/injector.test-node.ts +0 -187
  54. package/target/lib/injector.test-node.js +0 -231
  55. package/target/lib/injector.test-node.js.map +0 -1
  56. /package/target/lib/{injector.test-node.d.ts → injectable-el.test.d.ts} +0 -0
package/README.md CHANGED
@@ -16,10 +16,10 @@ Allows you to inject services into other class instances (including custom eleme
16
16
  - [Hierarchical Injectors](#hierarchical-injectors)
17
17
  - [Custom Elements](#custom-elements)
18
18
 
19
- ## Installation:
19
+ ## Installation
20
20
 
21
21
  ```BASH
22
- npm i @joist/di
22
+ npm i @joist/di@next
23
23
  ```
24
24
 
25
25
  ## Injectors
@@ -228,17 +228,43 @@ const app = new Injector();
228
228
  const url: string = await app.inject(URL_TOKEN);
229
229
  ```
230
230
 
231
+ This allows you to dynamically import services
232
+
233
+ ```ts
234
+ const HTTP_SERVICE = new StaticToken('HTTP_SERVICE', () => {
235
+ return import('./http.service.js').then((m) => new m.HttpService());
236
+ });
237
+
238
+ class HackerNewsService {
239
+ #http = inject(HTTP_SERVICE);
240
+
241
+ async getData() {
242
+ const http = await this.#http();
243
+
244
+ const url = new URL('https://hacker-news.firebaseio.com/v0/beststories.json');
245
+ url.searchParams.set('limitToFirst', count.toString());
246
+ url.searchParams.set('orderBy', '"$key"');
247
+
248
+ return http.fetchJson<string[]>(url);
249
+ }
250
+ }
251
+
252
+ const url: string = await app.inject(URL_TOKEN);
253
+ ```
254
+
231
255
  ## LifeCycle
232
256
 
233
257
  To help provide more information to services that are being created, joist will call several life cycle hooks as services are created. These hooks are defined using the provided symbols so there is no risk of naming colisions.
234
258
 
235
259
  ```ts
236
260
  class MyService {
237
- [LifeCycle.onInit]() {
261
+ @created()
262
+ onCreated() {
238
263
  // called the first time a service is created. (not pulled from cache)
239
264
  }
240
265
 
241
- [LifeCycle.onInject]() {
266
+ @injected()
267
+ onInjected() {
242
268
  // called every time a service is returned, whether it is from cache or not
243
269
  }
244
270
  }
@@ -246,12 +272,16 @@ class MyService {
246
272
 
247
273
  ## Hierarchical Injectors
248
274
 
249
- Injectors can be defined with a parent element. The top most parent will (by default) be where services are constructed and cached. Only if manually defined providers are found earlier in the chain will services be constructed lower. The injector resolution algorithm behaves as following.
275
+ Injectors can be defined with a parent. The top most parent will (by default) be where services are constructed and cached. Only if manually defined providers are found earlier in the chain will services be constructed lower. The injector resolution algorithm behaves as following.
250
276
 
251
277
  1. Do I have a cached instance locally?
252
278
  2. Do I have a local provider definition for the token?
253
- 3. Do I have a parent? Check parent for 1 and 2
254
- 4. All clear, go ahead and construct and cache the requested service
279
+ 3. Do I have a parent?
280
+ 4. Does parent have a local instance or provider definition?
281
+ 5. If parent exists but no instance found, create instance in parent.
282
+ 6. If not parent, All clear, go ahead and construct and cache the requested service.
283
+
284
+ Having injectors resolve this way means that all children have access to services created by their parents.
255
285
 
256
286
  ```mermaid
257
287
  graph TD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/di",
3
- "version": "4.0.0-next.2",
3
+ "version": "4.0.0-next.21",
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,11 @@
1
1
  import { InjectionToken } from './provider.js';
2
-
3
- import { Injectables } from './injector.js';
2
+ import { injectables } from './injector.js';
4
3
 
5
4
  export type Injected<T> = () => T;
6
5
 
7
6
  export function inject<This extends object, T>(token: InjectionToken<T>): Injected<T> {
8
7
  return function (this: This) {
9
- const injector = Injectables.get(this);
8
+ const injector = injectables.get(this);
10
9
 
11
10
  if (injector === undefined) {
12
11
  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,34 @@
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 {
27
+ constructor(public arg = 'a') {}
28
+ }
121
29
 
122
- child = el.querySelector<Child>('ctx-child')!;
30
+ const instance = new MyService('b');
123
31
 
124
- expect(child.a()).to.be.instanceOf(AltA);
125
- });
32
+ assert.ok(injectables.has(instance));
33
+ assert.ok(instance.arg === 'b');
126
34
  });