@joist/di 4.2.0 โ 4.2.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 +63 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Small and efficient dependency injection.
|
|
4
4
|
|
|
5
|
-
Allows you to inject services into other class instances (including custom elements and
|
|
5
|
+
Allows you to inject services into other class instances (including custom elements and Node.js).
|
|
6
|
+
|
|
7
|
+
## Benefits
|
|
8
|
+
|
|
9
|
+
- ๐ **Simple API**: Minimal boilerplate with intuitive decorators and injection
|
|
10
|
+
- ๐ช **Type Safety**: Full TypeScript support with proper type inference
|
|
11
|
+
- ๐ณ **Hierarchical DI**: Create scoped injectors with parent-child relationships
|
|
12
|
+
- โก๏ธ **Lazy Loading**: Services are only instantiated when needed
|
|
13
|
+
- ๐งช **Testing Friendly**: Easy mocking with provider overrides
|
|
14
|
+
- ๐งฉ **Web Component Support**: Built-in integration with custom elements
|
|
15
|
+
- ๐ **Context Pattern**: React-like context for web components
|
|
16
|
+
- ๐ **Lifecycle Hooks**: Fine-grained control over service initialization
|
|
17
|
+
- โฑ๏ธ **Async Support**: Handle asynchronous service creation
|
|
18
|
+
- ๐ฆ **Zero Dependencies**: Lightweight with no external dependencies
|
|
6
19
|
|
|
7
20
|
## Table of Contents
|
|
8
21
|
|
|
@@ -18,17 +31,21 @@ Allows you to inject services into other class instances (including custom eleme
|
|
|
18
31
|
|
|
19
32
|
## Installation
|
|
20
33
|
|
|
21
|
-
```
|
|
34
|
+
```bash
|
|
22
35
|
npm i @joist/di
|
|
23
36
|
```
|
|
24
37
|
|
|
25
38
|
## Injectors
|
|
26
39
|
|
|
27
|
-
Injectors are
|
|
40
|
+
Injectors are the core of the dependency injection system. They:
|
|
41
|
+
- Create and manage service instances
|
|
42
|
+
- Handle dependency resolution
|
|
43
|
+
- Maintain a hierarchy of injectors
|
|
44
|
+
- Cache service instances
|
|
28
45
|
|
|
29
46
|
## Services
|
|
30
47
|
|
|
31
|
-
At their simplest, services are
|
|
48
|
+
At their simplest, services are classes. Services can be constructed via an `Injector` and treated as singletons (the same instance is returned for each call to `Injector.inject()`).
|
|
32
49
|
|
|
33
50
|
```ts
|
|
34
51
|
const app = new Injector();
|
|
@@ -41,14 +58,16 @@ class Counter {
|
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
//
|
|
61
|
+
// These two calls will return the same instance
|
|
45
62
|
const foo = app.inject(Counter);
|
|
46
63
|
const bar = app.inject(Counter);
|
|
64
|
+
|
|
65
|
+
console.log(foo === bar); // true
|
|
47
66
|
```
|
|
48
67
|
|
|
49
68
|
## Injectable Services
|
|
50
69
|
|
|
51
|
-
Singleton services are great but the real benefit can be seen when passing instances of one service to another. Services are injected into other services using the `inject()`
|
|
70
|
+
Singleton services are great, but the real benefit can be seen when passing instances of one service to another. Services are injected into other services using the `inject()` function. In order to use `inject()`, classes must be decorated with `@injectable`.
|
|
52
71
|
|
|
53
72
|
`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
73
|
|
|
@@ -59,7 +78,6 @@ class App {
|
|
|
59
78
|
|
|
60
79
|
update(val: number) {
|
|
61
80
|
const instance = this.#counter();
|
|
62
|
-
|
|
63
81
|
instance.inc(val);
|
|
64
82
|
}
|
|
65
83
|
}
|
|
@@ -67,13 +85,12 @@ class App {
|
|
|
67
85
|
|
|
68
86
|
## Defining Providers
|
|
69
87
|
|
|
70
|
-
A
|
|
88
|
+
A key reason to use dependency injection is the ability to provide multiple implementations for a particular service. For example, we probably want a different HTTP client when running unit tests versus in our main application.
|
|
71
89
|
|
|
72
|
-
In the below
|
|
90
|
+
In the example below, we have a defined `HttpService` that wraps fetch. For our unit test, we'll use a custom implementation that returns just the data we want. This also has the benefit of avoiding test framework-specific mocks.
|
|
73
91
|
|
|
74
92
|
```ts
|
|
75
93
|
// services.ts
|
|
76
|
-
|
|
77
94
|
class HttpService {
|
|
78
95
|
fetch(url: string, init?: RequestInit) {
|
|
79
96
|
return fetch(url, init);
|
|
@@ -94,7 +111,6 @@ class ApiService {
|
|
|
94
111
|
|
|
95
112
|
```ts
|
|
96
113
|
// services.test.ts
|
|
97
|
-
|
|
98
114
|
test('should return json', async () => {
|
|
99
115
|
class MockHttpService extends HttpService {
|
|
100
116
|
async fetch() {
|
|
@@ -114,11 +130,11 @@ test('should return json', async () => {
|
|
|
114
130
|
});
|
|
115
131
|
```
|
|
116
132
|
|
|
117
|
-
### Service
|
|
133
|
+
### Service Level Providers
|
|
118
134
|
|
|
119
|
-
Under the hood, each service decorated with `@injectable()` creates its own injector. This means that it is possible to
|
|
135
|
+
Under the hood, each service decorated with `@injectable()` creates its own injector. This means that it is possible to define providers from that level down.
|
|
120
136
|
|
|
121
|
-
The below
|
|
137
|
+
The example below will use this particular instance of `Logger` as well as any other services injected into this service.
|
|
122
138
|
|
|
123
139
|
```ts
|
|
124
140
|
class Logger {
|
|
@@ -139,7 +155,7 @@ class MyService {}
|
|
|
139
155
|
|
|
140
156
|
### Factories
|
|
141
157
|
|
|
142
|
-
In addition to defining providers with classes you can also use factory functions. Factories allow for more flexibility
|
|
158
|
+
In addition to defining providers with classes, you can also use factory functions. Factories allow for more flexibility in deciding exactly how a service is created. This is helpful when the instance that is provided depends on some runtime value.
|
|
143
159
|
|
|
144
160
|
```ts
|
|
145
161
|
class Logger {
|
|
@@ -162,9 +178,9 @@ const app = new Injector([
|
|
|
162
178
|
]);
|
|
163
179
|
```
|
|
164
180
|
|
|
165
|
-
### Accessing the
|
|
181
|
+
### Accessing the Injector in Factories
|
|
166
182
|
|
|
167
|
-
Factories provide more flexibility but sometimes will require access to the injector itself. For this reason the factory method is passed the injector that is being used to construct the requested service.
|
|
183
|
+
Factories provide more flexibility but sometimes will require access to the injector itself. For this reason, the factory method is passed the injector that is being used to construct the requested service.
|
|
168
184
|
|
|
169
185
|
```ts
|
|
170
186
|
class Logger {
|
|
@@ -187,7 +203,6 @@ const app = new Injector([
|
|
|
187
203
|
{
|
|
188
204
|
factory(i) {
|
|
189
205
|
const logger = i.inject(Logger);
|
|
190
|
-
|
|
191
206
|
return new Feature(logger);
|
|
192
207
|
}
|
|
193
208
|
}
|
|
@@ -197,10 +212,10 @@ const app = new Injector([
|
|
|
197
212
|
|
|
198
213
|
## StaticTokens
|
|
199
214
|
|
|
200
|
-
In most cases a token is any constructable class. There are cases where you might want to return other data types that aren't objects.
|
|
215
|
+
In most cases, a token is any constructable class. There are cases where you might want to return other data types that aren't objects.
|
|
201
216
|
|
|
202
217
|
```ts
|
|
203
|
-
//
|
|
218
|
+
// Token that resolves to a string
|
|
204
219
|
const URL_TOKEN = new StaticToken<string>('app_url');
|
|
205
220
|
|
|
206
221
|
const app = new Injector([
|
|
@@ -213,7 +228,7 @@ const app = new Injector([
|
|
|
213
228
|
]);
|
|
214
229
|
```
|
|
215
230
|
|
|
216
|
-
### Default
|
|
231
|
+
### Default Values
|
|
217
232
|
|
|
218
233
|
A static token can be provided a default factory function to use on creation.
|
|
219
234
|
|
|
@@ -221,9 +236,9 @@ A static token can be provided a default factory function to use on creation.
|
|
|
221
236
|
const URL_TOKEN = new StaticToken('app_url', () => '/default-url/');
|
|
222
237
|
```
|
|
223
238
|
|
|
224
|
-
### Async
|
|
239
|
+
### Async Values
|
|
225
240
|
|
|
226
|
-
Static tokens can also leverage promises for cases when you need to
|
|
241
|
+
Static tokens can also leverage promises for cases when you need to asynchronously create your service instances.
|
|
227
242
|
|
|
228
243
|
```ts
|
|
229
244
|
// StaticToken<Promise<string>>
|
|
@@ -234,7 +249,7 @@ const app = new Injector();
|
|
|
234
249
|
const url: string = await app.inject(URL_TOKEN);
|
|
235
250
|
```
|
|
236
251
|
|
|
237
|
-
This allows you to dynamically import services
|
|
252
|
+
This allows you to dynamically import services:
|
|
238
253
|
|
|
239
254
|
```ts
|
|
240
255
|
const HttpService = new StaticToken('HTTP_SERVICE', () => {
|
|
@@ -254,72 +269,70 @@ class HackerNewsService {
|
|
|
254
269
|
return http.fetchJson<string[]>(url);
|
|
255
270
|
}
|
|
256
271
|
}
|
|
257
|
-
|
|
258
|
-
const url: string = await app.inject(URL_TOKEN);
|
|
259
272
|
```
|
|
260
273
|
|
|
261
274
|
## LifeCycle
|
|
262
275
|
|
|
263
|
-
To help provide more information to services that are being created,
|
|
276
|
+
To help provide more information to services that are being created, Joist will call several lifecycle hooks as services are created. These hooks are defined using the provided decorators so there is no risk of naming collisions.
|
|
264
277
|
|
|
265
278
|
```ts
|
|
266
279
|
class MyService {
|
|
267
280
|
@created()
|
|
268
281
|
onCreated() {
|
|
269
|
-
//
|
|
282
|
+
// Called the first time a service is created (not pulled from cache)
|
|
270
283
|
}
|
|
271
284
|
|
|
272
285
|
@injected()
|
|
273
286
|
onInjected() {
|
|
274
|
-
//
|
|
287
|
+
// Called every time a service is returned, whether it is from cache or not
|
|
275
288
|
}
|
|
276
289
|
}
|
|
277
290
|
```
|
|
278
291
|
|
|
279
292
|
## Hierarchical Injectors
|
|
280
293
|
|
|
281
|
-
Injectors can be defined with a parent. The top
|
|
294
|
+
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 follows:
|
|
282
295
|
|
|
283
296
|
1. Do I have a cached instance locally?
|
|
284
297
|
2. Do I have a local provider definition for the token?
|
|
285
298
|
3. Do I have a parent?
|
|
286
299
|
4. Does parent have a local instance or provider definition?
|
|
287
300
|
5. If parent exists but no instance found, create instance in parent.
|
|
288
|
-
6. If
|
|
301
|
+
6. If no parent, all clear, go ahead and construct and cache the requested service.
|
|
289
302
|
|
|
290
303
|
Having injectors resolve this way means that all children have access to services created by their parents.
|
|
291
304
|
|
|
292
305
|
```mermaid
|
|
293
306
|
graph TD
|
|
294
307
|
RootInjector --> InjectorA;
|
|
295
|
-
InjectorA -->InjectorB;
|
|
308
|
+
InjectorA --> InjectorB;
|
|
296
309
|
InjectorA --> InjectorC;
|
|
297
310
|
InjectorA --> InjectorD;
|
|
298
311
|
InjectorD --> InjectorE;
|
|
299
312
|
```
|
|
300
313
|
|
|
301
314
|
In the above tree, if InjectorE requests a service, it will navigate up to the RootInjector and cache.
|
|
302
|
-
If InjectorB then requests the same token, it will
|
|
315
|
+
If InjectorB then requests the same token, it will receive the same cached instance from RootInjector.
|
|
303
316
|
|
|
304
|
-
On the other hand if a provider is defined at InjectorD, then the service will be constructed and cached there.
|
|
305
|
-
InjectorB would given a NEW
|
|
317
|
+
On the other hand, if a provider is defined at InjectorD, then the service will be constructed and cached there.
|
|
318
|
+
InjectorB would be given a NEW instance created from RootInjector.
|
|
306
319
|
This is because InjectorB does not fall under InjectorD.
|
|
307
320
|
This behavior allows for services to be "scoped" within a certain branch of the tree. This is what allows for the scoped custom element behavior defined in the next section.
|
|
308
321
|
|
|
309
|
-
## Custom Elements
|
|
322
|
+
## Custom Elements
|
|
310
323
|
|
|
311
|
-
Joist is built to work with custom elements. Since the document is a tree we can search up that tree for providers.
|
|
324
|
+
Joist is built to work with custom elements. Since the document is a tree, we can search up that tree for providers.
|
|
312
325
|
|
|
313
|
-
Setting your web page to work is very similar to any other JavaScript environment. There is a special `DOMInjector` class that will allow you to attach an injector to any location in the
|
|
326
|
+
Setting your web page to work is very similar to any other JavaScript environment. There is a special `DOMInjector` class that will allow you to attach an injector to any location in the DOM, in most cases this will be document.body.
|
|
314
327
|
|
|
315
|
-
```
|
|
328
|
+
```ts
|
|
316
329
|
const app = new DOMInjector();
|
|
317
330
|
|
|
318
|
-
app.attach(document.body); //
|
|
331
|
+
app.attach(document.body); // Anything rendered in the body will have access to this injector.
|
|
319
332
|
|
|
320
333
|
class Colors {
|
|
321
334
|
primary = 'red';
|
|
322
|
-
|
|
335
|
+
secondary = 'green';
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
@injectable()
|
|
@@ -328,7 +341,6 @@ class MyElement extends HTMLElement {
|
|
|
328
341
|
|
|
329
342
|
connectedCallback() {
|
|
330
343
|
const { primary } = this.#colors();
|
|
331
|
-
|
|
332
344
|
this.style.background = primary;
|
|
333
345
|
}
|
|
334
346
|
}
|
|
@@ -336,13 +348,13 @@ class MyElement extends HTMLElement {
|
|
|
336
348
|
customElements.define('my-element', MyElement);
|
|
337
349
|
```
|
|
338
350
|
|
|
339
|
-
### Context Elements
|
|
351
|
+
### Context Elements
|
|
340
352
|
|
|
341
|
-
Context elements are where Hierarchical Injectors can really shine as they allow you to
|
|
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.
|
|
353
|
+
Context elements are where Hierarchical Injectors can really shine as they allow you to define React/Preact-esque "context" elements.
|
|
354
|
+
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.
|
|
355
|
+
This also makes it easy to use attributes to define values for the service.
|
|
344
356
|
|
|
345
|
-
```
|
|
357
|
+
```ts
|
|
346
358
|
class ColorCtx {
|
|
347
359
|
primary = "red";
|
|
348
360
|
secondary = "green";
|
|
@@ -364,11 +376,10 @@ class ColorCtx extends HTMLElement implements ColorCtx {
|
|
|
364
376
|
|
|
365
377
|
@injectable()
|
|
366
378
|
class MyElement extends HTMLElement {
|
|
367
|
-
#colors = inject(
|
|
379
|
+
#colors = inject(ColorCtx);
|
|
368
380
|
|
|
369
381
|
connectedCallback() {
|
|
370
382
|
const { primary } = this.#colors();
|
|
371
|
-
|
|
372
383
|
this.style.background = primary;
|
|
373
384
|
}
|
|
374
385
|
}
|
|
@@ -378,12 +389,12 @@ customElements.define('color-ctx', ColorCtx);
|
|
|
378
389
|
customElements.define('my-element', MyElement);
|
|
379
390
|
```
|
|
380
391
|
|
|
381
|
-
```
|
|
392
|
+
```html
|
|
382
393
|
<!-- Default Colors -->
|
|
383
394
|
<my-element></my-element>
|
|
384
395
|
|
|
385
|
-
<!--
|
|
386
|
-
<color-ctx primary="orange"
|
|
396
|
+
<!-- Colors come from context -->
|
|
397
|
+
<color-ctx primary="orange" secondary="blue">
|
|
387
398
|
<my-element></my-element>
|
|
388
399
|
</color-ctx>
|
|
389
400
|
```
|