('platform')
31 | ```
32 |
33 | _You can use the same name for a variable and a type in TypeScript._
34 |
35 | ## Class as Dependency
36 |
37 | An ES6 class could be a dependency item. You can declare its dependencies in its constructor. wedi would analyze dependency relation among different classes and instantiate them correctly.
38 |
39 | You can use `Need` to declare that `FileService` depends on `IPlatformService`.
40 |
41 | ```ts
42 | class FileService {
43 | constructor(@Need(IPlatformService) private logService: IPlatformService) {}
44 | }
45 | ```
46 |
47 | wedi would get or instantiates a `IPlatformService` before it instantiates `FileService`. And if it could not instantiate a `IPlatformService` it would throw an error.
48 |
49 | And identifiers created by `createIdentifier` could also be used to define dependency relationship. It's equivalent to the example above.
50 |
51 | ```ts
52 | class SomeService {
53 | constructor(@IPlatformService private platform: IPlatformService) {}
54 | }
55 | ```
56 |
57 | You can also use the `Optional` decorator to declare an optional dependency.
58 |
59 | ```ts
60 | class FileService {
61 | constructor(@Optional(OptionalDependency) private op?: OptionalDependency) {}
62 | }
63 | ```
64 |
65 | If `OptionalDependency` is not provided, wedi would not throw an error but return `null` instead to instantiate `FileService`.
66 |
67 | ## Value or Instance as Dependency
68 |
69 | It's easy to provide a value as dependency.
70 |
71 | ```ts
72 | const configDep = [IConfig, { useValue: '2020' }]
73 | ```
74 |
75 | ## Factory Function as Dependency
76 |
77 | You can create a dependency via `useFactory` that gives the control flow back to you on initializing.
78 |
79 | ```ts
80 | const useDep = [IUserService, {
81 | useFactory: (http: IHTTPService): IUserService => new TimeSerialUserService(http, TIME),
82 | deps: [IHTTPService] // this factory depends on IHTTPService.
83 | }]
84 | ```
85 |
86 | ## Provide Items
87 |
88 | Finally, you should wrap all items in an array and pass them to the constructor of `DependencyCollection`.
89 |
90 | ```ts
91 | const dependencies = [
92 | LogService,
93 | FileService,
94 | [IConfig, { useValue: '2020' }],
95 | [
96 | IUserService,
97 | {
98 | useFactory: (http: IHTTPService): IUserService =>
99 | new TimeSerialUserService(http, TIME),
100 | deps: [IHTTPService]
101 | }
102 | ],
103 | [IHTTPService, WebHTTPService]
104 | ]
105 | ```
106 |
107 | ## Singleton Dependency
108 |
109 | For dependencies that should be singleton in the application, it's recommended to use `registerSingleton`.
110 |
111 | ```ts
112 | registerSingleton(/* a dependency item */)
113 | ```
114 |
115 | Dependencies would be provided by the root provider. In another word, the provider which is constructed without a `parent` parameter.
116 |
117 | ## Lazy Instantiation
118 |
--------------------------------------------------------------------------------
/doc/src/injector.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Dependency Injection
3 | route: /di
4 | ---
5 |
6 | # Dependency Injection
7 |
8 | In case you are not familiar with dependency injection pattern, here are three major concepts in a dependency injection system you should know:
9 |
10 | - **Dependency Item**: Anything that could be used by other classes or React components. Usually they are identified by `key`s. A dependency could be a class, a value or a function, etc.
11 | - **Provider** (a.k.a injector). The manager of dependency items. It _provides_ dependency items and instantiate or evaluate dependency items at consumers' need.
12 | - **Consumer**: They consume dependency items. They use `key`s to get dependency items. A consumers can be a dependency item at the same time.
13 |
14 | ## DependencyCollection
15 |
16 | `DependencyCollection` is used to collect dependencies. Later it would be passed into an injector.
17 |
18 | ```ts
19 | const collection = new DependencyCollection([
20 | // ...items
21 | ])
22 | ```
23 |
24 | ## Injector
25 |
26 | `Injector` is the one who instantiates, provides and manages dependencies. And they will form a layered injection system.
27 |
28 | ```tsx
29 | const injector = new Injector(collection)
30 | ```
31 |
32 | ## Multi-Layered Injector System
33 |
34 | wedi supports multi-layered injector system. In another word, every injector could have child injectors. A child injector could ask its parent injector for a dependency when it could not provide a by itself.
35 |
36 | ## Why Dependency Injection?
37 |
--------------------------------------------------------------------------------
/doc/src/introduction.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Introduction
3 | route: /
4 | ---
5 |
6 | # Introduction
7 |
8 | ## What is wedi?
9 |
10 | **wedi** is a lightweight toolkit to let you use dependency injection (DI) pattern in TypeScript and especially React with TypeScript.
11 |
12 | - Completely opt-in. It's up to you to decide when and where to apply dependency injection pattern.
13 | - Provide a multi-level dependency injection system.
14 | - Support injecting classes, instances, values and factories.
15 | - Support React class component.
16 | - Support React Hooks (functional component).
17 |
18 | You can use wedi to:
19 |
20 | - Mange state of applications
21 | - Reuse logic
22 | - Deal with cross-platform problems
23 | - Write code that is loosely-coupled, easy to understand and maintain
24 |
25 | ## Getting Started
26 |
27 | _This guide assumes basic knowledge of TypeScript, React and dependency injection pattern. If you are totally innocent of any idea above, it might not be the best idea to get started with wedi._
28 |
29 | Install wedi via npm or yarn:
30 |
31 | ```shell
32 | npm install wedi
33 |
34 | # or
35 | yarn add wedi
36 | ```
37 |
38 | Add you need to enable decorator in tsconfig.json.
39 |
40 | ```json
41 | {
42 | "compilerOptions": {
43 | "experimentalDecorators": true
44 | }
45 | }
46 | ```
47 |
48 | ## Declare a Dependency
49 |
50 | Declare something that another class or React component could depend on is very simple. It could just be easy as a ES6 class!
51 |
52 | ```tsx
53 | class AuthenticationService {
54 | avatar = 'https://img.yourcdn.com/avatar'
55 | }
56 | ```
57 |
58 | wedi let you declare other kinds of dependencies such as plain values and factory functions. Read Dependencies for more details.
59 |
60 | ## Use in React
61 |
62 | You could provide a dependency in a React component, and use it in its child components.
63 |
64 | ```tsx
65 | function App() {
66 | const collection = useCollection([AuthenticationService])
67 |
68 | return
69 |
70 |
71 |
72 |
73 | }
74 |
75 | function Profile() {
76 | const authS = useDependency(AuthenticationService)
77 |
78 | return
79 | }
80 | ```
81 |
82 | ## With RxJS
83 |
84 | wedi provide some Hooks that helps you use wedi with RxJS smoothly.
85 |
86 | ```tsx
87 | function ReRenderOnNewValue() {
88 | const notificationS = useDependency(NotificationService)
89 | const val = useDependencyValue(notificationS.data$)
90 |
91 | // re-return when data$ emits a new value
92 | }
93 | ```
94 |
95 | For more, please read With [RxJS](/rx) for more details.
96 |
97 | ## Demo
98 |
99 | Here is a TodoMVC [demo](https://wendellhu95.github.io/wedi-demo) built with wedi.
100 |
101 | ## Links
102 |
103 | - [GitHub Repo](https://github.com/wendellhu95/wedi)
104 | - [Doc](https://wedi.wendellhu.xyz)
105 |
106 | ## License
107 |
108 | MIT. Copyright Wendell Hu 2019-2020.
109 |
--------------------------------------------------------------------------------
/doc/src/react.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: React
3 | route: /react
4 | ---
5 |
6 | # React
7 |
8 | wedi provides API let you use dependency injection in React conveniently.
9 |
10 | ## Class Component as Provider
11 |
12 | The `Provide` decorator could inject items into the decorated component and its child components.
13 |
14 | ```ts
15 | import { Provide } from 'wedi';
16 | import { FileService } from 'services/file';
17 | import { IPlatformService } from 'services/platform';
18 |
19 | @Provide([
20 | FileService,
21 | [IPlatformService, { useClass: MobilePlatformService } ];
22 | ])
23 | class ClassComponent extends Component {
24 | // FileService and IPlatformService is accessible in the component and its children
25 | }
26 | ```
27 |
28 | ## Class Component as Consumer
29 |
30 | If you would like consume dependencies in a class component, you should assign `contextType` to be `InjectionContext` and get dependencies using the `Inject` decorator.
31 |
32 | ```ts
33 | import { Inject, InjectionContext } from 'wedi'
34 | import { IPlatformService } from 'services/platform'
35 |
36 | class ClassConsumer extends Component {
37 | static contextType = InjectionContext
38 |
39 | @Inject(FileService) fileService!: FileService // accessible to all methods of this class
40 | }
41 | ```
42 |
43 | A class component can consume items provided by itself.
44 |
45 | ```ts
46 | import { Inject, InjectionContext, Provide } from 'wedi';
47 | import { IPlatformService } from 'services/platform';
48 |
49 | @Provide([
50 | FileService,
51 | [IPlatformService, { useClass: MobilePlatformService }];
52 | ])
53 | class ClassComponent extends Component {
54 | static contextType = InjectionContext;
55 |
56 | @Inject(IPlatformService) platformService!: IPlatformService; // this is MobilePlatformService
57 | }
58 | ```
59 |
60 | You can pass `true` as the second parameter of `Inject` to indicate that a dependency is optional.
61 |
62 | ```ts
63 | class ClassComponent extends Component {
64 | static contextType = InjectionContext
65 |
66 | @Inject(CanBeNullService, true) canBeNullService?: CanBeNullService // this can be null
67 | }
68 | ```
69 |
70 | ## Functional Component as Provider
71 |
72 | `useCollection` and `InjectionLayer` could make functional components as providers and make sure that dependencies wouldn't get re-instantiated when components re-render.
73 |
74 | ```tsx
75 | import { useCollection, Provider } from 'wedi'
76 |
77 | function FunctionProvider() {
78 | const collection = useCollection([FileService])
79 |
80 | return (
81 |
82 | {/* Child components can use FileService. */}
83 |
84 | )
85 | }
86 | ```
87 |
88 | You could also use injectors directly. But this is only recommended when the injector is outside of the React component tree.
89 |
90 | ```tsx
91 | const injectorFromAnOtherPartOfYourProgram = getInjector()
92 |
93 | function YourReactRoot(props: { injector: Injector }) {
94 | return (
95 |
96 |
97 |
98 | )
99 | }
100 |
101 | ReactDOM.render(
102 | ,
105 | containerEl
106 | )
107 | ```
108 |
109 | In this way, you could easily integrate React with other part of you application easily.
110 |
111 | You can see that when a component tries to get a dependency, it would always ask the **nearest** provider for it, which means you could use scoped state management with wedi.
112 |
113 | ## Functional Component as Consumer
114 |
115 | `useDependency` can help you to hook in dependencies. You can assign the second parameter `true` to mark the injected dependency as optional.
116 |
117 | ```tsx
118 | import { useDependency } from 'wedi'
119 | import { FileService } from 'services/file'
120 | import { LogService } from 'services/log'
121 |
122 | function FunctionConsumer() {
123 | const fileService: FileService = useDependency(FileService)
124 | const log: LogService | null = useDependency(LogService, true)
125 |
126 | return {
127 | /* use dependencies */
128 | }
129 | }
130 | ```
131 |
132 | Note that functional cannot consume items provided by itself. By you could use `connectProvider` to make things easier.
133 |
134 | ```tsx
135 | import { FileService } from 'services/file'
136 | import { LogService } from 'services/log'
137 |
138 | const FunctionConsumer = connectProvider((function() {
139 | const fileService: FileService = useDependency(FileService)
140 | const log: LogService | null = useDependency(LogService, true)
141 |
142 | return {
143 | /* use dependencies */
144 | }), {
145 | collection: new DependencyCollection([FileService, LogService])
146 | })
147 | }
148 | ```
149 |
150 | ## Multi-Layered Injector System
151 |
152 | injectors of wedi could have child components and React components could have child components. Combined, you could use multi-layered injector system in React seamlessly.
153 |
154 | ```tsx
155 | @Provide([
156 | [IConfig, { useValue: 'A' }],
157 | [IConfigRoot, { useValue: 'inRoot' }]
158 | ])
159 | class ParentProvider extends Component {
160 | render() {
161 | return
162 | }
163 | }
164 |
165 | @Provide([[IConfig, { useValue: 'B' }]])
166 | class ChildProvider extends Component {
167 | render() {
168 | return
169 | }
170 | }
171 |
172 | function Consumer() {
173 | const config = useDependency(IConfig)
174 | const rootConfig = useDependency(IConfigRoot)
175 |
176 | return (
177 |
178 | {config}, {rootConfig}
179 |
// B, inRoot
180 | )
181 | }
182 | ```
183 |
184 | ## Inject React Component
185 |
186 | You could inject React Component as a dependency, too.
187 |
188 | ```tsx
189 | const IDropdown = createIdentifier('dropdown')
190 | const IConfig = createIdentifier('config')
191 |
192 | const WebDropdown = function() {
193 | const dep = useDependency(IConfig) // could use dependencies in its host environment
194 | return WeDropdown, {dep}
195 | }
196 |
197 | @Provide([
198 | [IDropdown, { useValue: WebDropdown }],
199 | [IConfig, { useValue: 'wedi' }]
200 | ])
201 | class Header extends Component {
202 | static contextType = InjectionContext
203 |
204 | @Inject(IDropdown) private dropdown: any
205 |
206 | render() {
207 | const Dropdown = this.dropdown
208 | return // WeDropdown, wedi
209 | }
210 | }
211 | ```
212 |
--------------------------------------------------------------------------------
/doc/src/rx.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: With RxJS
3 | route: /rx
4 | ---
5 |
6 | # With RxJS
7 |
8 | _This guide assumes that you have a basic knowledge of RxJS and reactive programming._
9 |
10 | wedi gives you a clear model for state management and logic reuse. With RxJS, wedi brings reactive programming to your application.
11 |
12 | ## An Example
13 |
14 | Here is a real-life example. Assuming that we need to fetch latest notifications every 10 seconds after the application bootstraps and the header bar is rendered on the screen. And when the previous request for notification gets delayed, we need to drop it when the current request is sent.
15 |
16 | See the RxJS + wedi version:
17 |
18 | ```tsx
19 | class NotificationService implements Disposable {
20 | destroy$: Subject()
21 | notifications$: Observable>
22 |
23 | constructor(
24 | @Need(ILifecycleService) lifecycleS: ILifecycleService,
25 | @Need(IHttpService) httpS: IHttpService
26 | ) {
27 | this.destroy = new Subject();
28 | this.notificationS = this.lifecycleS.bootstrap$
29 | .pipe(
30 | take(1),
31 | concatMap(() => interval(10000)),
32 | concatMap(() => httpS.request(/* some url */)),
33 | startWith([]),
34 | takeUntil(this.destroy$),
35 | )
36 | }
37 |
38 | dispose(): void {
39 | this.destroy$.next()
40 | this.destroy$.complete()
41 | }
42 | }
43 |
44 | function Header() {
45 | const collection = useCollection([NotificationService])
46 |
47 | return
48 |
49 |
50 |
51 |
52 | }
53 |
54 |
55 | function NotificationDisplayer() {
56 | const notiS = useDependency(NotificationService)
57 | const notifications = useDependencyValue(notiS.notifications$)
58 |
59 | // render notifications
60 | }
61 | ```
62 |
63 | You can see that the code is concise, declarative, easy to understand and maintain. Logic is completely moved from components to services.
64 |
65 | In fact, you could use any reactive programming library with wedi since it just provide a framework on which you can put observables and subscriptions. But wedi provides Hooks that works with RxJS to make your life easier.
66 |
67 | ## Value Context
68 |
69 | Sometimes you need to subscribe to the same observable value in a component and its child components. You could use `useDependencyValue`. But this would cause unnecessary reconciliation. You could subscribe in the parent component and pass values to the child components, but it would be troublesome if the child component is deeply wrapped. So it's nature to use React Context here. wedi provides `useDependencyContext` and `useDependencyContextValue` to make this easier.
70 |
71 | ```tsx
72 | function Parent() {
73 | const authS = useDependency(AuthenticationService)
74 | const { Provider: AuthProvider } = useDependencyContext(authS.auth$, {})
75 |
76 | return (
77 |
78 |
79 |
80 | )
81 | }
82 |
83 | function Child() {
84 | const authS = useDependency(AuthenticationService)
85 | const auth = useDependencyContextValue(authS.auth$)
86 |
87 | // adjust UI according to authentication info
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | The demo is using 'react' and 'react-dom' from the outside 'node_modules' to avoid 'calling hooks warning'.
2 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wedi-demo",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "start": "webpack-dev-server --mode development --hot --progress --color --port 3000 --open",
6 | "build": "webpack -p --progress --colors"
7 | },
8 | "devDependencies": {
9 | "@babel/core": "^7.2.2",
10 | "@types/classnames": "^2.2.9",
11 | "@types/node": "^10.12.18",
12 | "@types/react": "^16.8.2",
13 | "@types/react-dom": "^16.8.0",
14 | "@types/webpack": "^4.4.23",
15 | "@types/webpack-env": "1.13.6",
16 | "babel-loader": "^8.0.5",
17 | "classnames": "^2.2.6",
18 | "clean-webpack-plugin": "^2.0.1",
19 | "css-loader": "^2.1.0",
20 | "html-loader": "^1.0.0-alpha.0",
21 | "html-webpack-plugin": "^4.0.0-alpha",
22 | "husky": "^3.1.0",
23 | "jest": "^24.9.0",
24 | "react-hot-loader": "^4.12.18",
25 | "style-loader": "^0.23.1",
26 | "ts-loader": "^5.3.3",
27 | "tslint": "^5.20.1",
28 | "typescript": "^3.7.0",
29 | "webpack": "^4.28.4",
30 | "webpack-cli": "^3.2.1",
31 | "webpack-dev-server": "^3.1.14",
32 | "webpack-merge": "^4.2.2"
33 | },
34 | "dependencies": {
35 | "wedi": "link:../"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/example/src/App.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | font-weight: 300;
34 | }
35 |
36 | :focus {
37 | outline: 0;
38 | }
39 |
40 | .hidden {
41 | display: none;
42 | }
43 |
44 | .todoapp {
45 | background: #fff;
46 | margin: 130px 0 40px 0;
47 | position: relative;
48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
49 | }
50 |
51 | .todoapp input::-webkit-input-placeholder {
52 | font-style: italic;
53 | font-weight: 300;
54 | color: #e6e6e6;
55 | }
56 |
57 | .todoapp input::-moz-placeholder {
58 | font-style: italic;
59 | font-weight: 300;
60 | color: #e6e6e6;
61 | }
62 |
63 | .todoapp input::input-placeholder {
64 | font-style: italic;
65 | font-weight: 300;
66 | color: #e6e6e6;
67 | }
68 |
69 | .todoapp h1 {
70 | position: absolute;
71 | top: -155px;
72 | width: 100%;
73 | font-size: 100px;
74 | font-weight: 100;
75 | text-align: center;
76 | color: rgba(175, 47, 47, 0.15);
77 | -webkit-text-rendering: optimizeLegibility;
78 | -moz-text-rendering: optimizeLegibility;
79 | text-rendering: optimizeLegibility;
80 | }
81 |
82 | .new-todo,
83 | .edit {
84 | position: relative;
85 | margin: 0;
86 | width: 100%;
87 | font-size: 24px;
88 | font-family: inherit;
89 | font-weight: inherit;
90 | line-height: 1.4em;
91 | border: 0;
92 | color: inherit;
93 | padding: 6px;
94 | border: 1px solid #999;
95 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
96 | box-sizing: border-box;
97 | -webkit-font-smoothing: antialiased;
98 | -moz-osx-font-smoothing: grayscale;
99 | }
100 |
101 | .new-todo {
102 | padding: 16px 16px 16px 60px;
103 | border: none;
104 | background: rgba(0, 0, 0, 0.003);
105 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
106 | }
107 |
108 | .main {
109 | position: relative;
110 | z-index: 2;
111 | border-top: 1px solid #e6e6e6;
112 | }
113 |
114 | .toggle-all {
115 | text-align: center;
116 | border: none; /* Mobile Safari */
117 | opacity: 0;
118 | position: absolute;
119 | }
120 |
121 | .toggle-all + label {
122 | width: 60px;
123 | height: 34px;
124 | font-size: 0;
125 | position: absolute;
126 | top: 14px;
127 | left: -13px;
128 | -webkit-transform: rotate(90deg);
129 | transform: rotate(90deg);
130 | }
131 |
132 | .toggle-all + label:before {
133 | content: '❯';
134 | font-size: 22px;
135 | color: #e6e6e6;
136 | padding: 10px 27px 10px 27px;
137 | }
138 |
139 | .toggle-all:checked + label:before {
140 | color: #737373;
141 | }
142 |
143 | .todo-list {
144 | margin: 0;
145 | padding: 0;
146 | list-style: none;
147 | }
148 |
149 | .todo-list li {
150 | position: relative;
151 | font-size: 24px;
152 | border-bottom: 1px solid #ededed;
153 | }
154 |
155 | .todo-list li:last-child {
156 | border-bottom: none;
157 | }
158 |
159 | .todo-list li.editing {
160 | border-bottom: none;
161 | padding: 0;
162 | }
163 |
164 | .todo-list li.editing .edit {
165 | display: block;
166 | width: 506px;
167 | padding: 12px 16px;
168 | margin: 0 0 0 43px;
169 | }
170 |
171 | .todo-list li.editing .view {
172 | display: none;
173 | }
174 |
175 | .todo-list li .toggle {
176 | text-align: center;
177 | width: 40px;
178 | /* auto, since non-WebKit browsers doesn't support input styling */
179 | height: auto;
180 | position: absolute;
181 | top: 0;
182 | bottom: 0;
183 | margin: auto 0;
184 | border: none; /* Mobile Safari */
185 | -webkit-appearance: none;
186 | appearance: none;
187 | }
188 |
189 | .todo-list li .toggle {
190 | opacity: 0;
191 | }
192 |
193 | .todo-list li .toggle + label {
194 | /*
195 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
196 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
197 | */
198 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
199 | background-repeat: no-repeat;
200 | background-position: center left;
201 | }
202 |
203 | .todo-list li .toggle:checked + label {
204 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
205 | }
206 |
207 | .todo-list li label {
208 | word-break: break-all;
209 | padding: 15px 15px 15px 60px;
210 | display: block;
211 | line-height: 1.2;
212 | transition: color 0.4s;
213 | }
214 |
215 | .todo-list li.completed label {
216 | color: #d9d9d9;
217 | text-decoration: line-through;
218 | }
219 |
220 | .todo-list li .destroy {
221 | display: none;
222 | position: absolute;
223 | top: 0;
224 | right: 10px;
225 | bottom: 0;
226 | width: 40px;
227 | height: 40px;
228 | margin: auto 0;
229 | font-size: 30px;
230 | color: #cc9a9a;
231 | margin-bottom: 11px;
232 | transition: color 0.2s ease-out;
233 | }
234 |
235 | .todo-list li .destroy:hover {
236 | color: #af5b5e;
237 | }
238 |
239 | .todo-list li .destroy:after {
240 | content: '×';
241 | }
242 |
243 | .todo-list li:hover .destroy {
244 | display: block;
245 | }
246 |
247 | .todo-list li .edit {
248 | display: none;
249 | }
250 |
251 | .todo-list li.editing:last-child {
252 | margin-bottom: -1px;
253 | }
254 |
255 | .footer {
256 | color: #777;
257 | padding: 10px 15px;
258 | height: 20px;
259 | text-align: center;
260 | border-top: 1px solid #e6e6e6;
261 | }
262 |
263 | .footer:before {
264 | content: '';
265 | position: absolute;
266 | right: 0;
267 | bottom: 0;
268 | left: 0;
269 | height: 50px;
270 | overflow: hidden;
271 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
272 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
273 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
274 | }
275 |
276 | .todo-count {
277 | float: left;
278 | text-align: left;
279 | }
280 |
281 | .todo-count strong {
282 | font-weight: 300;
283 | }
284 |
285 | .filters {
286 | margin: 0;
287 | padding: 0;
288 | list-style: none;
289 | position: absolute;
290 | right: 0;
291 | left: 0;
292 | }
293 |
294 | .filters li {
295 | display: inline;
296 | }
297 |
298 | .filters li a {
299 | color: inherit;
300 | margin: 3px;
301 | padding: 3px 7px;
302 | text-decoration: none;
303 | border: 1px solid transparent;
304 | border-radius: 3px;
305 | }
306 |
307 | .filters li a:hover {
308 | border-color: rgba(175, 47, 47, 0.1);
309 | }
310 |
311 | .filters li a.selected {
312 | border-color: rgba(175, 47, 47, 0.2);
313 | }
314 |
315 | .clear-completed,
316 | html .clear-completed:active {
317 | float: right;
318 | position: relative;
319 | line-height: 20px;
320 | text-decoration: none;
321 | cursor: pointer;
322 | }
323 |
324 | .clear-completed:hover {
325 | text-decoration: underline;
326 | }
327 |
328 | .info {
329 | margin: 65px auto 0;
330 | color: #bfbfbf;
331 | font-size: 10px;
332 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
333 | text-align: center;
334 | }
335 |
336 | .info p {
337 | line-height: 1;
338 | }
339 |
340 | .info a {
341 | color: inherit;
342 | text-decoration: none;
343 | font-weight: 400;
344 | }
345 |
346 | .info a:hover {
347 | text-decoration: underline;
348 | }
349 |
350 | /*
351 | Hack to remove background from Mobile Safari.
352 | Can't use it globally since it destroys checkboxes in Firefox
353 | */
354 | @media screen and (-webkit-min-device-pixel-ratio: 0) {
355 | .toggle-all,
356 | .todo-list li .toggle {
357 | background: none;
358 | }
359 |
360 | .todo-list li .toggle {
361 | height: 40px;
362 | }
363 | }
364 |
365 | @media (max-width: 430px) {
366 | .footer {
367 | height: 50px;
368 | }
369 |
370 | .filters {
371 | bottom: 10px;
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { KeyboardEvent, useRef } from 'react';
2 | import { hot } from 'react-hot-loader';
3 | import { Provider, useCollection, useDependency, useUpdateBinder } from 'wedi';
4 |
5 | import './App.css';
6 |
7 | import Footer from './Footer';
8 | import { RouterService } from './services/router';
9 | import { StateService } from './services/state';
10 | import { TodoService } from './services/todo';
11 | import TodoItem from './TodoItem';
12 |
13 | function AppContainer() {
14 | const collection = useCollection([TodoService, StateService, RouterService]);
15 |
16 | return (
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | function TodoMVC() {
24 | const stateService = useDependency(StateService)!;
25 | const todoService = useDependency(TodoService)!;
26 | const inputRef = useRef(null);
27 |
28 | useUpdateBinder(stateService.updated$.asObservable());
29 | useUpdateBinder(todoService.updated$.asObservable());
30 |
31 | function handleKeydown(e: KeyboardEvent): void {
32 | if (e.keyCode !== 13) {
33 | return;
34 | }
35 |
36 | e.preventDefault();
37 |
38 | const val = inputRef.current?.value;
39 |
40 | if (val) {
41 | todoService.addTodo(val);
42 | inputRef.current!.value = '';
43 | }
44 | }
45 |
46 | const todoItems = todoService.shownTodos.map((todo) => {
47 | return ;
48 | });
49 |
50 | const todoPart = todoService.todoCount ? (
51 |
62 | ) : null;
63 |
64 | const footerPart = todoService.todoCount ? : null;
65 |
66 | return (
67 |
68 |
79 | {todoPart}
80 | {footerPart}
81 |
82 | );
83 | }
84 |
85 | export default hot(module)(() => );
86 |
--------------------------------------------------------------------------------
/example/src/Footer.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 |
4 | import { useDependency } from 'wedi';
5 |
6 | import { SHOWING, StateService } from './services/state';
7 | import { TodoService } from './services/todo';
8 | import { pluralize } from './utils/pluralize';
9 |
10 | export default function Footer() {
11 | const todoService = useDependency(TodoService)!;
12 | const stateService = useDependency(StateService)!;
13 |
14 | return (
15 |
16 |
17 | {todoService.activeTodoCount} {' '}
18 | {pluralize(todoService.activeTodoCount, 'item')} left
19 |
20 |
52 | {todoService.completedCount > 0 ? (
53 | todoService.clearCompleted()}
56 | >
57 | Clear completed
58 |
59 | ) : null}
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/example/src/TodoItem.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React, { FormEvent, KeyboardEvent, useRef, useState } from 'react';
3 |
4 | import { useDependency } from 'wedi';
5 |
6 | import { StateService } from './services/state';
7 | import { ITodo, TodoService } from './services/todo';
8 |
9 | export interface ITodoItemProps {
10 | key: string;
11 | todo: ITodo;
12 | onEdit?(todo: ITodo): void;
13 | onSave?(todo: ITodo): void;
14 | onCancel?(): void;
15 | }
16 |
17 | export default function TodoItem(props: ITodoItemProps) {
18 | const { todo } = props;
19 |
20 | const [inputValue, setInputValue] = useState(todo.title);
21 | const inputRef = useRef(null);
22 | const todoService = useDependency(TodoService);
23 | const stateService = useDependency(StateService);
24 |
25 | const handleEdit = function() {
26 | setInputValue(todo.title);
27 |
28 | stateService?.setEditing(todo.id);
29 |
30 | setTimeout(() => inputRef?.current!.focus(), 16);
31 | };
32 |
33 | const handleSubmit = function(e: FormEvent) {
34 | const val = inputValue.trim();
35 |
36 | stateService?.setEditing('');
37 |
38 | if (val) {
39 | setInputValue(val);
40 | todoService?.save(todo, val);
41 | } else {
42 | todoService?.destroy(todo);
43 | }
44 | };
45 |
46 | const handleKeydown = function(e: KeyboardEvent) {
47 | if (e.keyCode === 27) {
48 | setInputValue(todo.title);
49 | } else if (e.keyCode === 13) {
50 | handleSubmit(e);
51 | }
52 | };
53 |
54 | return (
55 |
61 |
62 | todoService?.toggle(todo)}
67 | />
68 | handleEdit()}>{todo.title}
69 | todoService?.destroy(todo)}
72 | >
73 |
74 | handleSubmit(e)}
79 | onChange={(e) => setInputValue(e.target.value)}
80 | onKeyDown={(e) => handleKeydown(e)}
81 | />
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/example/src/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 | React + DI • TodoMVC
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { registerSingleton } from 'wedi';
4 |
5 | import App from './App';
6 |
7 | import { IStoreService } from './services/store/store';
8 | import { LocalStoreService } from './services/store/store.web';
9 |
10 | registerSingleton(IStoreService, LocalStoreService);
11 |
12 | ReactDOM.render( , document.getElementsByClassName('todoapp')[0]);
13 |
--------------------------------------------------------------------------------
/example/src/services/router.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 |
3 | export class RouterService {
4 | router$ = new Subject();
5 |
6 | constructor() {
7 | window.addEventListener('hashchange', (e) => {
8 | const url = e.newURL;
9 | const segment = url.split('#')[1] || '/';
10 |
11 | this.router$.next(segment);
12 | });
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/src/services/state.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 | import { Need } from 'wedi';
3 |
4 | import { RouterService } from './router';
5 |
6 | export enum SHOWING {
7 | ALL_TODOS,
8 | ACTIVE_TODOS,
9 | COMPLETED_TODOS
10 | }
11 |
12 | export class StateService {
13 | nowShowing: SHOWING = SHOWING.ALL_TODOS;
14 | editing?: string;
15 | updated$ = new Subject();
16 |
17 | constructor(@Need(RouterService) private routerService: RouterService) {
18 | this.routerService.router$.subscribe((router) => {
19 | this.nowShowing =
20 | router === '/active'
21 | ? SHOWING.ACTIVE_TODOS
22 | : router === '/completed'
23 | ? SHOWING.COMPLETED_TODOS
24 | : SHOWING.ALL_TODOS;
25 | this.updated$.next();
26 | });
27 | }
28 |
29 | setEditing(id: string): void {
30 | this.editing = id;
31 | this.update();
32 | }
33 |
34 | private update(): void {
35 | this.updated$.next();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/example/src/services/store/store.ts:
--------------------------------------------------------------------------------
1 | import { createIdentifier } from 'wedi';
2 |
3 | export interface IStoreService {
4 | store(namespace: string): any;
5 | store(namespace: string, data: any): void;
6 | }
7 |
8 | export const IStoreService = createIdentifier('store');
9 |
--------------------------------------------------------------------------------
/example/src/services/store/store.web.ts:
--------------------------------------------------------------------------------
1 | import { IStoreService } from './store';
2 |
3 | export class LocalStoreService implements IStoreService {
4 | store(namespace: string): any;
5 | store(namespace: string, data: any): void;
6 | store(namespace: string, data?: any): void | any {
7 | if (data) {
8 | return localStorage.setItem(namespace, JSON.stringify(data));
9 | } else {
10 | const store = localStorage.getItem(namespace);
11 | return (store && JSON.parse(store)) || [];
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/src/services/todo.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 | import { Need } from 'wedi';
3 |
4 | import { SHOWING, StateService } from './state';
5 | import { IStoreService } from './store/store';
6 |
7 | function uuid(): string {
8 | /*jshint bitwise:false */
9 | let i;
10 | let random;
11 | let id = '';
12 |
13 | for (i = 0; i < 32; i++) {
14 | random = (Math.random() * 16) | 0;
15 | if (i === 8 || i === 12 || i === 16 || i === 20) {
16 | id += '-';
17 | }
18 | id += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16);
19 | }
20 |
21 | return id;
22 | }
23 |
24 | export interface ITodo {
25 | id: string;
26 | title: string;
27 | completed: boolean;
28 | }
29 |
30 | /**
31 | * Storing all todo items, and provide methods to manipulate them.
32 | */
33 | export class TodoService {
34 | todos: ITodo[];
35 | updated$ = new Subject();
36 |
37 | get shownTodos(): ITodo[] {
38 | return this.todos.filter((todo) => {
39 | switch (this.stateService.nowShowing) {
40 | case SHOWING.ACTIVE_TODOS:
41 | return !todo.completed;
42 | case SHOWING.COMPLETED_TODOS:
43 | return todo.completed;
44 | default:
45 | return true;
46 | }
47 | });
48 | }
49 |
50 | get todoCount(): number {
51 | return this.todos.length;
52 | }
53 |
54 | get activeTodoCount(): number {
55 | return this.todos.reduce(
56 | (acc, todo) => (todo.completed ? acc : acc + 1),
57 | 0
58 | );
59 | }
60 |
61 | get completedCount(): number {
62 | return this.todos.length - this.activeTodoCount;
63 | }
64 |
65 | constructor(
66 | @Need(StateService) private stateService: StateService,
67 | @IStoreService private storeService: IStoreService
68 | ) {
69 | this.todos = this.storeService.store('TODO');
70 | }
71 |
72 | inform() {
73 | this.storeService.store('TODO', this.todos);
74 | this.updated$.next();
75 | }
76 |
77 | addTodo(title: string): void {
78 | this.todos = this.todos.concat({
79 | id: uuid(),
80 | title: title,
81 | completed: false
82 | });
83 |
84 | this.inform();
85 | }
86 |
87 | toggleAll(checked: boolean): void {
88 | this.todos = this.todos.map((todo: ITodo) => ({
89 | ...todo,
90 | completed: checked
91 | }));
92 |
93 | this.inform();
94 | }
95 |
96 | toggle(todoToToggle: ITodo) {
97 | this.todos = this.todos.map((todo: ITodo) => {
98 | return todo !== todoToToggle
99 | ? todo
100 | : { ...todo, completed: !todo.completed };
101 | });
102 |
103 | this.inform();
104 | }
105 |
106 | destroy(todo: ITodo) {
107 | this.todos = this.todos.filter((candidate) => candidate !== todo);
108 |
109 | this.inform();
110 | }
111 |
112 | save(todoToSave: ITodo, text: string) {
113 | this.todos = this.todos.map((todo) =>
114 | todo !== todoToSave ? todo : { ...todo, title: text }
115 | );
116 |
117 | this.inform();
118 | }
119 |
120 | clearCompleted() {
121 | this.todos = this.todos.filter((todo) => !todo.completed);
122 |
123 | this.inform();
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/example/src/utils/extend.ts:
--------------------------------------------------------------------------------
1 | export function extend(...objs: any[]): any {
2 | const newObj: any = {};
3 | for (let i = 0; i < objs.length; i++) {
4 | const obj = objs[i];
5 | for (const key in obj) {
6 | if (obj.hasOwnProperty(key)) {
7 | newObj[key] = obj[key];
8 | }
9 | }
10 | }
11 | return newObj;
12 | }
13 |
--------------------------------------------------------------------------------
/example/src/utils/pluralize.ts:
--------------------------------------------------------------------------------
1 | export function pluralize(count: number, word: string): string {
2 | return count === 1 ? word : word + 's';
3 | }
4 |
--------------------------------------------------------------------------------
/example/src/utils/uuid.ts:
--------------------------------------------------------------------------------
1 | export function uuid(): string {
2 | /*jshint bitwise:false */
3 | let i;
4 | let random;
5 | let id = '';
6 |
7 | for (i = 0; i < 32; i++) {
8 | random = (Math.random() * 16) | 0;
9 | if (i === 8 || i === 12 || i === 16 || i === 20) {
10 | id += '-';
11 | }
12 | id += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16);
13 | }
14 |
15 | return id;
16 | }
17 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "jsx": "react",
5 | "lib": [
6 | "esnext",
7 | "dom"
8 | ],
9 | "sourceMap": true,
10 | "target": "es5",
11 | "outDir": "dist",
12 | "module": "commonjs",
13 | "moduleResolution": "node",
14 | "strict": true,
15 | "declaration": true,
16 | "allowSyntheticDefaultImports": true,
17 | "experimentalDecorators": true,
18 | "noUnusedLocals": true,
19 | "esModuleInterop": true,
20 | "types": [
21 | "node",
22 | "jest"
23 | ],
24 | "typeRoots": [
25 | "./node_modules/@types"
26 | ]
27 | },
28 | "include": [
29 | "src/**/*"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const package = require('./package.json');
4 |
5 | // constiables
6 | const isProduction =
7 | process.argv.indexOf('-p') >= 0 || process.env.NODE_ENV === 'production';
8 | const sourcePath = path.join(__dirname, './src');
9 | const outPath = path.join(__dirname, './demo');
10 |
11 | // plugins
12 | const HtmlWebpackPlugin = require('html-webpack-plugin');
13 | const CleanWebpackPlugin = require('clean-webpack-plugin');
14 |
15 | module.exports = {
16 | context: sourcePath,
17 | entry: {
18 | app: './main.tsx'
19 | },
20 | output: {
21 | path: outPath,
22 | filename: isProduction ? '[contenthash].js' : '[hash].js',
23 | chunkFilename: isProduction ? '[name].[contenthash].js' : '[name].[hash].js'
24 | },
25 | target: 'web',
26 | resolve: {
27 | extensions: ['.js', '.ts', '.tsx'],
28 | // Fix webpack's default behavior to not load packages with jsnext:main module
29 | // (jsnext:main directs not usually distributable es6 format, but es6 sources)
30 | mainFields: ['module', 'browser', 'main'],
31 | alias: {
32 | lib: path.resolve(__dirname, 'src/lib'),
33 | example: path.resolve(__dirname, 'src/example')
34 | }
35 | },
36 | module: {
37 | rules: [
38 | {
39 | test: /\.tsx?$/,
40 | use: [
41 | !isProduction && {
42 | loader: 'babel-loader',
43 | options: { plugins: ['react-hot-loader/babel'] }
44 | },
45 | {
46 | loader: 'ts-loader'
47 | }
48 | ].filter(Boolean)
49 | },
50 | {
51 | test: /\.css$/,
52 | use: ['style-loader', 'css-loader']
53 | },
54 | { test: /\.html$/, use: 'html-loader' }
55 | ]
56 | },
57 | optimization: {
58 | splitChunks: {
59 | name: true,
60 | cacheGroups: {
61 | commons: {
62 | chunks: 'initial',
63 | minChunks: 2
64 | },
65 | vendors: {
66 | test: /[\\/]node_modules[\\/]/,
67 | chunks: 'all',
68 | filename: isProduction
69 | ? 'vendor.[contenthash].js'
70 | : 'vendor.[hash].js',
71 | priority: -10
72 | }
73 | }
74 | },
75 | runtimeChunk: true
76 | },
77 | plugins: [
78 | new webpack.EnvironmentPlugin({
79 | NODE_ENV: 'development', // use 'development' unless process.env.NODE_ENV is defined
80 | DEBUG: false
81 | }),
82 | new CleanWebpackPlugin(),
83 | new HtmlWebpackPlugin({
84 | template: 'assets/index.html',
85 | minify: {
86 | minifyJS: true,
87 | minifyCSS: true,
88 | removeComments: true,
89 | useShortDoctype: true,
90 | collapseWhitespace: true,
91 | collapseInlineTagWhitespace: true
92 | },
93 | append: {
94 | head: ``
95 | },
96 | meta: {
97 | title: package.name,
98 | description: package.description,
99 | keywords: Array.isArray(package.keywords)
100 | ? package.keywords.join(',')
101 | : undefined
102 | }
103 | })
104 | ],
105 | devServer: {
106 | contentBase: sourcePath,
107 | hot: true,
108 | inline: true,
109 | historyApiFallback: {
110 | disableDotRule: true
111 | },
112 | stats: 'minimal',
113 | clientLogLevel: 'warning'
114 | },
115 | // https://webpack.js.org/configuration/devtool/
116 | devtool: isProduction ? 'hidden-source-map' : 'cheap-module-eval-source-map',
117 | node: {
118 | // workaround for webpack-dev-server issue
119 | // https://github.com/webpack/webpack-dev-server/issues/60#issuecomment-103411179
120 | fs: 'empty',
121 | net: 'empty'
122 | }
123 | };
124 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['/test'],
3 | testMatch: ['**/*.test.ts', '**/*.test.tsx'],
4 | transform: {
5 | '^.+\\.(ts|tsx)$': 'ts-jest'
6 | },
7 | moduleNameMapper: {
8 | wedi: '/src/index.ts'
9 | },
10 | moduleDirectories: ['.', 'src', 'node_modules']
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wedi",
3 | "version": "0.6.0",
4 | "author": "Wendell Hu ",
5 | "description": "A lightweight dependency injection (DI) library for TypeScript, along with a binding for React.",
6 | "main": "./dist/index.js",
7 | "module": "./esm/index.js",
8 | "react-native": "./esm/index.js",
9 | "types": "./dist/index.d.ts",
10 | "files": [
11 | "dist/**",
12 | "esm/**"
13 | ],
14 | "scripts": {
15 | "start": "webpack-dev-server --mode development --hot --progress --color --port 3000 --open",
16 | "build": "npm run build:esm && npm run build:cjs",
17 | "build:cjs": "ncc build src/index.ts -o dist -m -e react",
18 | "build:esm": "tsc --module ES6 --outDir esm",
19 | "test": "jest --coverage",
20 | "prettier": "prettier --write \"./{src,test}/**/*.{ts,tsx,css}\" && prettier --write \"./doc/src/**/*.{ts,tsx,mdx}\""
21 | },
22 | "devDependencies": {
23 | "@testing-library/react": "^9.4.0",
24 | "@types/jest": "^24.0.25",
25 | "@types/node": "^10.12.18",
26 | "@types/react": "^16.8.2",
27 | "@types/react-dom": "^16.8.0",
28 | "@types/webpack": "^4.4.23",
29 | "@types/webpack-env": "1.13.6",
30 | "@zeit/ncc": "^0.21.0",
31 | "codecov": "^3.5.0",
32 | "jest": "^24.9.0",
33 | "prettier": "^1.19.1",
34 | "react": "^16.8.1",
35 | "react-dom": "^16.8.1",
36 | "rxjs": "^6.5.4",
37 | "ts-jest": "^24.2.0",
38 | "tslint": "^5.20.1",
39 | "typescript": "^3.7.0"
40 | },
41 | "peerDependencies": {
42 | "react": "^16.8.1",
43 | "react-dom": "^16.8.1",
44 | "rxjs": "^6.5.4"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/scripts/jest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is the entry for debug single test file in vscode
3 | *
4 | * Not using node_modules/.bin/jest due to cross platform issues, see
5 | * https://github.com/microsoft/vscode-recipes/issues/107
6 | */
7 | require('jest').run(process.argv);
8 |
--------------------------------------------------------------------------------
/src/collection.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DependencyItem,
3 | DependencyKey,
4 | DependencyValue,
5 | Disposable,
6 | InitPromise,
7 | isDisposable,
8 | Ctor
9 | } from './typings'
10 |
11 | export class DependencyCollection implements Disposable {
12 | public disposed: boolean = false
13 |
14 | private readonly items = new Map<
15 | DependencyKey,
16 | DependencyValue | any
17 | >()
18 |
19 | constructor(deps: DependencyItem[] = []) {
20 | for (const dep of deps) {
21 | if (dep instanceof Array) {
22 | const [depKey, depItem] = dep
23 | this.add(depKey, depItem)
24 | } else {
25 | this.add(dep)
26 | }
27 | }
28 | }
29 |
30 | add(ctor: Ctor): void
31 | add(key: DependencyKey, item: DependencyValue | T): void
32 | add(ctorOrKey: DependencyKey, item?: DependencyValue | T): void {
33 | this.ensureCollectionNotDisposed()
34 |
35 | if (item) {
36 | this.items.set(ctorOrKey, item)
37 | } else {
38 | this.items.set(ctorOrKey, new InitPromise(ctorOrKey as Ctor))
39 | }
40 | }
41 |
42 | has(key: DependencyKey): boolean {
43 | this.ensureCollectionNotDisposed()
44 |
45 | return this.items.has(key)
46 | }
47 |
48 | get(key: DependencyKey): T | DependencyValue | undefined {
49 | this.ensureCollectionNotDisposed()
50 |
51 | return this.items.get(key)
52 | }
53 |
54 | dispose(): void {
55 | this.disposed = true
56 |
57 | this.items.forEach((item) => {
58 | if (isDisposable(item)) {
59 | item.dispose()
60 | }
61 | })
62 | }
63 |
64 | private ensureCollectionNotDisposed(): void {
65 | if (this.disposed) {
66 | throw new Error(
67 | `[wedi] Dependency collection is not accessible after it disposes!`
68 | )
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/decorators.ts:
--------------------------------------------------------------------------------
1 | import { Ctor, DependencyKey, Identifier, IdentifierSymbol } from './typings'
2 | import { dependencyIds, setDependencies } from './utils'
3 |
4 | export function createIdentifier(name: string): Identifier {
5 | if (dependencyIds.has(name)) {
6 | console.warn(`[wedi] duplicated identifier name ${name}.`)
7 |
8 | return dependencyIds.get(name)!
9 | }
10 |
11 | const id = function(target: Ctor, _key: string, index: number): void {
12 | setDependencies(target, id, index, false)
13 | } as Identifier
14 |
15 | id.toString = () => name
16 | id[IdentifierSymbol] = true
17 |
18 | dependencyIds.set(name, id)
19 |
20 | return id
21 | }
22 |
23 | /**
24 | * wrap a Identifier with this function to make it optional
25 | */
26 | export function Optional(key: DependencyKey) {
27 | return function(target: Ctor, _key: string, index: number) {
28 | setDependencies(target, key, index, true)
29 | }
30 | }
31 |
32 | /**
33 | * used inside constructor for services to claim dependencies
34 | */
35 | export function Need(key: DependencyKey) {
36 | return function(target: Ctor, _key: string, index: number) {
37 | setDependencies(target, key, index, false)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/idle.ts:
--------------------------------------------------------------------------------
1 | export interface IdleDeadline {
2 | readonly didTimeout: boolean
3 | timeRemaining(): DOMHighResTimeStamp
4 | }
5 |
6 | export type DisposableCallback = () => void
7 |
8 | /**
9 | * this run the callback when CPU is idle. Will fallback to setTimeout if
10 | * the browser doesn't support requestIdleCallback
11 | */
12 | export let runWhenIdle: (
13 | callback: (idle?: IdleDeadline) => void,
14 | timeout?: number
15 | ) => DisposableCallback
16 |
17 | // declare global variables because apparently the type file doesn't have it, for now
18 | declare function requestIdleCallback(
19 | callback: (args: IdleDeadline) => void,
20 | options?: { timeout: number }
21 | ): number
22 | declare function cancelIdleCallback(
23 | handle: number
24 | ): void
25 |
26 | // use an IIFE to set up runWhenIdle
27 | ;(function() {
28 | if (
29 | typeof requestIdleCallback !== 'undefined' &&
30 | typeof cancelIdleCallback !== 'undefined'
31 | ) {
32 | // use native requestIdleCallback
33 | runWhenIdle = (runner, timeout?) => {
34 | const handle: number = requestIdleCallback(
35 | runner,
36 | typeof timeout === 'number' ? { timeout } : undefined
37 | )
38 | let disposed = false
39 | return () => {
40 | if (disposed) {
41 | return
42 | }
43 | disposed = true
44 | clearTimeout(handle)
45 | }
46 | }
47 | } else {
48 | // use setTimeout as hack
49 | const dummyIdle: IdleDeadline = Object.freeze({
50 | didTimeout: true,
51 | timeRemaining() {
52 | return 15
53 | }
54 | })
55 | runWhenIdle = (runner) => {
56 | const handle = setTimeout(() => runner(dummyIdle))
57 | let disposed = false
58 | return () => {
59 | if (disposed) {
60 | return
61 | }
62 | disposed = true
63 | clearTimeout(handle)
64 | }
65 | }
66 | }
67 | })()
68 |
69 | /**
70 | * a wrapper of a executor so it can be evaluated when it's necessary or the CPU is idle
71 | *
72 | * the type of the returned value of the executor would be T
73 | */
74 | export class IdleValue {
75 | private readonly executor: () => void
76 | private readonly disposeCallback: () => void
77 |
78 | private didRun: boolean = false
79 | private value?: T
80 | private error?: Error
81 |
82 | constructor(executor: () => T) {
83 | this.executor = () => {
84 | try {
85 | this.value = executor()
86 | } catch (err) {
87 | this.error = err
88 | } finally {
89 | this.didRun = true
90 | }
91 | }
92 | this.disposeCallback = runWhenIdle(() => this.executor())
93 | }
94 |
95 | dispose(): void {
96 | this.disposeCallback()
97 | }
98 |
99 | getValue(): T {
100 | if (!this.didRun) {
101 | this.dispose()
102 | this.executor()
103 | }
104 | if (this.error) {
105 | throw this.error
106 | }
107 | return this.value!
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // core
2 | export { DependencyCollection } from './collection'
3 | export { createIdentifier, Need, Optional } from './decorators'
4 | export { Injector } from './injector'
5 | export { registerSingleton } from './singleton'
6 | export {
7 | ClassItem,
8 | isClassItem,
9 | ValueItem,
10 | isValueItem,
11 | FactoryItem,
12 | isFactoryItem,
13 | DependencyValue,
14 | DependencyItem,
15 | Disposable,
16 | isDisposable
17 | } from './typings'
18 |
19 | // react bindings
20 | export { Provide, Inject } from './react/decorators'
21 | export {
22 | InjectionContext,
23 | Provider,
24 | InjectionProviderProps,
25 | connectProvider
26 | } from './react/context'
27 | export {
28 | useCollection,
29 | useDependency,
30 | useMultiDependencies
31 | } from './react/hooks'
32 | export {
33 | useDependencyValue,
34 | useUpdateBinder,
35 | useDependencyContext,
36 | useDependencyContextValue
37 | } from './react/rx'
38 |
--------------------------------------------------------------------------------
/src/injector.ts:
--------------------------------------------------------------------------------
1 | import { DependencyCollection } from './collection'
2 | import { IdleValue } from './idle'
3 | import { getSingletonDependencies } from './singleton'
4 | import {
5 | Ctor,
6 | DependencyKey,
7 | DependencyValue,
8 | FactoryItem,
9 | Disposable,
10 | InitPromise,
11 | isClassItem,
12 | isFactoryItem,
13 | isValueItem,
14 | Identifier,
15 | isInitPromise
16 | } from './typings'
17 | import {
18 | assertRecursionNotTrappedInACircle,
19 | completeInitialization,
20 | getDependencies,
21 | getDependencyKeyName,
22 | requireInitialization
23 | } from './utils'
24 |
25 | export class Injector implements Disposable {
26 | private readonly parent?: Injector
27 | private readonly collection: DependencyCollection
28 |
29 | constructor(collection?: DependencyCollection, parent?: Injector) {
30 | const _collection = collection || new DependencyCollection()
31 |
32 | // if there's no parent injector, should get singleton dependencies
33 | if (!parent) {
34 | const newDependencies = getSingletonDependencies()
35 | // root injector would not re-add dependencies when the component
36 | // it embeds re-render
37 | newDependencies.forEach((d) => {
38 | if (!_collection.has(d[0])) {
39 | _collection.add(d[0], d[1])
40 | }
41 | })
42 | }
43 |
44 | this.collection = _collection
45 | this.parent = parent
46 | }
47 |
48 | dispose(): void {
49 | this.collection.dispose()
50 | }
51 |
52 | add(ctor: Ctor): void
53 | add(key: Identifier, item: DependencyValue): void
54 | add(ctorOrKey: Ctor | Identifier, item?: DependencyValue): void {
55 | this.collection.add(ctorOrKey as any, item as any)
56 | }
57 |
58 | /**
59 | * create a child Initializer to build layered injection system
60 | */
61 | createChild(
62 | dependencies: DependencyCollection = new DependencyCollection()
63 | ): Injector {
64 | return new Injector(dependencies, this)
65 | }
66 |
67 | /**
68 | * get a dependency or create one in the current injector
69 | */
70 | getOrInit(key: DependencyKey): T
71 | getOrInit(key: DependencyKey, optional: true): T | null
72 | getOrInit(key: DependencyKey, optional?: true): T | null {
73 | const thing = this.getDependencyOrIdentifierPair(key)
74 |
75 | if (typeof thing === 'undefined') {
76 | if (!optional) {
77 | throw new Error(
78 | `[wedi] "${getDependencyKeyName(
79 | key
80 | )}" is not provided by any injector.`
81 | )
82 | }
83 | return null
84 | } else if (isInitPromise(thing)) {
85 | return this.createAndCacheInstance(key, thing)
86 | } else if (isValueItem(thing)) {
87 | return thing.useValue
88 | } else if (isFactoryItem(thing)) {
89 | return this.invokeDependencyFactory(key as Identifier, thing)
90 | } else if (isClassItem(thing)) {
91 | return this.createAndCacheInstance(
92 | key,
93 | new InitPromise(thing.useClass, !!thing.lazyInstantiation)
94 | )
95 | } else {
96 | return thing as T
97 | }
98 | }
99 |
100 | /**
101 | * initialize a class in the scope of the injector
102 | * @param ctor The class to be initialized
103 | */
104 | createInstance(ctor: Ctor | InitPromise, ...extraParams: any[]): T {
105 | const theCtor = ctor instanceof InitPromise ? ctor.ctor : ctor
106 | const dependencies = getDependencies(theCtor).sort(
107 | (a, b) => a.index - b.index
108 | )
109 | const resolvedArgs: any[] = []
110 |
111 | let args = [...extraParams]
112 |
113 | for (const dependency of dependencies) {
114 | const thing = this.getOrInit(dependency.id, true)
115 |
116 | if (thing === null && !dependency.optional) {
117 | throw new Error(
118 | `[wedi] "${
119 | theCtor.name
120 | }" relies on a not provided dependency "${getDependencyKeyName(
121 | dependency.id
122 | )}".`
123 | )
124 | }
125 |
126 | resolvedArgs.push(thing)
127 | }
128 |
129 | const firstDependencyArgIndex =
130 | dependencies.length > 0 ? dependencies[0].index : args.length
131 |
132 | if (args.length !== firstDependencyArgIndex) {
133 | console.warn(
134 | `[wedi] expected ${firstDependencyArgIndex} non-injected parameters ` +
135 | `but ${args.length} parameters are provided.`
136 | )
137 |
138 | const delta = firstDependencyArgIndex - args.length
139 | if (delta > 0) {
140 | args = [...args, ...new Array(delta).fill(undefined)]
141 | } else {
142 | args = args.slice(0, firstDependencyArgIndex)
143 | }
144 | }
145 |
146 | return new theCtor(...args, ...resolvedArgs)
147 | }
148 |
149 | private getDependencyOrIdentifierPair(
150 | id: DependencyKey
151 | ): T | DependencyValue | undefined {
152 | return (
153 | this.collection.get(id) ||
154 | (this.parent ? this.parent.getDependencyOrIdentifierPair(id) : undefined)
155 | )
156 | }
157 |
158 | private putDependencyBack(key: DependencyKey, value: T): void {
159 | if (this.collection.get(key)) {
160 | this.collection.add(key, value)
161 | } else {
162 | this.parent!.putDependencyBack(key, value)
163 | }
164 | }
165 |
166 | private createAndCacheInstance(
167 | dKey: DependencyKey,
168 | initPromise: InitPromise
169 | ) {
170 | requireInitialization()
171 | assertRecursionNotTrappedInACircle(dKey)
172 |
173 | const ctor = initPromise.ctor
174 | let thing: T
175 |
176 | if (initPromise.lazyInstantiation) {
177 | const idle = new IdleValue(() => this.doCreateInstance(dKey, ctor))
178 | thing = new Proxy(Object.create(null), {
179 | get(target: any, key: string | number | symbol): any {
180 | if (key in target) {
181 | return target[key]
182 | }
183 | const obj = idle.getValue()
184 | let prop = (obj as any)[key]
185 | if (typeof prop !== 'function') {
186 | return prop
187 | }
188 | prop = prop.bind(obj)
189 | target[key] = prop
190 | return prop
191 | },
192 | set(_target: any, key: string | number | symbol, value: any): boolean {
193 | ;(idle.getValue() as any)[key] = value
194 | return true
195 | }
196 | }) as T
197 | } else {
198 | thing = this.doCreateInstance(dKey, ctor)
199 | }
200 |
201 | completeInitialization()
202 |
203 | return thing
204 | }
205 |
206 | private doCreateInstance(id: DependencyKey, ctor: Ctor): T {
207 | const thing = this.createInstance(ctor)
208 | this.putDependencyBack(id, thing)
209 | return thing
210 | }
211 |
212 | private invokeDependencyFactory(
213 | id: Identifier,
214 | factory: FactoryItem
215 | ): T {
216 | // TODO: should report missing dependency for factories?
217 | const dependencies =
218 | factory.deps?.map((dp) => this.getOrInit(dp, true)) || []
219 | const thing = factory.useFactory.call(null, dependencies)
220 |
221 | this.collection.add(id, {
222 | useValue: thing
223 | })
224 |
225 | return thing
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/react/context.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, ComponentType, PropsWithChildren } from 'react'
2 |
3 | import { DependencyCollection } from '../collection'
4 | import { Injector } from '../injector'
5 |
6 | interface InjectionContextValue {
7 | injector: Injector | null
8 | }
9 |
10 | export const InjectionContext = createContext({
11 | injector: null
12 | })
13 | InjectionContext.displayName = 'InjectionContext'
14 |
15 | const InjectionConsumer = InjectionContext.Consumer
16 | const InjectionProvider = InjectionContext.Provider
17 |
18 | export interface InjectionProviderProps {
19 | collection?: DependencyCollection
20 | // support providing an injector directly, so React binding
21 | // can use parent injector outside of React
22 | injector?: Injector
23 | }
24 |
25 | /**
26 | * the React binding of wedi
27 | *
28 | * it uses the React context API to specify injection positions and
29 | * layered injector tree
30 | *
31 | * ```tsx
32 | *
33 | * { children }
34 | *
35 | * ```
36 | */
37 | export function Provider(props: PropsWithChildren) {
38 | const { collection, children, injector } = props
39 |
40 | return (
41 |
42 | {(context: InjectionContextValue) => {
43 | const parentInjector = context.injector
44 |
45 | if (!!collection === !!injector) {
46 | throw new Error(
47 | '[wedi] should provide a collection or an injector to "Provider".'
48 | )
49 | }
50 |
51 | const finalInjector =
52 | injector ||
53 | parentInjector?.createChild(collection) ||
54 | new Injector(collection!)
55 |
56 | return (
57 |
58 | {children}
59 |
60 | )
61 | }}
62 |
63 | )
64 | }
65 |
66 | /**
67 | * return a HOC that enable functional component to add injector
68 | * in a convenient way
69 | */
70 | export function connectProvider(
71 | Comp: ComponentType,
72 | options: InjectionProviderProps
73 | ): ComponentType {
74 | const { injector, collection } = options
75 |
76 | return function ComponentWithInjector(props: T) {
77 | return (
78 |
79 |
80 |
81 | )
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/react/decorators.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ComponentClass, createElement } from 'react'
2 |
3 | import { DependencyCollection } from '../collection'
4 | import { Injector } from '../injector'
5 | import { Ctor, DependencyItem, Identifier } from '../typings'
6 | import { getDependencyKeyName } from '../utils'
7 | import { Provider } from './context'
8 |
9 | /**
10 | * an decorator that could be used on a React class component
11 | * to provide a injection context on that component
12 | */
13 | export function Provide(items: DependencyItem[]) {
14 | return function(target: any): any {
15 | function getChild(this: Component) {
16 | return createElement(target, this.props)
17 | }
18 |
19 | class ProviderWrapper extends Component {
20 | $$collection: DependencyCollection
21 |
22 | constructor(props: any, context: any) {
23 | super(props, context)
24 | this.$$collection = new DependencyCollection(items)
25 | }
26 |
27 | componentWillUnmount(): void {
28 | this.$$collection.dispose()
29 | }
30 |
31 | render() {
32 | return (
33 |
34 | {getChild.call(this)}
35 |
36 | )
37 | }
38 | }
39 |
40 | ;(ProviderWrapper as ComponentClass).displayName = `ProviderWrapper.${target.name}`
41 |
42 | return ProviderWrapper
43 | }
44 | }
45 |
46 | /**
47 | * returns decorator that could be used on a property of
48 | * a React class component to inject a dependency
49 | */
50 | export function Inject(
51 | id: Identifier | Ctor,
52 | optional: boolean = false
53 | ) {
54 | return function(target: any, propName: string, _originDescriptor?: any): any {
55 | return {
56 | // when user is trying to get the service, get it from the injector in
57 | // the current context
58 | get(): T | null {
59 | // tslint:disable-next-line:no-invalid-this
60 | const thisAsComponent: Component = this as any
61 |
62 | ensureInjectionContextExists(thisAsComponent)
63 |
64 | const injector: Injector = thisAsComponent.context.injector
65 | const thing = injector?.getOrInit(id, true)
66 |
67 | if (!optional && !thing) {
68 | throw Error(
69 | `[wedi] Cannot get an instance of "${getDependencyKeyName(id)}".`
70 | )
71 | }
72 |
73 | return thing || null
74 | },
75 | set(_value: never) {
76 | throw Error(
77 | `[wedi] You can never set value to a dependency. Check "${propName}" of "${getDependencyKeyName(
78 | id
79 | )}".`
80 | )
81 | }
82 | }
83 | }
84 | }
85 |
86 | function ensureInjectionContextExists(component: Component): void {
87 | if (!component.context || !component.context.injector) {
88 | throw Error(
89 | `[wedi] You should make "InjectorContext" as ${component.constructor.name}'s default context type. ` +
90 | 'If you want to use multiple context, please check this page on React documentation. ' +
91 | 'https://reactjs.org/docs/context.html#classcontexttype'
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/react/hooks.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useRef } from 'react'
2 |
3 | import { DependencyCollection } from '../collection'
4 | import { DependencyItem, DependencyKey } from '../typings'
5 | import { getDependencyKeyName } from '../utils'
6 | import { InjectionContext } from './context'
7 |
8 | /**
9 | * when providing dependencies in a functional component, it would be expensive
10 | * (not to mention logic incorrectness)
11 | */
12 | export function useCollection(
13 | entries?: DependencyItem[]
14 | ): DependencyCollection {
15 | const collectionRef = useRef(new DependencyCollection(entries))
16 | useEffect(() => () => collectionRef.current.dispose(), [])
17 | return collectionRef.current
18 | }
19 |
20 | /**
21 | * this function support using dependency injection in a function component
22 | * with the help of React Hooks
23 | */
24 | export function useDependency(key: DependencyKey): T
25 | export function useDependency(
26 | key: DependencyKey,
27 | optional: true
28 | ): T | null
29 | export function useDependency(
30 | key: DependencyKey,
31 | optional?: boolean
32 | ): T | null {
33 | const { injector } = useContext(InjectionContext)
34 | const thing = injector?.getOrInit(key, true)
35 |
36 | if (!optional && !thing) {
37 | throw Error(
38 | `[wedi] Cannot get an instance of "${getDependencyKeyName(key)}".`
39 | )
40 | }
41 |
42 | return thing || null
43 | }
44 |
45 | type Nullable = T | null
46 |
47 | export function useMultiDependencies(
48 | keys: [DependencyKey, DependencyKey]
49 | ): [Nullable, Nullable]
50 | export function useMultiDependencies(
51 | keys: [DependencyKey, DependencyKey, DependencyKey]
52 | ): [Nullable, Nullable, Nullable]
53 | export function useMultiDependencies(
54 | keys: [
55 | DependencyKey,
56 | DependencyKey,
57 | DependencyKey,
58 | DependencyKey
59 | ]
60 | ): [Nullable, Nullable, Nullable, Nullable]
61 | export function useMultiDependencies(
62 | keys: [
63 | DependencyKey,
64 | DependencyKey,
65 | DependencyKey,
66 | DependencyKey,
67 | DependencyKey
68 | ]
69 | ): [Nullable, Nullable, Nullable, Nullable, Nullable]
70 | export function useMultiDependencies(
71 | keys: [
72 | DependencyKey,
73 | DependencyKey,
74 | DependencyKey,
75 | DependencyKey,
76 | DependencyKey,
77 | DependencyKey
78 | ]
79 | ): [
80 | Nullable,
81 | Nullable,
82 | Nullable,
83 | Nullable,
84 | Nullable,
85 | Nullable
86 | ]
87 | export function useMultiDependencies(keys: any[]): any[] {
88 | const ret = new Array(keys.length).fill(null)
89 | const { injector } = useContext(InjectionContext)
90 |
91 | keys.forEach((key, index) => {
92 | ret[index] = injector?.getOrInit(key) ?? null
93 | })
94 |
95 | return ret
96 | }
97 |
--------------------------------------------------------------------------------
/src/react/rx.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useEffect,
3 | useState,
4 | createContext,
5 | useMemo,
6 | useContext,
7 | useCallback,
8 | ReactNode,
9 | Context,
10 | useRef
11 | } from 'react'
12 | import { BehaviorSubject, Observable } from 'rxjs'
13 |
14 | /**
15 | * unwrap an observable value, return it to the component for rendering, and
16 | * trigger re-render when value changes
17 | *
18 | * **IMPORTANT**. Parent and child components should not subscribe to the same
19 | * observable, otherwise unnecessary re-render would be triggered. Instead, the
20 | * top-most component should subscribe and pass value of the observable to
21 | * its offspring, by props or context.
22 | *
23 | * If you have to do that, consider using `useDependencyContext` and
24 | * `useDependencyContextValue` instead.
25 | */
26 | export function useDependencyValue(
27 | depValue$: Observable,
28 | defaultValue?: T
29 | ): T | undefined {
30 | const _defaultValue: T | undefined =
31 | depValue$ instanceof BehaviorSubject && typeof defaultValue === 'undefined'
32 | ? depValue$.getValue()
33 | : defaultValue
34 | const [value, setValue] = useState(_defaultValue)
35 |
36 | useEffect(() => {
37 | const subscription = depValue$.subscribe((val: T) => setValue(val))
38 | return () => subscription.unsubscribe()
39 | }, [depValue$])
40 |
41 | return value
42 | }
43 |
44 | /**
45 | * subscribe to a signal that emits whenever data updates and re-render
46 | *
47 | * @param update$ a signal that the data the functional component depends has updated
48 | */
49 | export function useUpdateBinder(update$: Observable): void {
50 | const [, dumpSet] = useState(0)
51 |
52 | useEffect(() => {
53 | const subscription = update$.subscribe(() => dumpSet((prev) => prev + 1))
54 | return () => subscription.unsubscribe()
55 | }, [])
56 | }
57 |
58 | const DepValueMapProvider = new WeakMap, Context>()
59 |
60 | /**
61 | * subscribe to an observable value from a service, creating a context for it so
62 | * it child component won't have to subscribe again and cause unnecessary
63 | */
64 | export function useDependencyContext(
65 | depValue$: Observable,
66 | defaultValue?: T
67 | ) {
68 | const depRef = useRef | undefined>(undefined)
69 | const value = useDependencyValue(depValue$, defaultValue)
70 | const Context = useMemo(() => {
71 | return createContext(value)
72 | }, [depValue$])
73 | const Provider = useCallback(
74 | (props: { initialState?: T; children: ReactNode }) => {
75 | return {props.children}
76 | },
77 | [depValue$, value]
78 | )
79 |
80 | if (depRef.current !== depValue$) {
81 | if (depRef.current) {
82 | DepValueMapProvider.delete(depRef.current)
83 | }
84 |
85 | depRef.current = depValue$
86 | DepValueMapProvider.set(depValue$, Context)
87 | }
88 |
89 | return {
90 | Provider,
91 | value
92 | }
93 | }
94 |
95 | export function useDependencyContextValue(
96 | depValue$: Observable
97 | ): T | undefined {
98 | const context = DepValueMapProvider.get(depValue$)
99 |
100 | if (!context) {
101 | throw new Error(
102 | `[wedi] try to read context value but no ancestor component subscribed it.`
103 | )
104 | }
105 |
106 | return useContext(context)
107 | }
108 |
--------------------------------------------------------------------------------
/src/singleton.ts:
--------------------------------------------------------------------------------
1 | import { ClassItem, Ctor, Identifier } from './typings'
2 |
3 | let singletonDependenciesHaveBeenFetched = false
4 | let haveWarned = false
5 |
6 | const singletonDependencies: [Identifier, ClassItem][] = []
7 |
8 | export function registerSingleton(
9 | id: Identifier,
10 | ctor: Ctor,
11 | lazyInstantiation = false
12 | ): void {
13 | const index = singletonDependencies.findIndex(
14 | (d) => d[0].toString() === id.toString() || d[0] === id
15 | )
16 |
17 | if (index !== -1) {
18 | singletonDependencies[index] = [id, { useClass: ctor, lazyInstantiation }]
19 | console.warn(`[wedi] Duplicated registration of ${id.toString()}.`)
20 | } else {
21 | singletonDependencies.push([id, { useClass: ctor, lazyInstantiation }])
22 | }
23 | }
24 |
25 | /**
26 | * for top-layer injectors to fetch all singleton dependencies
27 | */
28 | export function getSingletonDependencies(): [
29 | Identifier,
30 | ClassItem
31 | ][] {
32 | if (singletonDependenciesHaveBeenFetched && !haveWarned) {
33 | console.warn(
34 | '[wedi] More than one root injectors tried to fetch singleton dependencies. ' +
35 | 'This may cause undesired behavior in your application.'
36 | )
37 |
38 | haveWarned = true
39 | }
40 |
41 | singletonDependenciesHaveBeenFetched = true
42 |
43 | return singletonDependencies
44 | }
45 |
--------------------------------------------------------------------------------
/src/typings.ts:
--------------------------------------------------------------------------------
1 | export type Ctor = new (...args: any[]) => T
2 |
3 | export const IdentifierSymbol = Symbol('$$WEDI_IDENTIFIER')
4 |
5 | export interface Identifier {
6 | type?: T
7 | toString(): string
8 | [IdentifierSymbol]: boolean
9 | (target: Ctor, key: string, index: number): void
10 | }
11 |
12 | export function isIdentifier(thing: any): thing is Identifier {
13 | return thing[IdentifierSymbol]
14 | }
15 |
16 | export interface DependencyMeta {
17 | id: DependencyKey
18 | index: number
19 | optional: boolean
20 | }
21 |
22 | export class InitPromise {
23 | readonly ctor: any
24 | readonly lazyInstantiation: boolean
25 |
26 | constructor(ctor: Ctor, lazyInstantiation: boolean = false) {
27 | this.ctor = ctor
28 | this.lazyInstantiation = lazyInstantiation
29 | }
30 | }
31 |
32 | export function isInitPromise(thing: any): thing is InitPromise {
33 | return thing instanceof InitPromise
34 | }
35 |
36 | export interface ClassItem {
37 | useClass: Ctor
38 | lazyInstantiation?: boolean
39 | }
40 |
41 | export function isClassItem(thing: any): thing is ClassItem {
42 | return !!(thing as any).useClass
43 | }
44 |
45 | export interface ValueItem {
46 | useValue: T
47 | }
48 |
49 | export function isValueItem(thing: any): thing is ValueItem {
50 | return !!(thing as any).useValue
51 | }
52 |
53 | export interface FactoryItem {
54 | useFactory(...args: any[]): T
55 | deps?: DependencyKey[]
56 | }
57 |
58 | export function isFactoryItem(thing: any): thing is FactoryItem {
59 | return !!(thing as any).useFactory
60 | }
61 |
62 | export type DependencyValue =
63 | | Ctor
64 | | InitPromise
65 | | ValueItem
66 | | ClassItem
67 | | FactoryItem
68 |
69 | export type DependencyKey = Identifier