@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.
Files changed (2) hide show
  1. package/README.md +63 -52
  2. 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 node).
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
- ```BASH
34
+ ```bash
22
35
  npm i @joist/di
23
36
  ```
24
37
 
25
38
  ## Injectors
26
39
 
27
- Injectors are what are used to construct new [services](#services). Injectors can manually [provide implementations](#defining-providers) of services. Injectors can also have [parents](#hierarchical-injectors), parent injectors can define services for all of it's children.
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 classses. Services can be constructed via an `Injector` and treated are singletons (The same instance is returned for each call to Injector.inject()).
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
- // these two calls will return the same instance
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()` fuction. In order to use `inject()` classes must be decorated with `@injectable`.
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 big 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 vs in our main application.
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 example we have a defined HttpService that wraps fetch. but for our unit test we will use a custom implementation that returns just the data we want. This also has the benefit of avoiding test framework specific mocks.
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 level providers
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 defined providers from that level down.
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 example will use this particular instance of Logger as wall as any other services injected into this service.
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 for deciding exactly how a service is created. This is helpful when which instance that is provided depends on some runtime value.
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 injector in factories
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
- // token that resolves to a string
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 values
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 values
239
+ ### Async Values
225
240
 
226
- Static tokens can also leverage promises for cases when you need to async create your service instances.
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, 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.
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
- // called the first time a service is created. (not pulled from cache)
282
+ // Called the first time a service is created (not pulled from cache)
270
283
  }
271
284
 
272
285
  @injected()
273
286
  onInjected() {
274
- // called every time a service is returned, whether it is from cache or not
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 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.
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 not parent, All clear, go ahead and construct and cache the requested service.
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 recieve the same cached instance from RootInjector.
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 instances created from RootInjector.
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 dom, in most cases this will be document.body.
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
- ```TS
328
+ ```ts
316
329
  const app = new DOMInjector();
317
330
 
318
- app.attach(document.body); // anything rendered in the body will have access to this injector.
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
- secodnary = 'green';
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 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.
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
- ```TS
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(COLOR_CTX);
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
- ```HTML
392
+ ```html
382
393
  <!-- Default Colors -->
383
394
  <my-element></my-element>
384
395
 
385
- <!-- colors come from ctx -->
386
- <color-ctx primary="orange" secondard="blue">
396
+ <!-- Colors come from context -->
397
+ <color-ctx primary="orange" secondary="blue">
387
398
  <my-element></my-element>
388
399
  </color-ctx>
389
400
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/di",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",