@adimm/x-injection-reactjs 0.1.2 → 0.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 +405 -147
- package/dist/index.cjs +132 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -182
- package/dist/index.d.ts +106 -182
- package/dist/index.js +117 -147
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
|
-
xInjection ReactJS
|
|
3
|
-
<img src="https://
|
|
2
|
+
xInjection ReactJS <a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank" alt="Release Version"><img src="https://badgen.net/npm/v/@adimm/x-injection-reactjs"></a>
|
|
3
|
+
<img src="https://badgen.net/npm/license/@adimm/x-injection-reactjs" alt="License">
|
|
4
|
+
<a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection-reactjs" target="__blank" alt="Release Version"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection-reactjs"></a>
|
|
4
5
|
</h1>
|
|
5
6
|
|
|
6
7
|
<p align="center">
|
|
7
8
|
<a href="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/ci.yml?query=branch%3Amain" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/ci.yml/badge.svg?branch=main"></a>
|
|
8
9
|
<a href="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/publish.yml" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection-reactjs/actions/workflows/publish.yml/badge.svg"></a>
|
|
9
10
|
<br>
|
|
10
|
-
<img src="https://
|
|
11
|
-
<a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank" alt="Monthly Downloads"><img src="https://
|
|
11
|
+
<img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection-reactjs">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@adimm/x-injection-reactjs" target="__blank" alt="Monthly Downloads"><img src="https://badgen.net/npm/dm/@adimm/x-injection-reactjs"></a>
|
|
12
13
|
</p>
|
|
13
14
|
|
|
14
15
|
## Table of Contents
|
|
@@ -18,11 +19,14 @@ xInjection ReactJS <a href="https://www.npmjs.com/package/@adimm/x-injectio
|
|
|
18
19
|
- [Installation](#installation)
|
|
19
20
|
- [TypeScript Configuration](#typescript-configuration)
|
|
20
21
|
- [Getting Started](#getting-started)
|
|
22
|
+
- [Component ProviderModules](#component-providermodules)
|
|
23
|
+
- [Component Injection](#component-injection)
|
|
24
|
+
- [Via anonymous function](#via-anonymous-function)
|
|
25
|
+
- [Via named function](#via-named-function)
|
|
26
|
+
- [Hook Injection](#hook-injection)
|
|
21
27
|
- [Examples](#examples)
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
- [Safe method](#safe-method)
|
|
25
|
-
- [Experimental method](#experimental-method)
|
|
28
|
+
- [Composable components](#composable-components)
|
|
29
|
+
- [Unit Tests](#unit-tests)
|
|
26
30
|
- [Documentation](#documentation)
|
|
27
31
|
- [Contributing](#contributing)
|
|
28
32
|
|
|
@@ -65,193 +69,447 @@ Add the following options to your `tsconfig.json` to enable decorator metadata:
|
|
|
65
69
|
|
|
66
70
|
## Getting Started
|
|
67
71
|
|
|
68
|
-
If you never used the parent library (`xInjection`), then please access the official [xInjection Repository](https://github.com/AdiMarianMutu/x-injection?tab=readme-ov-file#
|
|
72
|
+
If you never used the parent library (`xInjection`), then please access the official [xInjection Repository](https://github.com/AdiMarianMutu/x-injection?tab=readme-ov-file#getting-started) to better understand how to use its `ReactJS` implementation.
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
### Component ProviderModules
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
A [ComponentProviderModule](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html) isn't so different than the original [ProviderModule](https://adimarianmutu.github.io/x-injection/interfaces/IProviderModule.html) from the base `xInjection` library, the main difference being that it'll automatically create a [clone](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html#clone) of itself whenever a component is `mounted` and during the `unmount` process it'll [dispose](https://adimarianmutu.github.io/x-injection-reactjs/interfaces/IComponentProviderModule.html#dispose) itself.
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
a parent component will not be able to access those dependencies instances.
|
|
78
|
+
This is needed so:
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
- Each instance of a component has its own instance of the `ProviderModule` _(also known as `ContextualizedModule`)_
|
|
81
|
+
- Whenever a component is unmounted, the container of that `ContextualizedModule` is destroyed, making sure that the resources can be garbage-collected by the JS garbage collector.
|
|
82
|
+
|
|
83
|
+
> **Note:** By default each `ContextualizedModule` has its `InjectionScope` set to `Singleton`, you can of course change it by providing the [defaultScope](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#defaultscope) property.
|
|
84
|
+
|
|
85
|
+
### Component Injection
|
|
86
|
+
|
|
87
|
+
In order to be able to inject dependencies into your components, you must first supply them with a `ComponentProviderModule`.
|
|
88
|
+
|
|
89
|
+
This is how you can create one:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
@Injectable()
|
|
93
|
+
export class UserService {
|
|
94
|
+
firstName: string;
|
|
95
|
+
lastName: string;
|
|
96
|
+
|
|
97
|
+
generateFullName(): string {
|
|
98
|
+
return `${firstName} ${lastName}`;
|
|
81
99
|
}
|
|
82
100
|
}
|
|
83
101
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
providers: [RandomNumberService],
|
|
102
|
+
export const UserComponentModule = new ComponentProviderModule({
|
|
103
|
+
identifier: Symbol('UserComponentModule'),
|
|
104
|
+
providers: [UserService],
|
|
88
105
|
});
|
|
89
106
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
module={RandomNumberComponentModule}
|
|
94
|
-
render={() => {
|
|
95
|
-
const service = useInject(RandomNumberService);
|
|
96
|
-
|
|
97
|
-
return <h1>A random number: {service.generate()}</h1>;
|
|
98
|
-
}}
|
|
99
|
-
/>
|
|
100
|
-
);
|
|
107
|
+
interface UserInfoProps {
|
|
108
|
+
firstName: string;
|
|
109
|
+
lastName: string;
|
|
101
110
|
}
|
|
102
111
|
```
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
Now you have to actually provide the `UserComponentModule` to your component(s). You can do so with 2 different methods:
|
|
114
|
+
|
|
115
|
+
#### Via anonymous function
|
|
105
116
|
|
|
106
|
-
|
|
117
|
+
If you prefer to use the `const Component = () => {}` syntax, then you must use the [provideModuleToComponent](https://adimarianmutu.github.io/x-injection-reactjs/functions/provideModuleToComponent.html) method as shown below:
|
|
118
|
+
|
|
119
|
+
> **Note:** _This is the preferred method as it allows you to avoid wrapping your component within another provider once created._
|
|
107
120
|
|
|
108
121
|
```tsx
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
// The UserInfo component will correctly infer the interface of `UserInfoProps` automatically!
|
|
123
|
+
export const UserInfo = provideModuleToComponent(UserComponentModule, ({ firstName, lastName }: UserInfoProps) => {
|
|
124
|
+
const userService = useInject(UserService);
|
|
125
|
+
|
|
126
|
+
userService.firstName = firstName;
|
|
127
|
+
userService.lastName = lastName;
|
|
128
|
+
|
|
129
|
+
return <p>Hello {userService.generateFullName()}!</p>;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function MyApp() {
|
|
133
|
+
return <UserInfo firstName="John" lastName="Doe" />;
|
|
134
|
+
// Result
|
|
135
|
+
//
|
|
136
|
+
// <p>Hello John Doe!</p>
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Via named function
|
|
141
|
+
|
|
142
|
+
Or if you prefer to use the `function Component() {}` syntax, then you must use the [ProvideModule](https://adimarianmutu.github.io/x-injection-reactjs/functions/ProvideModule.html) `HoC` as shown below:
|
|
143
|
+
|
|
144
|
+
> **Note:** _If you need to access the contextualized `module` forwarded to your component, you can wrap the component props with the [PropsWithModule](https://adimarianmutu.github.io/x-injection-reactjs/types/PropsWithModule.html) generic type._
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
export function UserInfo({ firstName, lastName }: UserInfoProps) {
|
|
148
|
+
const userService = useInject(UserService);
|
|
149
|
+
|
|
150
|
+
userService.firstName = firstName;
|
|
151
|
+
userService.lastName = lastName;
|
|
152
|
+
|
|
153
|
+
return <p>Hello {userService.generateFullName()}!</p>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function MyApp() {
|
|
157
|
+
return (
|
|
158
|
+
<ProvideModule module={UserComponentModule}>
|
|
159
|
+
<UserInfo firstName="John" lastName="Doe" />
|
|
160
|
+
</ProvideModule>
|
|
161
|
+
);
|
|
162
|
+
// Result
|
|
163
|
+
//
|
|
164
|
+
// <p>Hello John Doe!</p>
|
|
113
165
|
}
|
|
166
|
+
```
|
|
114
167
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
That's all you need to do, at least for simple components 😃.
|
|
169
|
+
|
|
170
|
+
> You can find more complex examples at the [Examples](#examples) section.
|
|
171
|
+
|
|
172
|
+
### Hook Injection
|
|
173
|
+
|
|
174
|
+
You already have seen in action the low-level [useInject](https://adimarianmutu.github.io/x-injection-reactjs/functions/useInject.html) hook _(take a look also at the [useInjectMany](https://adimarianmutu.github.io/x-injection-reactjs/functions/useInjectMany.html) hook)_. It is quite useful when you just have to inject quickly some dependencies into a component quite simple.
|
|
175
|
+
|
|
176
|
+
What it does under the hood? Finds the nearest contextualized module and resolves from it the required dependencies into your component, that's all.
|
|
177
|
+
|
|
178
|
+
But, as your UI will grow, you'll soon discover that you may inject more dependencies into a component, or even in multiple components, therefore you'll end up writing a lot of duplicated code, well, as per the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#:~:text=%22Don't%20repeat%20yourself%22,redundancy%20in%20the%20first%20place.) principle, that's not good! 🥲
|
|
179
|
+
|
|
180
|
+
This means that we can actually use the [hookFactory](https://adimarianmutu.github.io/x-injection-reactjs/functions/hookFactory.html) method to compose a _custom_ hook with access to any dependency available in the component contextualized module.
|
|
181
|
+
|
|
182
|
+
Having the above examples with the `UserService`, we'll create a custom `generateFullName` hook.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// The `HookWithDeps` generic type will help
|
|
186
|
+
// in making sure that the `useGenerateUserFullName` hooks params are correctly visible.
|
|
187
|
+
// The 1st generic param must be the hook params (Like `UserInfoProps`)
|
|
188
|
+
// and starting from the 2nd generic param you must provide the type of your dependencies.
|
|
189
|
+
const useGenerateUserFullName = hookFactory({
|
|
190
|
+
// The `use` property is where you write your hook implementation.
|
|
191
|
+
use: ({ firstName, lastName, deps: [userService] }: HookWithDeps<UserInfoProps, UserService>) => {
|
|
192
|
+
userService.firstName = firstName;
|
|
193
|
+
userService.lastName = lastName;
|
|
194
|
+
|
|
195
|
+
return userService.generateFullName();
|
|
196
|
+
},
|
|
197
|
+
// The `inject` array is very important,
|
|
198
|
+
// here we basically specify which dependencies should be injected into the custom hook.
|
|
199
|
+
// Also, keep in mind that the order of the `inject` array matters, the order of the `deps` prop
|
|
200
|
+
// is determined by the order of the `inject` array!
|
|
201
|
+
inject: [UserService],
|
|
120
202
|
});
|
|
203
|
+
```
|
|
121
204
|
|
|
122
|
-
|
|
123
|
-
<ModuleProvider
|
|
124
|
-
module={RandomNumberComponentModule}
|
|
125
|
-
render={() => {
|
|
126
|
-
// This hook is necessary in order to expose the component instance
|
|
127
|
-
// context up to the parent component
|
|
128
|
-
useExposeComponentModuleContext();
|
|
205
|
+
Now you can use it in inside any component which has access to a contextualized module which can provide the `UserService`.
|
|
129
206
|
|
|
130
|
-
|
|
207
|
+
```tsx
|
|
208
|
+
export function UserInfo({ firstName, lastName }: UserInfoProps) {
|
|
209
|
+
const userFullName = useGenerateFullName({ firstName, lastName });
|
|
131
210
|
|
|
132
|
-
|
|
133
|
-
}}
|
|
134
|
-
/>;
|
|
211
|
+
return <p>Hello {userFullName}!</p>;
|
|
135
212
|
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Examples
|
|
216
|
+
|
|
217
|
+
### Composable components
|
|
218
|
+
|
|
219
|
+
In a real world scenario, you'll definitely have custom components which render other custom components and so on... _(like a [Matryoshka doll](https://en.wikipedia.org/wiki/Matryoshka_doll))_
|
|
220
|
+
|
|
221
|
+
So you may find yourself wanting to be able to control a dependency/service of a child component from a parent component, with `xInject` this is very easy to achieve thanks to the `ProviderModule` architecture, because each `module` can `import` and `export` other dependencies _(or modules)_ it fits in perfectly within the [declarative programming](https://en.wikipedia.org/wiki/Declarative_programming) world!
|
|
136
222
|
|
|
137
|
-
|
|
223
|
+
In this example, we'll build 4 components, each with its own purpose. However, the `autocomplete` component will be the one capable of accessing the services of all of them.
|
|
138
224
|
|
|
139
|
-
|
|
140
|
-
|
|
225
|
+
- An `inputbox`
|
|
226
|
+
- A `list viewer`
|
|
227
|
+
- A `dropdown`
|
|
228
|
+
- An `autocomplete`
|
|
141
229
|
|
|
142
|
-
|
|
143
|
-
|
|
230
|
+
<hr>
|
|
231
|
+
|
|
232
|
+
> Inputbox
|
|
233
|
+
|
|
234
|
+
`inputbox.service.ts`
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
@Injectable()
|
|
238
|
+
export class InputboxService {
|
|
239
|
+
currentValue = '';
|
|
240
|
+
|
|
241
|
+
// We'll initialize this soon enough.
|
|
242
|
+
setStateValue!: (newValue: string) => void;
|
|
243
|
+
|
|
244
|
+
/** Can be used to update the {@link currentValue} of the `inputbox`. */
|
|
245
|
+
setValue(newValue: string): void {
|
|
246
|
+
this.currentValue = newValue;
|
|
247
|
+
|
|
248
|
+
this.setStateValue(this.currentValue);
|
|
144
249
|
}
|
|
145
250
|
}
|
|
146
251
|
|
|
147
|
-
export const
|
|
148
|
-
identifier: Symbol('
|
|
149
|
-
|
|
150
|
-
|
|
252
|
+
export const InputboxModule = new ComponentProviderModule({
|
|
253
|
+
identifier: Symbol('InputboxModule'),
|
|
254
|
+
provides: [InputboxService],
|
|
255
|
+
exports: [InputboxService],
|
|
151
256
|
});
|
|
257
|
+
```
|
|
152
258
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const service = useInject(ParentService);
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<>
|
|
162
|
-
<TapIntoComponent
|
|
163
|
-
// By using the fluid syntax
|
|
164
|
-
contextInstance={() => ({
|
|
165
|
-
// If one of the children did expose the `RandomNumberComponentModule`
|
|
166
|
-
// module, we'll be able to access its instance.
|
|
167
|
-
tryGet: RandomNumberComponentModule,
|
|
168
|
-
thenDo: (ctx) => {
|
|
169
|
-
const randomNumberService_FromComponentInstance = ctx.get(RandomNumberComponentModule);
|
|
170
|
-
|
|
171
|
-
service.injectRandomNumberService(randomNumberService_FromComponentInstance);
|
|
172
|
-
},
|
|
173
|
-
})}>
|
|
174
|
-
<RandomNumberComponent />
|
|
175
|
-
</TapIntoComponent>
|
|
176
|
-
|
|
177
|
-
<TapIntoComponent
|
|
178
|
-
// By accessing the entire underlying context map which may contain even more
|
|
179
|
-
// modules exposed by more children down the tree.
|
|
180
|
-
contextInstance={(ctxMap) => {
|
|
181
|
-
const ctx = ctxMap.get(RandomNumberComponentModule.toString());
|
|
182
|
-
if (!ctx) return;
|
|
183
|
-
|
|
184
|
-
const randomNumberService_FromComponentInstance = ctx.get(RandomNumberComponentModule);
|
|
185
|
-
|
|
186
|
-
service.injectRandomNumberService(randomNumberService_FromComponentInstance);
|
|
187
|
-
}}>
|
|
188
|
-
<RandomNumberComponent />
|
|
189
|
-
</TapIntoComponent>
|
|
190
|
-
</>
|
|
191
|
-
);
|
|
192
|
-
}}
|
|
193
|
-
/>
|
|
194
|
-
);
|
|
259
|
+
`inputbox.tsx`
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
export interface InputboxProps {
|
|
263
|
+
initialValue: string;
|
|
195
264
|
}
|
|
265
|
+
|
|
266
|
+
export const Inputbox = provideModuleToComponent(InputboxModule, ({ initialValue }: InputboxProps) => {
|
|
267
|
+
const service = useInject(InputboxService);
|
|
268
|
+
const [, setCurrentValue] = useState(initialValue);
|
|
269
|
+
service.setStateValue = setCurrentValue;
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
service.currentValue = initialValue;
|
|
273
|
+
}, [initialValue]);
|
|
274
|
+
|
|
275
|
+
return <input value={service.currentValue} onChange={(e) => service.setValue(e.currentTarget.value)} />;
|
|
276
|
+
});
|
|
196
277
|
```
|
|
197
278
|
|
|
198
|
-
|
|
279
|
+
<hr>
|
|
280
|
+
|
|
281
|
+
> Listview
|
|
199
282
|
|
|
200
|
-
|
|
201
|
-
|
|
283
|
+
`listview.service.ts`
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
@Injectable()
|
|
287
|
+
export class ListviewService {
|
|
288
|
+
items = [];
|
|
289
|
+
|
|
290
|
+
/* Remaining fancy implementation */
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const ListviewModule = new ComponentProviderModule({
|
|
294
|
+
identifier: Symbol('ListviewModule'),
|
|
295
|
+
provides: [ListviewService],
|
|
296
|
+
exports: [ListviewService],
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`listview.tsx`
|
|
202
301
|
|
|
203
302
|
```tsx
|
|
204
|
-
export
|
|
303
|
+
export interface ListviewProps {
|
|
304
|
+
items: any[];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const Listview = provideModuleToComponent(ListviewModule, ({ items }: ListviewProps) => {
|
|
308
|
+
const service = useInject(ListviewService);
|
|
309
|
+
|
|
310
|
+
/* Remaining fancy implementation */
|
|
311
|
+
|
|
205
312
|
return (
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// has mounted and rendered!
|
|
212
|
-
useRerenderOnChildrenModuleContextLoaded();
|
|
213
|
-
|
|
214
|
-
// We should use the `useInjectOnRender` instead of the default `useInject`
|
|
215
|
-
// hook which re-uses the same instance of the injected dependency
|
|
216
|
-
// between re-renders.
|
|
217
|
-
//
|
|
218
|
-
// Note: It may still work with the `useInject` hook too, but it may not be predictable.
|
|
219
|
-
const service = useInjectOnRender(ParentService);
|
|
220
|
-
|
|
221
|
-
// At this point the `service.randomNumberService` instance should be the one
|
|
222
|
-
// from the `RandomNumberComponent` below.
|
|
223
|
-
//
|
|
224
|
-
// Note: Expect during the 1st render cycle to not be the same instance as the one used by the child component!
|
|
225
|
-
// The `xInjection` container will still supply the correct provider to the
|
|
226
|
-
// constructor parameter, but it'll be a new transient instance.
|
|
227
|
-
console.log(service.randomNumberService.generate());
|
|
228
|
-
|
|
229
|
-
// As we are now using the `useRerenderOnChildrenModuleContextLoaded` hook
|
|
230
|
-
// there's no need anymore for the `TapIntoComponent` wrapper.
|
|
231
|
-
return <RandomNumberComponent />;
|
|
232
|
-
}}
|
|
233
|
-
/>
|
|
313
|
+
<div>
|
|
314
|
+
{service.items.map((item) => (
|
|
315
|
+
<span key={item}>{item}</span>
|
|
316
|
+
))}
|
|
317
|
+
</div>
|
|
234
318
|
);
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
<hr>
|
|
323
|
+
|
|
324
|
+
> Dropdown
|
|
325
|
+
|
|
326
|
+
Now keep close attention to how we implement the `Dropdown` component, as it'll actually be the _parent_ controlling the `Listview` component own service.
|
|
327
|
+
|
|
328
|
+
`dropdown.service.ts`
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
@Injectable()
|
|
332
|
+
export class DropdownService {
|
|
333
|
+
constructor(readonly listviewService: ListviewService) {
|
|
334
|
+
// We can already take control of the children `ListviewService`!
|
|
335
|
+
this.listviewService.items = [1, 2, 3, 4, 5];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Remaining fancy implementation */
|
|
235
339
|
}
|
|
340
|
+
|
|
341
|
+
export const DropdownModule = new ComponentProviderModule({
|
|
342
|
+
identifier: Symbol('DropdownModule'),
|
|
343
|
+
// It is very important that we import all the exportable dependencies from the `ListviewModule`!
|
|
344
|
+
imports: [ListviewModule],
|
|
345
|
+
provides: [DropdownService],
|
|
346
|
+
exports: [
|
|
347
|
+
// Let's also re-export the dependencies of the `ListviewModule` so once we import the `DropdownModule`
|
|
348
|
+
// somewhere elese, we get access to the `ListviewModule` exported dependencies as well!
|
|
349
|
+
ListviewModule,
|
|
350
|
+
// Let's not forget to also export our `DropdownService` :)
|
|
351
|
+
DropdownService,
|
|
352
|
+
],
|
|
353
|
+
});
|
|
236
354
|
```
|
|
237
355
|
|
|
238
|
-
|
|
356
|
+
`dropdown.tsx`
|
|
239
357
|
|
|
240
358
|
```tsx
|
|
241
|
-
export
|
|
242
|
-
|
|
359
|
+
export interface DropdownProps {
|
|
360
|
+
listviewProps: ListviewProps;
|
|
361
|
+
|
|
362
|
+
initialSelectedValue: number;
|
|
243
363
|
}
|
|
244
364
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
365
|
+
export const Dropdown = provideModuleToComponent(
|
|
366
|
+
ListviewModule,
|
|
367
|
+
({
|
|
368
|
+
listviewProps,
|
|
369
|
+
initialSelectedValue,
|
|
370
|
+
// Here it is important that we get access to the contextualized module
|
|
371
|
+
// so we can forward it to the `Listview` component!
|
|
372
|
+
module,
|
|
373
|
+
}: DropdownProps) => {
|
|
374
|
+
const service = useInject(DropdownService);
|
|
375
|
+
|
|
376
|
+
/* Remaining fancy implementation */
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div className="fancy-dropdown">
|
|
380
|
+
<span>{initialSelectedValue}</span>
|
|
381
|
+
|
|
382
|
+
{/* Here we forward the contextualized module which will be sent from the parent component consuming this component,
|
|
383
|
+
in our case, the `Autocomplete` component. */}
|
|
384
|
+
<Listview module={module} />
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
<hr>
|
|
392
|
+
|
|
393
|
+
> Autocomplete
|
|
394
|
+
|
|
395
|
+
And finally the grand finale!
|
|
396
|
+
|
|
397
|
+
`autocomplete.service.ts`
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
@Injectable()
|
|
401
|
+
export class AutocompleteService {
|
|
402
|
+
constructor(
|
|
403
|
+
readonly inputboxService: InputboxService,
|
|
404
|
+
readonly dropdownService: DropdownService
|
|
405
|
+
) {
|
|
406
|
+
// Here we can override even what the `Dropdown` has already overriden!
|
|
407
|
+
this.dropdownService.listviewService.items = [29, 9, 1969];
|
|
408
|
+
|
|
409
|
+
// However doing the following, will throw an error because the `Inputbox` component
|
|
410
|
+
// at this time is not yet mounted, therefore the `setStateValue` state setter
|
|
411
|
+
// method doesn't exist yet.
|
|
412
|
+
//
|
|
413
|
+
// A better way would be to use a store manager so you can generate your application state through
|
|
414
|
+
// the services, rather than inside the UI (components should be used only to render the data, not to manipulate/manage it).
|
|
415
|
+
this.inputboxService.setValue('xInjection');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* Remaining fancy implementation */
|
|
251
419
|
}
|
|
420
|
+
|
|
421
|
+
export const AutocompleteModule = new ComponentProviderModule({
|
|
422
|
+
identifier: Symbol('AutocompleteModule'),
|
|
423
|
+
imports: [InputboxModule, DropdownModule],
|
|
424
|
+
provides: [AutocompleteService],
|
|
425
|
+
// If we don't plan to share the internal dependencies of the
|
|
426
|
+
// Autocomplete component, then we can omit the `exports` array declaration.
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`autocomplete.tsx`
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
export interface AutocompleteProps {
|
|
434
|
+
inputboxProps: InputboxProps;
|
|
435
|
+
dropdownProps: DropdownProps;
|
|
436
|
+
|
|
437
|
+
currentText: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export const Autocomplete = provideModuleToComponent(
|
|
441
|
+
AutocompleteModule,
|
|
442
|
+
({
|
|
443
|
+
dropdownProps,
|
|
444
|
+
currentText,
|
|
445
|
+
|
|
446
|
+
module,
|
|
447
|
+
}: AutocompleteProps) => {
|
|
448
|
+
const service = useInject(AutocompleteService);
|
|
449
|
+
|
|
450
|
+
console.log(service.dropdownService.listviewService.items);
|
|
451
|
+
// Produces: [29, 9, 1969]
|
|
452
|
+
|
|
453
|
+
/* Remaining fancy implementation */
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<div className="fancy-autocomplete">
|
|
457
|
+
{/* Let's not forget to forward the module to both components we want to control */}
|
|
458
|
+
<Inputbox {...inputboxProps} module={module} >
|
|
459
|
+
<Dropdown {...dropdownProps} module={module} />
|
|
460
|
+
</div>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
This should cover the fundamentals of how you can build a scalable UI by using the `xInjection` Dependency Injection 😊
|
|
467
|
+
|
|
468
|
+
> **Note:** _Keep in mind that both library ([xInjection](https://www.npmjs.com/package/@adimm/x-injection) & [xInjection ReactJS](https://www.npmjs.com/package/@adimm/x-injection-reactjs)) are still young and being developed, therefore the internals and public API may change in the near future._
|
|
469
|
+
|
|
470
|
+
## Unit Tests
|
|
471
|
+
|
|
472
|
+
It is very easy to create mock modules so you can provide them to your components in your unit tests.
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
class ApiService {
|
|
476
|
+
constructor(private readonly userService: UserService) {}
|
|
477
|
+
|
|
478
|
+
async sendRequest<T>(location: LocationParams): Promise<T> {
|
|
479
|
+
// Pseudo Implementation
|
|
480
|
+
return this.sendToLocation(user, location);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private async sendToLocation(user: User, location: any): Promise<any> {}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const ApiModule = new ComponentProviderModule({
|
|
487
|
+
identifier: Symbol('ApiModule'),
|
|
488
|
+
providers: [UserService, ApiService],
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const ApiModuleMocked = new ComponentProviderModule({
|
|
492
|
+
identifier: Symbol('ApiModule_MOCK'),
|
|
493
|
+
providers: [
|
|
494
|
+
{
|
|
495
|
+
provide: UserService,
|
|
496
|
+
useClass: UserService_Mock,
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
provide: ApiService,
|
|
500
|
+
useValue: {
|
|
501
|
+
sendRequest: async (location) => {
|
|
502
|
+
console.log(location);
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
await act(async () => render(<RealComponent module={ApiModuleMocked} />));
|
|
252
510
|
```
|
|
253
511
|
|
|
254
|
-
|
|
512
|
+
Now what you have to do is just to provide the `ApiModuleMocked` instead of the `ApiModule` 😎
|
|
255
513
|
|
|
256
514
|
## Documentation
|
|
257
515
|
|