Skip to content

Commit 5cb51d4

Browse files
committed
feat(dynamic): support @dynamic get module by name
1 parent 9d85daa commit 5cb51d4

File tree

2 files changed

+126
-57
lines changed

2 files changed

+126
-57
lines changed
Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,83 @@
1+
/* eslint-disable no-undef-init */
12
/* eslint-disable @typescript-eslint/no-unused-vars */
23
/* eslint-disable @typescript-eslint/no-explicit-any */
34
import { multiOptional, ServiceIdentifier } from 'reactant-di';
45

5-
import { containerKey, dynamicModulesKey, identifierKey } from '../constants';
6+
import {
7+
containerKey,
8+
dynamicModulesKey,
9+
identifierKey,
10+
modulesKey,
11+
} from '../constants';
612
import type { DynamicModules, PropertyDescriptor } from '../interfaces';
713

8-
type Dynamic = <M extends boolean = false>(
9-
serviceIdentifier: ServiceIdentifier<unknown>,
14+
type Dynamic = <M extends boolean = false, T extends boolean = false>(
15+
serviceIdentifierOrName: ServiceIdentifier<unknown>,
1016
options?: {
1117
/**
1218
* Whether to inject multiple instances.
1319
*/
14-
multiple?: boolean;
20+
multiple?: T extends false ? false : M;
21+
/**
22+
* use token identifier to get service, use name to get service by default.
23+
*/
24+
useToken?: T;
1525
}
1626
) => (
1727
target: object,
1828
key: string | symbol,
1929
descriptor?: PropertyDescriptor<unknown>
2030
) => void;
2131

22-
export const dynamic: Dynamic = (serviceIdentifier, options) => (
23-
target,
24-
key
25-
) => {
26-
const multipleInject = options?.multiple ?? false;
27-
if (multipleInject) {
28-
multiOptional(serviceIdentifier)(class {});
29-
}
30-
function getter(this: any) {
31-
const dynamicModules: DynamicModules = this[dynamicModulesKey];
32-
if (__DEV__ && !dynamicModules) {
32+
export const dynamic: Dynamic =
33+
(serviceIdentifierOrName, options) => (target, key) => {
34+
const multipleInject = options?.multiple ?? false;
35+
const useToken = options?.useToken ?? false;
36+
if (multipleInject) {
37+
multiOptional(serviceIdentifierOrName)(class {});
38+
}
39+
function getter(this: any) {
40+
if (!useToken) {
41+
return this[modulesKey]?.[serviceIdentifierOrName as string];
42+
}
43+
const dynamicModules: DynamicModules = this[dynamicModulesKey];
44+
if (__DEV__ && !dynamicModules) {
45+
throw new Error(
46+
`The property '${key.toString()}' is not readable when class ${
47+
this[identifierKey] ?? this.constructor.name
48+
} is constructing.`
49+
);
50+
}
51+
if (dynamicModules.has(serviceIdentifierOrName)) {
52+
return dynamicModules.get(serviceIdentifierOrName)!.value;
53+
}
54+
let value: unknown = undefined;
55+
try {
56+
const services = this[containerKey]!.getAll(serviceIdentifierOrName);
57+
value = !multipleInject ? services[0] : services;
58+
} catch (e) {
59+
//
60+
}
61+
dynamicModules.set(serviceIdentifierOrName, {
62+
multiple: multipleInject,
63+
value,
64+
});
65+
return value;
66+
}
67+
68+
function setter(this: any, _: unknown) {
3369
throw new Error(
34-
`The property '${key.toString()}' is not readable when class ${
70+
`Cannot assign to read only 'dynamic' injection property '${key.toString()}' of class '${
3571
this[identifierKey] ?? this.constructor.name
36-
} is constructing.`
72+
}'`
3773
);
3874
}
39-
if (dynamicModules.has(serviceIdentifier)) {
40-
return dynamicModules.get(serviceIdentifier)!.value;
41-
}
42-
let value: unknown = null;
43-
try {
44-
const services = this[containerKey]!.getAll(serviceIdentifier);
45-
value = !multipleInject ? services[0] : services;
46-
} catch (e) {
47-
//
48-
}
49-
dynamicModules.set(serviceIdentifier, { multiple: multipleInject, value });
50-
return value;
51-
}
52-
53-
function setter(this: any, _: unknown) {
54-
throw new Error(
55-
`Cannot assign to read only 'dynamic' injection property '${key.toString()}' of class '${
56-
this[identifierKey] ?? this.constructor.name
57-
}'`
58-
);
59-
}
6075

61-
// It should be compatible with the TS decorator and the babel decorator
62-
return {
63-
configurable: true,
64-
enumerable: true,
65-
get: getter,
66-
set: setter,
67-
} as any;
68-
};
76+
// It should be compatible with the TS decorator and the babel decorator
77+
return {
78+
configurable: true,
79+
enumerable: true,
80+
get: getter,
81+
set: setter,
82+
} as any;
83+
};

packages/reactant/test/integration/dynamic.test.tsx

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,75 @@ import {
66
state,
77
load,
88
ModuleRef,
9-
optional,
10-
ModuleOptions,
11-
inject,
12-
ClassProvider,
13-
multiInject,
14-
multiOptional,
159
dynamic,
1610
} from '../..';
1711

18-
test('base `dynamic`', async () => {
12+
test('base `dynamic` use name by default', async () => {
1913
@injectable()
2014
class Counter {
2115
constructor(public moduleRef: ModuleRef) {}
2216

23-
@dynamic('todo')
17+
@dynamic('todoModule')
18+
readonly todo?: ITodo;
19+
20+
@state
21+
count = 0;
22+
23+
@action
24+
increase() {
25+
this.count += 1;
26+
}
27+
28+
get _todo() {
29+
return this.moduleRef.get<ITodo>('todo');
30+
}
31+
}
32+
33+
const app = createApp({
34+
main: Counter,
35+
render: () => {
36+
//
37+
},
38+
});
39+
40+
interface ITodo {
41+
list: string[];
42+
add(text: string): void;
43+
}
44+
45+
@injectable({
46+
name: 'todoModule',
47+
})
48+
class Todo implements ITodo {
49+
@state
50+
list: string[] = [];
51+
52+
@action
53+
add(text: string) {
54+
this.list.push(text);
55+
}
56+
}
57+
expect(Object.values(app.store?.getState())).toEqual([{ count: 0 }]);
58+
expect(app.instance.todo).toBeUndefined();
59+
expect(() => {
60+
// eslint-disable-next-line no-unused-expressions
61+
app.instance._todo;
62+
}).toThrowError(`No matching bindings found for serviceIdentifier: todo`);
63+
await load(app.instance, [{ provide: 'todo', useClass: Todo }]);
64+
expect(Object.values(app.store?.getState())).toEqual([
65+
{ count: 0 },
66+
{ list: [] },
67+
]);
68+
expect(app.instance.todo).toBeInstanceOf(Todo);
69+
expect(app.instance.todo).toBe(app.instance._todo);
70+
});
71+
72+
test('base `dynamic` use token', async () => {
73+
@injectable()
74+
class Counter {
75+
constructor(public moduleRef: ModuleRef) {}
76+
77+
@dynamic('todo', { useToken: true })
2478
todo?: ITodo;
2579

2680
@state
@@ -59,7 +113,7 @@ test('base `dynamic`', async () => {
59113
}
60114
}
61115
expect(Object.values(app.store?.getState())).toEqual([{ count: 0 }]);
62-
expect(app.instance.todo).toBeNull();
116+
expect(app.instance.todo).toBeUndefined();
63117
expect(() => {
64118
// eslint-disable-next-line no-unused-expressions
65119
app.instance._todo;
@@ -76,7 +130,7 @@ test('base `dynamic`', async () => {
76130
test('base `dynamic` with multi-modules', async () => {
77131
@injectable()
78132
class Counter {
79-
@dynamic('todo', { multiple: true })
133+
@dynamic('todo', { multiple: true, useToken: true })
80134
todo?: ITodo[];
81135

82136
@state
@@ -111,7 +165,7 @@ test('base `dynamic` with multi-modules', async () => {
111165
}
112166
}
113167
expect(Object.values(app.store?.getState())).toEqual([{ count: 0 }]);
114-
expect(app.instance.todo).toBeNull();
168+
expect(app.instance.todo).toBeUndefined();
115169
await load(app.instance, [{ provide: 'todo', useClass: Todo }]);
116170
expect(Object.values(app.store?.getState())).toEqual([
117171
{ count: 0 },

0 commit comments

Comments
 (0)