@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.
- package/README.md +52 -34
- package/package.json +4 -14
- package/src/lib/dom-injector.test.ts +16 -18
- package/src/lib/dom-injector.ts +3 -4
- package/src/lib/inject.test.ts +33 -56
- package/src/lib/inject.ts +4 -6
- package/src/lib/injectable-el.test.ts +130 -0
- package/src/lib/injectable-el.ts +63 -0
- package/src/lib/injectable.test.ts +19 -116
- package/src/lib/injectable.ts +21 -57
- package/src/lib/injector.test.ts +132 -130
- package/src/lib/injector.ts +33 -19
- package/src/lib/lifecycle.test.ts +68 -64
- package/src/lib/lifecycle.ts +19 -4
- package/src/lib/metadata.ts +12 -0
- package/src/lib/provider.ts +16 -8
- package/src/lib.ts +1 -1
- package/target/lib/dom-injector.js +3 -4
- package/target/lib/dom-injector.js.map +1 -1
- package/target/lib/dom-injector.test.js +16 -18
- package/target/lib/dom-injector.test.js.map +1 -1
- package/target/lib/inject.js +3 -3
- package/target/lib/inject.js.map +1 -1
- package/target/lib/inject.test.js +58 -90
- package/target/lib/inject.test.js.map +1 -1
- package/target/lib/injectable-el.d.ts +334 -0
- package/target/lib/injectable-el.js +40 -0
- package/target/lib/injectable-el.js.map +1 -0
- package/target/lib/injectable-el.test.js +238 -0
- package/target/lib/injectable-el.test.js.map +1 -0
- package/target/lib/injectable.d.ts +5 -7
- package/target/lib/injectable.js +13 -42
- package/target/lib/injectable.js.map +1 -1
- package/target/lib/injectable.test.js +49 -204
- package/target/lib/injectable.test.js.map +1 -1
- package/target/lib/injector.d.ts +1 -1
- package/target/lib/injector.js +18 -14
- package/target/lib/injector.js.map +1 -1
- package/target/lib/injector.test.js +215 -216
- package/target/lib/injector.test.js.map +1 -1
- package/target/lib/lifecycle.d.ts +2 -4
- package/target/lib/lifecycle.js +15 -4
- package/target/lib/lifecycle.js.map +1 -1
- package/target/lib/lifecycle.test.js +142 -123
- package/target/lib/lifecycle.test.js.map +1 -1
- package/target/lib/metadata.d.ts +6 -0
- package/target/lib/metadata.js +5 -0
- package/target/lib/metadata.js.map +1 -0
- package/target/lib/provider.d.ts +6 -5
- package/target/lib/provider.js +10 -4
- package/target/lib/provider.js.map +1 -1
- package/target/lib.d.ts +1 -1
- package/target/lib.js +1 -1
- package/target/lib.js.map +1 -1
- package/src/lib/injectable-map.test.ts +0 -18
- package/src/lib/injectable-map.ts +0 -3
- package/src/lib/injector.test-node.ts +0 -187
- package/target/lib/injectable-map.d.ts +0 -3
- package/target/lib/injectable-map.js +0 -3
- package/target/lib/injectable-map.js.map +0 -1
- package/target/lib/injectable-map.test.js +0 -15
- package/target/lib/injectable-map.test.js.map +0 -1
- package/target/lib/injector.test-node.d.ts +0 -1
- package/target/lib/injector.test-node.js +0 -231
- package/target/lib/injector.test-node.js.map +0 -1
- /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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
261
|
+
@created()
|
|
262
|
+
onCreated() {
|
|
238
263
|
// called the first time a service is created. (not pulled from cache)
|
|
239
264
|
}
|
|
240
265
|
|
|
241
|
-
|
|
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
|
|
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?
|
|
254
|
-
4.
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
-
"
|
|
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 {
|
|
1
|
+
import { assert } from 'chai';
|
|
2
2
|
|
|
3
3
|
import { DOMInjector } from './dom-injector.js';
|
|
4
|
-
import {
|
|
4
|
+
import { injectables } from './injector.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
app.attach(root);
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
const injector = injectables.get(root);
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
assert.strictEqual(injector, app);
|
|
15
|
+
});
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
21
|
+
app.attach(root);
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
assert.strictEqual(injectables.get(root), app);
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
app.detach(root);
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
});
|
|
27
|
+
assert.strictEqual(injectables.get(root), undefined);
|
|
30
28
|
});
|
package/src/lib/dom-injector.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
5
|
+
injectables.set(root, this);
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
detach(root: HTMLElement) {
|
|
10
|
-
|
|
9
|
+
injectables.delete(root);
|
|
11
10
|
}
|
|
12
11
|
}
|
package/src/lib/inject.test.ts
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
48
|
+
assert.strictEqual(parent.inject(BarService).foo().value, '100');
|
|
49
|
+
});
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
51
|
+
it('should inject a static token', () => {
|
|
52
|
+
const TOKEN = new StaticToken('test', () => 'Hello World');
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
54
|
+
@injectable()
|
|
55
|
+
class HelloWorld {
|
|
56
|
+
hello = inject(TOKEN);
|
|
57
|
+
}
|
|
80
58
|
|
|
81
|
-
|
|
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 {
|
|
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 =
|
|
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
|
+
}
|