@joist/di 4.0.0-next.1 → 4.0.0-next.11

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 (66) hide show
  1. package/README.md +52 -34
  2. package/package.json +4 -14
  3. package/src/lib/dom-injector.test.ts +16 -18
  4. package/src/lib/dom-injector.ts +3 -4
  5. package/src/lib/inject.test.ts +33 -56
  6. package/src/lib/inject.ts +4 -6
  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 +19 -116
  10. package/src/lib/injectable.ts +21 -57
  11. package/src/lib/injector.test.ts +132 -130
  12. package/src/lib/injector.ts +33 -19
  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 +16 -8
  17. package/src/lib.ts +1 -1
  18. package/target/lib/dom-injector.js +3 -4
  19. package/target/lib/dom-injector.js.map +1 -1
  20. package/target/lib/dom-injector.test.js +16 -18
  21. package/target/lib/dom-injector.test.js.map +1 -1
  22. package/target/lib/inject.js +3 -3
  23. package/target/lib/inject.js.map +1 -1
  24. package/target/lib/inject.test.js +58 -90
  25. package/target/lib/inject.test.js.map +1 -1
  26. package/target/lib/injectable-el.d.ts +334 -0
  27. package/target/lib/injectable-el.js +40 -0
  28. package/target/lib/injectable-el.js.map +1 -0
  29. package/target/lib/injectable-el.test.js +238 -0
  30. package/target/lib/injectable-el.test.js.map +1 -0
  31. package/target/lib/injectable.d.ts +5 -7
  32. package/target/lib/injectable.js +13 -42
  33. package/target/lib/injectable.js.map +1 -1
  34. package/target/lib/injectable.test.js +49 -204
  35. package/target/lib/injectable.test.js.map +1 -1
  36. package/target/lib/injector.d.ts +1 -1
  37. package/target/lib/injector.js +18 -14
  38. package/target/lib/injector.js.map +1 -1
  39. package/target/lib/injector.test.js +215 -216
  40. package/target/lib/injector.test.js.map +1 -1
  41. package/target/lib/lifecycle.d.ts +2 -4
  42. package/target/lib/lifecycle.js +15 -4
  43. package/target/lib/lifecycle.js.map +1 -1
  44. package/target/lib/lifecycle.test.js +142 -123
  45. package/target/lib/lifecycle.test.js.map +1 -1
  46. package/target/lib/metadata.d.ts +6 -0
  47. package/target/lib/metadata.js +5 -0
  48. package/target/lib/metadata.js.map +1 -0
  49. package/target/lib/provider.d.ts +6 -5
  50. package/target/lib/provider.js +10 -4
  51. package/target/lib/provider.js.map +1 -1
  52. package/target/lib.d.ts +1 -1
  53. package/target/lib.js +1 -1
  54. package/target/lib.js.map +1 -1
  55. package/src/lib/injectable-map.test.ts +0 -18
  56. package/src/lib/injectable-map.ts +0 -3
  57. package/src/lib/injector.test-node.ts +0 -187
  58. package/target/lib/injectable-map.d.ts +0 -3
  59. package/target/lib/injectable-map.js +0 -3
  60. package/target/lib/injectable-map.js.map +0 -1
  61. package/target/lib/injectable-map.test.js +0 -15
  62. package/target/lib/injectable-map.test.js.map +0 -1
  63. package/target/lib/injector.test-node.d.ts +0 -1
  64. package/target/lib/injector.test-node.js +0 -231
  65. package/target/lib/injector.test-node.js.map +0 -1
  66. /package/target/lib/{injectable-map.test.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
@@ -53,7 +53,7 @@ Singleton services are great but the real benefit can be seen when passing insta
53
53
  `inject()` returns a function that will then return an instance of the requested service. This means that services are only created when they are needed and not when the class is constructed.
54
54
 
55
55
  ```ts
56
- @injectable
56
+ @injectable()
57
57
  class App {
58
58
  #counter = inject(Counter);
59
59
 
@@ -80,7 +80,7 @@ class HttpService {
80
80
  }
81
81
  }
82
82
 
83
- @injectable
83
+ @injectable()
84
84
  class ApiService {
85
85
  #http = inject(HttpService);
86
86
 
@@ -114,7 +114,7 @@ test('should return json', async () => {
114
114
 
115
115
  ### Service level providers
116
116
 
117
- Under the hood, each service decorated with `@injectable` creates its own injector. This means that it is possible to defined providers from that level down.
117
+ Under the hood, each service decorated with `@injectable()` creates its own injector. This means that it is possible to defined providers from that level down.
118
118
 
119
119
  The below example will use this particular instance of Logger as wall as any other services injected into this service.
120
120
 
@@ -129,10 +129,10 @@ class ConsoleLogger implements Logger {
129
129
  }
130
130
  }
131
131
 
132
- @injectable
133
- class MyService {
134
- static providers = [{ provide: Logger, use: ConsoleLogger }];
135
- }
132
+ @injectable({
133
+ providers: [{ provide: Logger, use: ConsoleLogger }]
134
+ })
135
+ class MyService {}
136
136
  ```
137
137
 
138
138
  ### Factories
@@ -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
@@ -286,7 +316,7 @@ class Colors {
286
316
  secodnary = 'green';
287
317
  }
288
318
 
289
- @injectable
319
+ @injectable()
290
320
  class MyElement extends HTMLElement {
291
321
  #colors = inject(Colors);
292
322
 
@@ -314,21 +344,20 @@ class Colors {
314
344
  secodnary = 'green';
315
345
  }
316
346
 
317
- @injectable
318
- class ColorCtx extends HTMLElement {
319
- // services can be scoped to a particular injectable
320
- static providers = [
347
+ @injectable({
348
+ providers: [
321
349
  {
322
350
  provide: Colors,
323
351
  use: class implements Colors {
324
352
  primary = 'orange';
325
353
  secondary = 'purple';
326
- },
327
- },
354
+ }
355
+ }
328
356
  ]
329
- }
357
+ })
358
+ class ColorCtx extends HTMLElement {}
330
359
 
331
- @injectable
360
+ @injectable()
332
361
  class MyElement extends HTMLElement {
333
362
  #colors = inject(Colors);
334
363
 
@@ -353,14 +382,3 @@ customElements.define('my-element', MyElement);
353
382
  <my-element></my-element>
354
383
  </color-ctx>
355
384
  ```
356
-
357
- ### Environment
358
-
359
- When using @joist/di with custom elements a default root injector is created dubbed 'environment'. This is the injector that all other injectors will eventually stop at.
360
- If you need to define something in this environment you can do so with the `defineEnvironment` method.
361
-
362
- ```ts
363
- import { defineEnvironment } from '@joist/di';
364
-
365
- defineEnvironment([{ provide: MyService, use: SomeOtherService }]);
366
- ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/di",
3
- "version": "4.0.0-next.1",
3
+ "version": "4.0.0-next.11",
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 { INJECTABLE_MAP } from './injectable.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 = INJECTABLE_MAP.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(INJECTABLE_MAP.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(INJECTABLE_MAP.get(root)).to.equal(undefined);
29
- });
27
+ assert.strictEqual(injectables.get(root), undefined);
30
28
  });
@@ -1,12 +1,11 @@
1
- import { INJECTABLE_MAP } from './injectable.js';
2
- import { Injector } from './injector.js';
1
+ import { injectables, Injector } from './injector.js';
3
2
 
4
3
  export class DOMInjector extends Injector {
5
4
  attach(root: HTMLElement) {
6
- INJECTABLE_MAP.set(root, this);
5
+ injectables.set(root, this);
7
6
  }
8
7
 
9
8
  detach(root: HTMLElement) {
10
- INJECTABLE_MAP.delete(root);
9
+ injectables.delete(root);
11
10
  }
12
11
  }
@@ -1,30 +1,17 @@
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
  }
26
13
 
27
- @injectable
14
+ @injectable()
28
15
  class BarService {
29
16
  foo = inject(FooService);
30
17
 
@@ -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,19 +1,17 @@
1
1
  import { InjectionToken } from './provider.js';
2
- import { INJECTABLE_MAP } from './injectable.js';
2
+ import { injectables } from './injector.js';
3
3
 
4
4
  export type Injected<T> = () => T;
5
5
 
6
- export function inject<This extends object, T>(
7
- token: InjectionToken<T>
8
- ): Injected<T> {
6
+ export function inject<This extends object, T>(token: InjectionToken<T>): Injected<T> {
9
7
  return function (this: This) {
10
- const injector = INJECTABLE_MAP.get(this);
8
+ const injector = injectables.get(this);
11
9
 
12
10
  if (injector === undefined) {
13
11
  const name = Object.getPrototypeOf(this.constructor).name;
14
12
 
15
13
  throw new Error(
16
- `${name} 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.`
14
+ `${name} 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.`
17
15
  );
18
16
  }
19
17
 
@@ -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
+ }