@joist/di 4.0.1-next.0 → 4.0.1
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 +87 -59
- package/package.json +7 -20
- package/src/lib/context/injector.ts +6 -0
- package/src/lib/context/protocol.ts +78 -0
- package/src/lib/dom-injector.test.ts +51 -17
- package/src/lib/dom-injector.ts +48 -6
- package/src/lib/inject.test.ts +50 -59
- package/src/lib/inject.ts +6 -8
- package/src/lib/injectable-el.test.ts +130 -0
- package/src/lib/injectable-el.ts +59 -0
- package/src/lib/injectable.test.ts +51 -109
- package/src/lib/injectable.ts +51 -60
- package/src/lib/injector.test.ts +153 -137
- package/src/lib/injector.ts +55 -64
- package/src/lib/lifecycle.test.ts +102 -68
- package/src/lib/lifecycle.ts +36 -4
- package/src/lib/metadata.ts +29 -0
- package/src/lib/provider.ts +27 -13
- package/src/lib.ts +11 -6
- package/target/lib/context/injector.d.ts +3 -0
- package/target/lib/context/injector.js +3 -0
- package/target/lib/context/injector.js.map +1 -0
- package/target/lib/context/protocol.d.ts +18 -0
- package/target/lib/context/protocol.js +15 -0
- package/target/lib/context/protocol.js.map +1 -0
- package/target/lib/dom-injector.d.ts +5 -3
- package/target/lib/dom-injector.js +30 -6
- package/target/lib/dom-injector.js.map +1 -1
- package/target/lib/dom-injector.test.js +39 -17
- package/target/lib/dom-injector.test.js.map +1 -1
- package/target/lib/inject.d.ts +1 -1
- package/target/lib/inject.js +4 -5
- package/target/lib/inject.js.map +1 -1
- package/target/lib/inject.test.js +74 -95
- package/target/lib/inject.test.js.map +1 -1
- package/target/lib/injectable-el.d.ts +6 -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 +7 -11
- package/target/lib/injectable.js +26 -42
- package/target/lib/injectable.js.map +1 -1
- package/target/lib/injectable.test.js +126 -206
- package/target/lib/injectable.test.js.map +1 -1
- package/target/lib/injector.d.ts +11 -6
- package/target/lib/injector.js +29 -49
- package/target/lib/injector.js.map +1 -1
- package/target/lib/injector.test.js +232 -218
- package/target/lib/injector.test.js.map +1 -1
- package/target/lib/lifecycle.d.ts +5 -4
- package/target/lib/lifecycle.js +21 -4
- package/target/lib/lifecycle.js.map +1 -1
- package/target/lib/lifecycle.test.js +187 -125
- package/target/lib/lifecycle.test.js.map +1 -1
- package/target/lib/metadata.d.ts +9 -0
- package/target/lib/metadata.js +15 -0
- package/target/lib/metadata.js.map +1 -0
- package/target/lib/provider.d.ts +13 -10
- package/target/lib/provider.js +11 -4
- package/target/lib/provider.js.map +1 -1
- package/target/lib.d.ts +6 -6
- package/target/lib.js +6 -6
- 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
|
|
|
@@ -102,7 +102,9 @@ test('should return json', async () => {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const app = new Injector(
|
|
105
|
+
const app = new Injector({
|
|
106
|
+
providers: [[HttpService, { use: MockHttpService }]]
|
|
107
|
+
});
|
|
106
108
|
const api = app.inject(ApiService);
|
|
107
109
|
|
|
108
110
|
const res = await api.getData();
|
|
@@ -114,7 +116,7 @@ test('should return json', async () => {
|
|
|
114
116
|
|
|
115
117
|
### Service level providers
|
|
116
118
|
|
|
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.
|
|
119
|
+
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
120
|
|
|
119
121
|
The below example will use this particular instance of Logger as wall as any other services injected into this service.
|
|
120
122
|
|
|
@@ -129,10 +131,10 @@ class ConsoleLogger implements Logger {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
@injectable
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
134
|
+
@injectable({
|
|
135
|
+
providers: [[Logger, { use: ConsoleLogger }]]
|
|
136
|
+
})
|
|
137
|
+
class MyService {}
|
|
136
138
|
```
|
|
137
139
|
|
|
138
140
|
### Factories
|
|
@@ -180,14 +182,16 @@ class Feature {
|
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
const app = new Injector([
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
[
|
|
186
|
+
Feature,
|
|
187
|
+
{
|
|
188
|
+
factory(i) {
|
|
189
|
+
const logger = i.inject(Logger);
|
|
187
190
|
|
|
188
|
-
|
|
191
|
+
return new Feature(logger);
|
|
192
|
+
}
|
|
189
193
|
}
|
|
190
|
-
|
|
194
|
+
]
|
|
191
195
|
]);
|
|
192
196
|
```
|
|
193
197
|
|
|
@@ -200,10 +204,12 @@ In most cases a token is any constructable class. There are cases where you migh
|
|
|
200
204
|
const URL_TOKEN = new StaticToken<string>('app_url');
|
|
201
205
|
|
|
202
206
|
const app = new Injector([
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
[
|
|
208
|
+
URL_TOKEN,
|
|
209
|
+
{
|
|
210
|
+
factory: () => '/my-app-url/'
|
|
211
|
+
}
|
|
212
|
+
]
|
|
207
213
|
]);
|
|
208
214
|
```
|
|
209
215
|
|
|
@@ -228,17 +234,43 @@ const app = new Injector();
|
|
|
228
234
|
const url: string = await app.inject(URL_TOKEN);
|
|
229
235
|
```
|
|
230
236
|
|
|
237
|
+
This allows you to dynamically import services
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const HttpService = new StaticToken('HTTP_SERVICE', () => {
|
|
241
|
+
return import('./http.service.js').then((m) => new m.HttpService());
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
class HackerNewsService {
|
|
245
|
+
#http = inject(HttpService);
|
|
246
|
+
|
|
247
|
+
async getData() {
|
|
248
|
+
const http = await this.#http();
|
|
249
|
+
|
|
250
|
+
const url = new URL('https://hacker-news.firebaseio.com/v0/beststories.json');
|
|
251
|
+
url.searchParams.set('limitToFirst', count.toString());
|
|
252
|
+
url.searchParams.set('orderBy', '"$key"');
|
|
253
|
+
|
|
254
|
+
return http.fetchJson<string[]>(url);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const url: string = await app.inject(URL_TOKEN);
|
|
259
|
+
```
|
|
260
|
+
|
|
231
261
|
## LifeCycle
|
|
232
262
|
|
|
233
263
|
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
264
|
|
|
235
265
|
```ts
|
|
236
266
|
class MyService {
|
|
237
|
-
|
|
267
|
+
@created()
|
|
268
|
+
onCreated() {
|
|
238
269
|
// called the first time a service is created. (not pulled from cache)
|
|
239
270
|
}
|
|
240
271
|
|
|
241
|
-
|
|
272
|
+
@injected()
|
|
273
|
+
onInjected() {
|
|
242
274
|
// called every time a service is returned, whether it is from cache or not
|
|
243
275
|
}
|
|
244
276
|
}
|
|
@@ -246,12 +278,16 @@ class MyService {
|
|
|
246
278
|
|
|
247
279
|
## Hierarchical Injectors
|
|
248
280
|
|
|
249
|
-
Injectors can be defined with a parent
|
|
281
|
+
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
282
|
|
|
251
283
|
1. Do I have a cached instance locally?
|
|
252
284
|
2. Do I have a local provider definition for the token?
|
|
253
|
-
3. Do I have a parent?
|
|
254
|
-
4.
|
|
285
|
+
3. Do I have a parent?
|
|
286
|
+
4. Does parent have a local instance or provider definition?
|
|
287
|
+
5. If parent exists but no instance found, create instance in parent.
|
|
288
|
+
6. If not parent, All clear, go ahead and construct and cache the requested service.
|
|
289
|
+
|
|
290
|
+
Having injectors resolve this way means that all children have access to services created by their parents.
|
|
255
291
|
|
|
256
292
|
```mermaid
|
|
257
293
|
graph TD
|
|
@@ -286,7 +322,7 @@ class Colors {
|
|
|
286
322
|
secodnary = 'green';
|
|
287
323
|
}
|
|
288
324
|
|
|
289
|
-
@injectable
|
|
325
|
+
@injectable()
|
|
290
326
|
class MyElement extends HTMLElement {
|
|
291
327
|
#colors = inject(Colors);
|
|
292
328
|
|
|
@@ -302,35 +338,38 @@ customElements.define('my-element', MyElement);
|
|
|
302
338
|
|
|
303
339
|
### Context Elements:
|
|
304
340
|
|
|
305
|
-
Context elements are where Hierarchical Injectors can really shine as they allow you to defined React/Preact esq "context" elements.
|
|
341
|
+
Context elements are where Hierarchical Injectors can really shine as they allow you to defined React/Preact esq "context" elements.
|
|
342
|
+
Since custom elements are treated the same as any other class they can define providers for their local scope. The `provideSelfAs` property will provide the current class for the tokens given.
|
|
343
|
+
This also makes it easy to attributes to define values for the service.
|
|
306
344
|
|
|
307
345
|
```TS
|
|
308
346
|
const app = new DOMInjector();
|
|
309
347
|
|
|
310
348
|
app.attach(document.body);
|
|
311
349
|
|
|
312
|
-
|
|
313
|
-
primary
|
|
314
|
-
|
|
350
|
+
interface ColorCtx {
|
|
351
|
+
primary: string;
|
|
352
|
+
secondary: string;
|
|
315
353
|
}
|
|
316
354
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
355
|
+
const COLOR_CTX = new StaticToken<ColorCtx>('COLOR_CTX')
|
|
356
|
+
|
|
357
|
+
@injectable({
|
|
358
|
+
provideSelfAs: [COLOR_CTX]
|
|
359
|
+
})
|
|
360
|
+
class ColorCtx extends HTMLElement implements ColorCtx {
|
|
361
|
+
get primary() {
|
|
362
|
+
return this.getAttribute("primary") ?? "red"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
get secondary() {
|
|
366
|
+
return this.getAttribute("secondary") ?? "green"
|
|
367
|
+
}
|
|
329
368
|
}
|
|
330
369
|
|
|
331
|
-
@injectable
|
|
370
|
+
@injectable()
|
|
332
371
|
class MyElement extends HTMLElement {
|
|
333
|
-
#colors = inject(
|
|
372
|
+
#colors = inject(COLOR_CTX);
|
|
334
373
|
|
|
335
374
|
connectedCallback() {
|
|
336
375
|
const { primary } = this.#colors();
|
|
@@ -339,28 +378,17 @@ class MyElement extends HTMLElement {
|
|
|
339
378
|
}
|
|
340
379
|
}
|
|
341
380
|
|
|
342
|
-
// Note: To use parent providers, the parent elements need to be defined first
|
|
381
|
+
// Note: To use parent providers, the parent elements need to be defined first!
|
|
343
382
|
customElements.define('color-ctx', ColorCtx);
|
|
344
383
|
customElements.define('my-element', MyElement);
|
|
345
384
|
```
|
|
346
385
|
|
|
347
386
|
```HTML
|
|
348
|
-
<!--
|
|
387
|
+
<!-- Error: No colors provided -->
|
|
349
388
|
<my-element></my-element>
|
|
350
389
|
|
|
351
|
-
<!--
|
|
352
|
-
<color-ctx>
|
|
390
|
+
<!-- colors come from ctx -->
|
|
391
|
+
<color-ctx primary="orange" secondard="blue">
|
|
353
392
|
<my-element></my-element>
|
|
354
393
|
</color-ctx>
|
|
355
394
|
```
|
|
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,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joist/di",
|
|
3
|
-
"version": "4.0.1
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./target/lib.js",
|
|
6
6
|
"module": "./target/lib.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"./*": {
|
|
12
|
-
"import": "./target/lib/*"
|
|
13
|
-
}
|
|
8
|
+
".": "./target/lib.js",
|
|
9
|
+
"./*": "./target/lib/*",
|
|
10
|
+
"./package.json": "./package.json"
|
|
14
11
|
},
|
|
15
12
|
"files": [
|
|
16
13
|
"src",
|
|
@@ -20,7 +17,7 @@
|
|
|
20
17
|
"description": "Dependency Injection for Vanilla JS classes",
|
|
21
18
|
"repository": {
|
|
22
19
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/
|
|
20
|
+
"url": "git+https://github.com/joist-framework/joist.git"
|
|
24
21
|
},
|
|
25
22
|
"keywords": [
|
|
26
23
|
"TypeScript",
|
|
@@ -31,7 +28,7 @@
|
|
|
31
28
|
"author": "deebloo",
|
|
32
29
|
"license": "MIT",
|
|
33
30
|
"bugs": {
|
|
34
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/joist-framework/joist/issues"
|
|
35
32
|
},
|
|
36
33
|
"publishConfig": {
|
|
37
34
|
"access": "public"
|
|
@@ -57,17 +54,7 @@
|
|
|
57
54
|
"test": {
|
|
58
55
|
"command": "wtr --config wtr.config.mjs",
|
|
59
56
|
"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": [
|
|
57
|
+
"wtr.config.mjs",
|
|
71
58
|
"target/**"
|
|
72
59
|
],
|
|
73
60
|
"output": [],
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A context key.
|
|
3
|
+
*
|
|
4
|
+
* A context key can be any type of object, including strings and symbols. The
|
|
5
|
+
* Context type brands the key type with the `__context__` property that
|
|
6
|
+
* carries the type of the value the context references.
|
|
7
|
+
*/
|
|
8
|
+
export type Context<KeyType, ValueType> = KeyType & { __context__: ValueType };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An unknown context type
|
|
12
|
+
*/
|
|
13
|
+
export type UnknownContext = Context<unknown, unknown>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A helper type which can extract a Context value type from a Context type
|
|
17
|
+
*/
|
|
18
|
+
export type ContextType<T extends UnknownContext> = T extends Context<
|
|
19
|
+
infer _,
|
|
20
|
+
infer V
|
|
21
|
+
>
|
|
22
|
+
? V
|
|
23
|
+
: never;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A function which creates a Context value object
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export function createContext<KeyType, ValueType>(key: KeyType) {
|
|
30
|
+
return key as Context<KeyType, ValueType>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A callback which is provided by a context requester and is called with the value satisfying the request.
|
|
35
|
+
* This callback can be called multiple times by context providers as the requested value is changed.
|
|
36
|
+
*/
|
|
37
|
+
export type ContextCallback<ValueType> = (
|
|
38
|
+
value: ValueType,
|
|
39
|
+
unsubscribe?: () => void,
|
|
40
|
+
) => void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An event fired by a context requester to signal it desires a named context.
|
|
44
|
+
*
|
|
45
|
+
* A provider should inspect the `context` property of the event to determine if it has a value that can
|
|
46
|
+
* satisfy the request, calling the `callback` with the requested value if so.
|
|
47
|
+
*
|
|
48
|
+
* If the requested context event contains a truthy `subscribe` value, then a provider can call the callback
|
|
49
|
+
* multiple times if the value is changed, if this is the case the provider should pass an `unsubscribe`
|
|
50
|
+
* function to the callback which requesters can invoke to indicate they no longer wish to receive these updates.
|
|
51
|
+
*/
|
|
52
|
+
export class ContextRequestEvent<T extends UnknownContext> extends Event {
|
|
53
|
+
context: T;
|
|
54
|
+
callback: ContextCallback<ContextType<T>>;
|
|
55
|
+
subscribe?: boolean;
|
|
56
|
+
|
|
57
|
+
public constructor(
|
|
58
|
+
context: T,
|
|
59
|
+
callback: ContextCallback<ContextType<T>>,
|
|
60
|
+
subscribe?: boolean,
|
|
61
|
+
) {
|
|
62
|
+
super("context-request", { bubbles: true, composed: true });
|
|
63
|
+
|
|
64
|
+
this.context = context;
|
|
65
|
+
this.callback = callback;
|
|
66
|
+
this.subscribe = subscribe;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare global {
|
|
71
|
+
interface HTMLElementEventMap {
|
|
72
|
+
/**
|
|
73
|
+
* A 'context-request' event can be emitted by any element which desires
|
|
74
|
+
* a context value to be injected by an external provider.
|
|
75
|
+
*/
|
|
76
|
+
"context-request": ContextRequestEvent<Context<unknown, unknown>>;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,30 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assert } from "chai";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { INJECTOR_CTX } from "./context/injector.js";
|
|
4
|
+
import {
|
|
5
|
+
ContextRequestEvent,
|
|
6
|
+
type UnknownContext,
|
|
7
|
+
} from "./context/protocol.js";
|
|
8
|
+
import { DOMInjector } from "./dom-injector.js";
|
|
9
|
+
import { Injector } from "./injector.js";
|
|
5
10
|
|
|
6
|
-
describe(
|
|
7
|
-
it(
|
|
8
|
-
const
|
|
9
|
-
|
|
11
|
+
describe("DOMInjector", () => {
|
|
12
|
+
it("should respond to elements looking for an injector", () => {
|
|
13
|
+
const injector = new DOMInjector();
|
|
14
|
+
injector.attach(document.body);
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
const host = document.createElement("div");
|
|
17
|
+
document.body.append(host);
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
let parent: Injector | null = null;
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
host.dispatchEvent(
|
|
22
|
+
new ContextRequestEvent(INJECTOR_CTX, (i) => {
|
|
23
|
+
parent = i;
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
assert.equal(parent, injector);
|
|
28
|
+
|
|
29
|
+
injector.detach();
|
|
30
|
+
host.remove();
|
|
16
31
|
});
|
|
17
32
|
|
|
18
|
-
it(
|
|
19
|
-
const
|
|
20
|
-
const
|
|
33
|
+
it("should send request looking for other injector contexts", () => {
|
|
34
|
+
const parent = new Injector();
|
|
35
|
+
const injector = new DOMInjector();
|
|
36
|
+
|
|
37
|
+
const cb = (e: ContextRequestEvent<UnknownContext>) => {
|
|
38
|
+
if (e.context === INJECTOR_CTX) {
|
|
39
|
+
e.callback(parent);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
document.body.addEventListener("context-request", cb);
|
|
44
|
+
|
|
45
|
+
injector.attach(document.body);
|
|
46
|
+
|
|
47
|
+
assert.equal(injector.parent, parent);
|
|
48
|
+
|
|
49
|
+
injector.detach();
|
|
50
|
+
document.body.removeEventListener("context-request", cb);
|
|
51
|
+
});
|
|
21
52
|
|
|
22
|
-
|
|
53
|
+
it("should throw an error if attempting to attach an already attached DOMInjector", () => {
|
|
54
|
+
const injector = new DOMInjector();
|
|
23
55
|
|
|
24
|
-
|
|
56
|
+
const el = document.createElement("div");
|
|
25
57
|
|
|
26
|
-
|
|
58
|
+
injector.attach(el);
|
|
27
59
|
|
|
28
|
-
|
|
60
|
+
assert.throw(() => {
|
|
61
|
+
injector.attach(el);
|
|
62
|
+
});
|
|
29
63
|
});
|
|
30
64
|
});
|
package/src/lib/dom-injector.ts
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { INJECTOR_CTX } from "./context/injector.js";
|
|
2
|
+
import {
|
|
3
|
+
ContextRequestEvent,
|
|
4
|
+
type UnknownContext,
|
|
5
|
+
} from "./context/protocol.js";
|
|
6
|
+
import { Injector } from "./injector.js";
|
|
3
7
|
|
|
4
8
|
export class DOMInjector extends Injector {
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
#element: HTMLElement | null = null;
|
|
10
|
+
#controller: AbortController | null = null;
|
|
11
|
+
|
|
12
|
+
get isAttached(): boolean {
|
|
13
|
+
return this.#element !== null && this.#controller !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
attach(element: HTMLElement): void {
|
|
17
|
+
if (this.isAttached) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`This DOMInjector is already attached to ${this.#element}. Detach first before attaching again`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.#element = element;
|
|
24
|
+
this.#controller = new AbortController();
|
|
25
|
+
|
|
26
|
+
this.#element.addEventListener(
|
|
27
|
+
"context-request",
|
|
28
|
+
(e: ContextRequestEvent<UnknownContext>) => {
|
|
29
|
+
if (e.context === INJECTOR_CTX) {
|
|
30
|
+
if (e.target !== element) {
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
|
|
33
|
+
e.callback(this);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{ signal: this.#controller.signal },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this.#element.dispatchEvent(
|
|
41
|
+
new ContextRequestEvent(INJECTOR_CTX, (parent) => {
|
|
42
|
+
this.parent = parent;
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
7
45
|
}
|
|
8
46
|
|
|
9
|
-
detach(
|
|
10
|
-
|
|
47
|
+
detach(): void {
|
|
48
|
+
if (this.#controller) {
|
|
49
|
+
this.#controller.abort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.#element = null;
|
|
11
53
|
}
|
|
12
54
|
}
|
package/src/lib/inject.test.ts
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assert } from "chai";
|
|
2
2
|
|
|
3
|
-
import { inject } from
|
|
4
|
-
import { injectable } from
|
|
5
|
-
import { Injector } from
|
|
6
|
-
import { StaticToken } from
|
|
3
|
+
import { inject } from "./inject.js";
|
|
4
|
+
import { injectable } from "./injectable.js";
|
|
5
|
+
import { Injector } from "./injector.js";
|
|
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
|
-
value =
|
|
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,53 @@ 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;
|
|
25
|
+
parent.inject(BarService);
|
|
26
|
+
}, "BarService is either not injectable or a service is being called in the constructor.");
|
|
27
|
+
});
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
});
|
|
29
|
+
it("should throw error if static token is unavailable", () => {
|
|
30
|
+
assert.throws(() => {
|
|
31
|
+
const TOKEN = new StaticToken("test");
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
class FooService {
|
|
53
|
-
value = '1';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@injectable
|
|
57
|
-
class BarService {
|
|
58
|
-
foo = inject(FooService);
|
|
59
|
-
}
|
|
33
|
+
const parent = new Injector();
|
|
60
34
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
use: class extends FooService {
|
|
65
|
-
value = '100';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
]);
|
|
35
|
+
parent.inject(TOKEN);
|
|
36
|
+
}, 'Provider not found for "test"');
|
|
37
|
+
});
|
|
69
38
|
|
|
70
|
-
|
|
39
|
+
it("should use the calling injector as parent", () => {
|
|
40
|
+
class FooService {
|
|
41
|
+
value = "1";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@injectable()
|
|
45
|
+
class BarService {
|
|
46
|
+
foo = inject(FooService);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parent = new Injector({
|
|
50
|
+
providers: [
|
|
51
|
+
[
|
|
52
|
+
FooService,
|
|
53
|
+
{
|
|
54
|
+
use: class extends FooService {
|
|
55
|
+
value = "100";
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
],
|
|
71
60
|
});
|
|
72
61
|
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
assert.strictEqual(parent.inject(BarService).foo().value, "100");
|
|
63
|
+
});
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
hello = inject(TOKEN);
|
|
79
|
-
}
|
|
65
|
+
it("should inject a static token", () => {
|
|
66
|
+
const TOKEN = new StaticToken("test", () => "Hello World");
|
|
80
67
|
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
@injectable()
|
|
69
|
+
class HelloWorld {
|
|
70
|
+
hello = inject(TOKEN);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(new HelloWorld().hello(), "Hello World");
|
|
83
74
|
});
|