├── .prettierignore ├── .yarnrc ├── .npmrc ├── examples ├── counter │ ├── src │ │ ├── polyfills.ts │ │ ├── styles.css │ │ ├── app │ │ │ ├── counter.css │ │ │ ├── app.tsx │ │ │ ├── counter.service.ts │ │ │ └── counter.tsx │ │ ├── main.ts │ │ └── index.html │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── github-user │ ├── src │ │ ├── polyfills.ts │ │ ├── styles.css │ │ ├── app │ │ │ ├── repo.model.ts │ │ │ ├── user.model.ts │ │ │ ├── app.tsx │ │ │ ├── components │ │ │ │ ├── repos.tsx │ │ │ │ ├── profile.tsx │ │ │ │ ├── user-profile.tsx │ │ │ │ └── search-user.tsx │ │ │ └── user.service.ts │ │ ├── main.ts │ │ └── index.html │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── tour-of-heroes │ ├── src │ │ ├── polyfills.ts │ │ ├── app │ │ │ ├── shared │ │ │ │ ├── index.ts │ │ │ │ └── pipes.ts │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── hero-detail.css │ │ │ │ ├── hero-search.css │ │ │ │ ├── dashboard.css │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── heroes.css │ │ │ │ ├── hero-search.tsx │ │ │ │ ├── hero-detail.tsx │ │ │ │ └── heroes.tsx │ │ │ ├── messages │ │ │ │ ├── index.ts │ │ │ │ ├── messages.service.ts │ │ │ │ ├── messages.css │ │ │ │ └── messages.tsx │ │ │ ├── hero.ts │ │ │ ├── app.css │ │ │ ├── http-client.service.ts │ │ │ ├── app.tsx │ │ │ └── hero.service.ts │ │ ├── main.ts │ │ ├── index.html │ │ ├── db.json │ │ └── styles.css │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── counter-with-logger │ ├── src │ │ ├── polyfills.ts │ │ ├── styles.css │ │ ├── app │ │ │ ├── logger.service.ts │ │ │ ├── app.tsx │ │ │ ├── counter.tsx │ │ │ └── counter.service.ts │ │ ├── main.ts │ │ └── index.html │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── counter-with-multiple-injectors │ ├── src │ │ ├── polyfills.ts │ │ ├── styles.css │ │ ├── app │ │ │ ├── helpers.ts │ │ │ ├── logger.service.ts │ │ │ ├── enhanced-logger.service.ts │ │ │ ├── counter.tsx │ │ │ ├── multiply-counter.service.ts │ │ │ ├── counter.service.ts │ │ │ └── app.tsx │ │ ├── main.ts │ │ └── index.html │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── img │ ├── counter-di.png │ ├── github-search-di.png │ ├── github-user-search.gif │ ├── counter-with-logger-di.png │ └── counter-with-multiple-di.png └── README.md ├── src ├── __tests__ │ ├── setup │ │ ├── utils.ts │ │ ├── services.ts │ │ └── components.tsx │ ├── types.spec.ts │ ├── components │ │ ├── __snapshots__ │ │ │ ├── provide-inject.spec.tsx.snap │ │ │ └── provide-inject.hoc.spec.tsx.snap │ │ ├── provide-inject.hoc.spec.tsx │ │ └── provide-inject.spec.tsx │ └── utils │ │ └── guards.spec.ts ├── environment.ts ├── facade │ └── lang.ts ├── index.ts ├── services │ ├── injector-context.ts │ └── stateful.ts ├── utils │ ├── helpers.ts │ └── guards.ts ├── types.ts └── components │ ├── provider.hoc.tsx │ ├── inject.tsx │ ├── async-pipe.tsx │ ├── debug.tsx │ ├── inject.hoc.tsx │ └── provider.tsx ├── .travis.yml ├── scripts ├── tsconfig.json └── copy.js ├── config ├── commitlint.config.js ├── setup-enzyme.js ├── setup-tests.js ├── prettier.config.js ├── tsconfig.json ├── types.js ├── jest.config.js ├── helpers.js ├── global.d.ts └── rollup.config.js ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── .editorconfig ├── .npmignore ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── tsconfig.json ├── LICENSE.md ├── CHANGELOG.md ├── tslint.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix false 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | access=public 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /examples/counter/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | -------------------------------------------------------------------------------- /examples/github-user/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pipes' 2 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | -------------------------------------------------------------------------------- /src/__tests__/setup/utils.ts: -------------------------------------------------------------------------------- 1 | export const select = (id: string) => `[data-test="${id}"]` 2 | -------------------------------------------------------------------------------- /examples/counter/src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'papercss/dist/paper.min.css'; 2 | 3 | /* Master Styles */ 4 | -------------------------------------------------------------------------------- /examples/github-user/src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'papercss/dist/paper.css'; 2 | 3 | /* Master Styles */ 4 | -------------------------------------------------------------------------------- /examples/img/counter-di.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hotell/rea-di/HEAD/examples/img/counter-di.png -------------------------------------------------------------------------------- /examples/counter-with-logger/src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'papercss/dist/paper.css'; 2 | 3 | /* Master Styles */ 4 | -------------------------------------------------------------------------------- /examples/img/github-search-di.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hotell/rea-di/HEAD/examples/img/github-search-di.png -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/shared/pipes.ts: -------------------------------------------------------------------------------- 1 | export const uppercase = (value: string) => value.toUpperCase() 2 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'papercss/dist/paper.css'; 2 | 3 | /* Master Styles */ 4 | -------------------------------------------------------------------------------- /examples/img/github-user-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hotell/rea-di/HEAD/examples/img/github-user-search.gif -------------------------------------------------------------------------------- /examples/img/counter-with-logger-di.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hotell/rea-di/HEAD/examples/img/counter-with-logger-di.png -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/helpers.ts: -------------------------------------------------------------------------------- 1 | export const getClassName = (cls: object) => cls.constructor.name 2 | -------------------------------------------------------------------------------- /examples/img/counter-with-multiple-di.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hotell/rea-di/HEAD/examples/img/counter-with-multiple-di.png -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dashboard' 2 | export * from './heroes' 3 | export * from './hero-detail' 4 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/messages/index.ts: -------------------------------------------------------------------------------- 1 | export { Messages } from './messages' 2 | export { MessageService } from './messages.service' 3 | -------------------------------------------------------------------------------- /examples/github-user/src/app/repo.model.ts: -------------------------------------------------------------------------------- 1 | export interface GithubUserRepo { 2 | name: string 3 | html_url: string 4 | description: string 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: '8' 3 | cache: yarn 4 | notifications: 5 | email: false 6 | install: 7 | - yarn 8 | script: 9 | - yarn build 10 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": [ 5 | ".", 6 | "../config/global.d.ts" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /config/commitlint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@commitlint/core').Config} 3 | */ 4 | const config = { extends: ['@commitlint/config-conventional'] } 5 | 6 | module.exports = config 7 | -------------------------------------------------------------------------------- /config/setup-enzyme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { configure } = require('enzyme') 3 | const EnzymeAdapter = require('enzyme-adapter-react-16') 4 | 5 | configure({ adapter: new EnzymeAdapter() }) 6 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | /** * @internal */ 2 | export const IS_DEV = process.env.NODE_ENV === 'development' 3 | 4 | /** * @internal */ 5 | export const IS_PROD = process.env.NODE_ENV === 'production' 6 | -------------------------------------------------------------------------------- /examples/counter/src/app/counter.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | } 3 | .counter-actions { 4 | display: flex; 5 | } 6 | .counter-actions > button { 7 | flex: 1; 8 | padding: 0.5rem; 9 | margin: 0.25rem; 10 | } 11 | -------------------------------------------------------------------------------- /src/facade/lang.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export const reflection = { 4 | defineMetadata: Reflect.defineMetadata, 5 | getMetadata: Reflect.getMetadata, 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .idea 3 | .cache 4 | .DS_Store 5 | node_modules 6 | 7 | coverage 8 | lib 9 | esm5 10 | lib-esm 11 | esm2015 12 | lib-fesm 13 | fesm 14 | umd 15 | bundles 16 | typings 17 | types 18 | docs 19 | dist 20 | 21 | ## this is generated by `npm pack` 22 | *.tgz 23 | package 24 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/app/logger.service.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | log(...args: any[]) { 3 | console.log(...args) 4 | } 5 | warn(...args: any[]) { 6 | console.warn(...args) 7 | } 8 | error(...args: any[]) { 9 | console.error(...args) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/logger.service.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | log(...args: any[]) { 3 | console.log(...args) 4 | } 5 | warn(...args: any[]) { 6 | console.warn(...args) 7 | } 8 | error(...args: any[]) { 9 | console.error(...args) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/setup-tests.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // add here any code that you wanna execute before tests like 4 | // - polyfills 5 | // - some custom code 6 | // for more docs check see https://jestjs.io/docs/en/configuration.html#setupfiles-array 7 | 8 | // @ts-ignore 9 | require('core-js/es7/reflect') 10 | -------------------------------------------------------------------------------- /config/prettier.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {import('./types').PrettierConfig} 5 | */ 6 | const config = { 7 | singleQuote: true, 8 | arrowParens: 'always', 9 | semi: false, 10 | bracketSpacing: true, 11 | trailingComma: 'es5', 12 | printWidth: 80, 13 | } 14 | 15 | module.exports = config 16 | -------------------------------------------------------------------------------- /examples/counter/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import './styles.css' 5 | 6 | import { App } from './app/app' 7 | 8 | bootstrap() 9 | 10 | function bootstrap() { 11 | const mountTo = document.getElementById('app') 12 | render(createElement(App), mountTo) 13 | } 14 | -------------------------------------------------------------------------------- /examples/github-user/src/main.ts: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | 3 | import { createElement } from 'react' 4 | import { render } from 'react-dom' 5 | 6 | import { App } from './app/app' 7 | 8 | bootstrap() 9 | 10 | function bootstrap() { 11 | const mountTo = document.getElementById('app') 12 | render(createElement(App), mountTo) 13 | } 14 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/hero.ts: -------------------------------------------------------------------------------- 1 | export class Hero { 2 | constructor(public id: number, public name: string) {} 3 | } 4 | 5 | /* 6 | Copyright 2017-2018 Google Inc. All Rights Reserved. 7 | Use of this source code is governed by an MIT-style license that 8 | can be found in the LICENSE file at http://angular.io/license 9 | */ 10 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import './styles.css' 5 | 6 | import { App } from './app/app' 7 | 8 | bootstrap() 9 | 10 | function bootstrap() { 11 | const mountTo = document.getElementById('app') 12 | render(createElement(App), mountTo) 13 | } 14 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import './styles.css' 5 | 6 | import { App } from './app/app' 7 | 8 | bootstrap() 9 | 10 | function bootstrap() { 11 | const mountTo = document.getElementById('app') 12 | render(createElement(App), mountTo) 13 | } 14 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/main.ts: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | 3 | import { createElement } from 'react' 4 | import { render } from 'react-dom' 5 | 6 | import { App } from './app/app' 7 | 8 | bootstrap() 9 | 10 | function bootstrap() { 11 | const mountTo = document.getElementById('app') 12 | render(createElement(App), mountTo) 13 | } 14 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/README.md: -------------------------------------------------------------------------------- 1 | # Tour of heroes app 2 | 3 | > https://angular.io/tutorial rewritten to showcase the power of rea-di + injection-js within React 4 | 5 | Run the [Tour of Heroes](.) example: 6 | 7 | ``` 8 | git clone https://github.com/hotell/rea-di.git 9 | 10 | cd rea-di/examples/tour-of-heroes 11 | yarn install 12 | yarn start 13 | ``` 14 | -------------------------------------------------------------------------------- /config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "allowJs": true, 7 | "checkJs": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "noEmit": true, 11 | "importHelpers": false 12 | }, 13 | "include": [ 14 | "." 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "eg2.tslint", 7 | "esbenp.prettier-vscode", 8 | "codezombiech.gitignore", 9 | "EditorConfig.EditorConfig" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Inject } from './components/inject' 2 | export { DependencyProvider } from './components/provider' 3 | export { Stateful } from './services/stateful' 4 | export { AsyncPipe } from './components/async-pipe' 5 | export { withInjectables } from './components/inject.hoc' 6 | export { withDependencyProvider } from './components/provider.hoc' 7 | export { tuple, optional } from './utils/helpers' 8 | -------------------------------------------------------------------------------- /src/services/injector-context.ts: -------------------------------------------------------------------------------- 1 | import { ReflectiveInjector } from 'injection-js' 2 | import { createContext } from 'react' 3 | 4 | export type ContextApi = { 5 | injector: ReflectiveInjector 6 | [providerName: string]: any 7 | } 8 | 9 | export const rootInjector = ReflectiveInjector.resolveAndCreate([]) 10 | 11 | export const Context = createContext({ injector: rootInjector }) 12 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter app 2 | 3 | Run the [Counter](.) example: 4 | 5 | ``` 6 | git clone https://github.com/hotell/rea-di.git 7 | 8 | cd rea-di/examples/counter 9 | yarn install 10 | yarn start 11 | ``` 12 | 13 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter). 14 | 15 | ## Injector tree 16 | 17 | ![Injector tree](../img/counter-di.png) 18 | -------------------------------------------------------------------------------- /examples/github-user/README.md: -------------------------------------------------------------------------------- 1 | # Github User Search app 2 | 3 | Run the [Github User Search](.) example: 4 | 5 | ``` 6 | git clone https://github.com/hotell/rea-di.git 7 | 8 | cd rea-di/examples/github-user 9 | yarn install 10 | yarn start 11 | ``` 12 | 13 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/github-user). 14 | 15 | ## Injector tree 16 | 17 | ![Injector tree](../img/github-search-di.png) 18 | -------------------------------------------------------------------------------- /examples/counter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Counter App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # all files 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | max_line_length = 80 13 | 14 | [*.{js,ts}] 15 | quote_type = single 16 | curly_bracket_next_line = false 17 | spaces_around_brackets = inside 18 | indent_brace_style = BSD KNF 19 | 20 | # HTML 21 | [*.html] 22 | quote_type = double 23 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Counter App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tour of heroes 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/github-user/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Github Users Search 👀 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/counter-with-logger/README.md: -------------------------------------------------------------------------------- 1 | # Counter with Logger app 2 | 3 | Run the [Counter with Logger app](.) example: 4 | 5 | ``` 6 | git clone https://github.com/hotell/rea-di.git 7 | 8 | cd rea-di/examples/counter-with-logger 9 | yarn install 10 | yarn start 11 | ``` 12 | 13 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter-with-logger). 14 | 15 | ## Injector tree 16 | 17 | ![Injector tree](../img/counter-with-logger-di.png) 18 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "lib": [ 6 | "dom","es2015" 7 | ], 8 | "module": "esnext", 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "outDir": "dist", 12 | "strict": true, 13 | "importHelpers": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true 16 | }, 17 | "include": [ 18 | "src" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/services/stateful.ts: -------------------------------------------------------------------------------- 1 | import { StateCallback } from '../types' 2 | 3 | export abstract class Stateful { 4 | protected abstract state: null | Readonly = null 5 | protected setState(stateFn: StateCallback) { 6 | const newState = { 7 | ...(this.state as object), 8 | ...(stateFn(this.state as T) as object), 9 | } as T 10 | 11 | // console.log({ newState }) 12 | this.state = newState 13 | 14 | return this.state 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Counter App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | 4 | # tools configs 5 | **/tsconfig.json 6 | tsconfig.*.json 7 | tslint.json 8 | **/webpack.config.js 9 | **/jest.config.js 10 | **/prettier.config.js 11 | 12 | # build scripts 13 | config/ 14 | scripts/ 15 | 16 | # Test files 17 | **/*.spec.js 18 | **/*.test.js 19 | **/*.test.d.ts 20 | **/*.spec.d.ts 21 | __tests__ 22 | coverage 23 | 24 | # Sources 25 | node_modules 26 | src 27 | docs 28 | examples 29 | 30 | ## this is generated by `npm pack` 31 | *.tgz 32 | package 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "tslint.autoFixOnSave": true, 4 | "tslint.enable": true, 5 | "editor.formatOnSave": true, 6 | "typescript.format.enable": false, 7 | "javascript.format.enable": false, 8 | "typescript.referencesCodeLens.enabled": true, 9 | "javascript.referencesCodeLens.enabled": true, 10 | "editor.rulers": [ 11 | 80,100 12 | ], 13 | "typescript.tsdk": "node_modules/typescript/lib", 14 | } 15 | -------------------------------------------------------------------------------- /examples/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "lib": [ 6 | "dom","es2015" 7 | ], 8 | "module": "esnext", 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "jsxFactory": "createElement", 12 | "outDir": "dist", 13 | "strict": true, 14 | "importHelpers": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/github-user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "lib": [ 6 | "dom","es2015" 7 | ], 8 | "module": "esnext", 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "jsxFactory": "createElement", 12 | "outDir": "dist", 13 | "strict": true, 14 | "importHelpers": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/counter-with-logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "lib": [ 6 | "dom","es2015" 7 | ], 8 | "module": "esnext", 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "jsxFactory": "createElement", 12 | "outDir": "dist", 13 | "strict": true, 14 | "importHelpers": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "lib": [ 6 | "dom","es2015" 7 | ], 8 | "module": "esnext", 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "jsxFactory": "createElement", 12 | "outDir": "dist", 13 | "strict": true, 14 | "importHelpers": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/counter/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { DependencyProvider } from '@martin_hotell/rea-di' 2 | import { Component, createElement } from 'react' 3 | 4 | import { Counter } from './counter' 5 | import { CounterService } from './counter.service' 6 | 7 | export class App extends Component { 8 | render() { 9 | return ( 10 |
11 |

Counter app

12 | 13 | 14 | 15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/README.md: -------------------------------------------------------------------------------- 1 | # Counter with Multiple Injectors and hierarchies app 2 | 3 | Run the [Counter with Multiple Injectors and hierarchies app](.) example: 4 | 5 | ``` 6 | git clone https://github.com/hotell/rea-di.git 7 | 8 | cd rea-di/examples/counter-with-multiple-injectors 9 | yarn install 10 | yarn start 11 | ``` 12 | 13 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter-with-multiple-injectors). 14 | 15 | ## Injector tree 16 | 17 | ![Injector tree](../img/counter-with-multiple-di.png) 18 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/enhanced-logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from './logger.service' 2 | 3 | const styles = ` 4 | color: blue; 5 | background-color: yellow; 6 | font-size: large 7 | ` 8 | 9 | export class EnhancedLogger implements Logger { 10 | log(...args: any[]) { 11 | this.enhancedLog(...args) 12 | } 13 | warn(...args: any[]) { 14 | this.enhancedLog(...args) 15 | } 16 | error(...args: any[]) { 17 | this.enhancedLog(...args) 18 | } 19 | 20 | private enhancedLog(...args: any[]) { 21 | console.log('%c%s', styles, ...args) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/app.css: -------------------------------------------------------------------------------- 1 | .app h1 { 2 | font-size: 1.2em; 3 | color: #999; 4 | margin-bottom: 0; 5 | } 6 | .app h2 { 7 | font-size: 2em; 8 | margin-top: 0; 9 | padding-top: 0; 10 | } 11 | .app nav a { 12 | padding: 5px 10px; 13 | text-decoration: none; 14 | margin-top: 10px; 15 | display: inline-block; 16 | background-color: #eee; 17 | border-radius: 4px; 18 | } 19 | .app nav a:visited, 20 | .app a:link { 21 | color: #607d8b; 22 | } 23 | .app nav a:hover { 24 | color: #039be5; 25 | background-color: #cfd8dc; 26 | } 27 | .app nav a.active { 28 | color: #039be5; 29 | } 30 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/messages/messages.service.ts: -------------------------------------------------------------------------------- 1 | import { Stateful } from '@martin_hotell/rea-di' 2 | import { Injectable } from 'injection-js' 3 | 4 | type State = Readonly 5 | const initialState = { 6 | messages: [] as string[], 7 | } 8 | @Injectable() 9 | export class MessageService extends Stateful { 10 | readonly state = initialState 11 | 12 | add(message: string) { 13 | this.setState((prevState) => ({ 14 | messages: [...prevState.messages, message], 15 | })) 16 | } 17 | 18 | clear() { 19 | this.setState((prevState) => ({ messages: [] })) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/types.spec.ts: -------------------------------------------------------------------------------- 1 | import { TypeMap } from '../types' 2 | import { CounterService, Logger } from './setup/services' 3 | 4 | describe(`types`, () => { 5 | describe(`TypeMap`, () => { 6 | const providersTokenMap = { 7 | logger: Logger, 8 | counter: CounterService, 9 | } 10 | 11 | it(`should properly annotate object map with Type/Class tokens`, () => { 12 | type Test = TypeMap 13 | 14 | const expected: Test = { 15 | counter: CounterService, 16 | logger: Logger, 17 | } 18 | 19 | expect(providersTokenMap).toEqual(expected) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/hero-detail.css: -------------------------------------------------------------------------------- 1 | .hero-detail label { 2 | display: inline-block; 3 | width: 3em; 4 | margin: 0.5em 0; 5 | color: #607d8b; 6 | font-weight: bold; 7 | } 8 | .hero-detail input { 9 | height: 2em; 10 | font-size: 1em; 11 | padding-left: 0.4em; 12 | } 13 | .hero-detail button { 14 | margin-top: 20px; 15 | font-family: Arial; 16 | background-color: #eee; 17 | border: none; 18 | padding: 5px 10px; 19 | border-radius: 4px; 20 | cursor: pointer; 21 | cursor: hand; 22 | } 23 | .hero-detail button:hover { 24 | background-color: #cfd8dc; 25 | } 26 | .hero-detail button:disabled { 27 | background-color: #eee; 28 | color: #ccc; 29 | cursor: auto; 30 | } 31 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { DependencyProvider, Inject } from '@martin_hotell/rea-di' 2 | import { Component, createElement } from 'react' 3 | 4 | import { Counter } from './counter' 5 | import { CounterService } from './counter.service' 6 | import { Logger } from './logger.service' 7 | 8 | export class App extends Component { 9 | render() { 10 | return ( 11 |
12 |

Counter app

13 | 14 | 15 | {(counterService) => } 16 | 17 | 18 |
19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "heroes": [ 3 | { 4 | "id": 11, 5 | "name": "Mr. Nice" 6 | }, 7 | { 8 | "id": 12, 9 | "name": "Narco" 10 | }, 11 | { 12 | "id": 13, 13 | "name": "Bombasto" 14 | }, 15 | { 16 | "id": 14, 17 | "name": "Celeritas" 18 | }, 19 | { 20 | "id": 15, 21 | "name": "Magneta" 22 | }, 23 | { 24 | "id": 16, 25 | "name": "RubberMan" 26 | }, 27 | { 28 | "id": 17, 29 | "name": "Dynama" 30 | }, 31 | { 32 | "id": 18, 33 | "name": "Dr IQ" 34 | }, 35 | { 36 | "id": 19, 37 | "name": "Magma" 38 | }, 39 | { 40 | "id": 20, 41 | "name": "Tornado" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/__tests__/setup/services.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from 'injection-js' 2 | 3 | import { Stateful } from '../../services/stateful' 4 | 5 | @Injectable() 6 | export class Logger { 7 | log(...args: any[]) { 8 | console.log(...args) 9 | } 10 | } 11 | 12 | type State = Readonly<{ 13 | count: number 14 | }> 15 | 16 | @Injectable() 17 | export class CounterService extends Stateful { 18 | readonly state: State = { count: 0 } 19 | 20 | constructor(private logger: Logger) { 21 | super() 22 | } 23 | inc() { 24 | this.logger.log('inc called') 25 | this.setState((prevState) => ({ count: prevState.count + 1 })) 26 | } 27 | dec() { 28 | this.logger.log('dec called') 29 | this.setState((prevState) => ({ count: prevState.count - 1 })) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config/types.js: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | // ===== JEST ==== 4 | 5 | /** 6 | * @typedef {import('ts-jest/dist/types').TsJestConfig} TsJestConfig 7 | */ 8 | 9 | // @TODO https://github.com/Microsoft/TypeScript/issues/24916 10 | /** 11 | * @typedef {Partial} JestConfig 12 | */ 13 | 14 | /** 15 | * @typedef {typeof import('jest-config').defaults} JestDefaultConfig 16 | */ 17 | 18 | // ==== PRETTIER ==== 19 | /** 20 | * @typedef {import('prettier').Options} PrettierConfig 21 | */ 22 | 23 | // ==== ROLLUP ==== 24 | /** 25 | * @typedef {import('rollup').InputOptions & { output: import('rollup').OutputOptions | Array }} RollupConfig 26 | */ 27 | 28 | /** 29 | * @typedef {import('rollup').Plugin} RollupPlugin 30 | */ 31 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-example", 3 | "version": "1.0.0", 4 | "main": "src/index.html", 5 | "author": "Martin Hochel ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "parcel ./src/index.html", 9 | "build": "rm -rf dist && parcel build src/index.html" 10 | }, 11 | "dependencies": { 12 | "@abraham/reflection": "0.4.2", 13 | "@martin_hotell/rea-di": "1.0.0", 14 | "core-js": "2.5.7", 15 | "injection-js": "2.2.1", 16 | "papercss": "1.6.0", 17 | "react": "16.4.1", 18 | "react-dom": "16.4.1", 19 | "tslib": "1.9.3" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "16.7.7", 23 | "@types/react-dom": "16.0.11", 24 | "parcel-bundler": "1.10.3", 25 | "typescript": "3.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/counter/src/app/counter.service.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-magic-numbers 2 | import { Stateful } from '@martin_hotell/rea-di' 3 | import { Injectable } from 'injection-js' 4 | 5 | type State = Readonly 6 | 7 | const initialState = { 8 | count: 0, 9 | } 10 | 11 | @Injectable() 12 | export class CounterService extends Stateful { 13 | readonly state = initialState 14 | 15 | increment() { 16 | this.setState((prevState) => ({ count: prevState.count + 1 })) 17 | } 18 | decrement() { 19 | this.setState((prevState) => ({ count: prevState.count - 1 })) 20 | } 21 | incrementIfOdd() { 22 | if (this.state.count % 2 !== 0) { 23 | this.increment() 24 | } 25 | } 26 | incrementAsync() { 27 | setTimeout(() => this.increment(), 1000) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/counter-with-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-example", 3 | "version": "1.0.0", 4 | "main": "src/index.html", 5 | "author": "Martin Hochel ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "parcel ./src/index.html", 9 | "build": "rm -rf dist && parcel build src/index.html" 10 | }, 11 | "dependencies": { 12 | "@abraham/reflection": "0.4.2", 13 | "@martin_hotell/rea-di": "1.0.0", 14 | "core-js": "2.5.7", 15 | "injection-js": "2.2.1", 16 | "papercss": "1.6.0", 17 | "react": "16.4.1", 18 | "react-dom": "16.4.1", 19 | "tslib": "1.9.3" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "16.7.7", 23 | "@types/react-dom": "16.0.11", 24 | "parcel-bundler": "1.10.3", 25 | "typescript": "3.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/github-user/src/app/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface GithubUser { 2 | login: string 3 | id: 1 4 | node_id: string 5 | avatar_url: string 6 | gravatar_id: string 7 | url: string 8 | html_url: string 9 | followers_url: string 10 | following_url: string 11 | gists_url: string 12 | starred_url: string 13 | subscriptions_url: string 14 | organizations_url: string 15 | repos_url: string 16 | events_url: string 17 | received_events_url: string 18 | type: string 19 | site_admin: false 20 | name: string 21 | company: string 22 | blog: string 23 | location: string 24 | email: string 25 | hireable: false 26 | bio: string 27 | public_repos: number 28 | public_gists: number 29 | followers: number 30 | following: number 31 | created_at: string 32 | updated_at: string 33 | } 34 | -------------------------------------------------------------------------------- /config/jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Partial} 3 | */ 4 | const config = { 5 | preset: 'ts-jest', 6 | rootDir: '..', 7 | testMatch: [ 8 | '/src/**/__tests__/**/?(*.)+(spec|test).ts?(x)', 9 | '/src/**/?(*.)+(spec|test).ts?(x)', 10 | ], 11 | testPathIgnorePatterns: ['dist'], 12 | coverageThreshold: { 13 | global: { 14 | branches: 80, 15 | functions: 80, 16 | lines: 80, 17 | statements: 80, 18 | }, 19 | }, 20 | setupFiles: ['/config/setup-tests.js'], 21 | setupTestFrameworkScriptFile: '/config/setup-enzyme.js', 22 | snapshotSerializers: ['enzyme-to-json/serializer'], 23 | watchPlugins: [ 24 | 'jest-watch-typeahead/filename', 25 | 'jest-watch-typeahead/testname', 26 | ], 27 | } 28 | 29 | module.exports = config 30 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter-example", 3 | "version": "1.0.0", 4 | "main": "src/index.html", 5 | "author": "Martin Hochel ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "parcel ./src/index.html", 9 | "build": "rm -rf dist && parcel build src/index.html" 10 | }, 11 | "dependencies": { 12 | "@abraham/reflection": "0.4.2", 13 | "@martin_hotell/rea-di": "1.0.0", 14 | "core-js": "2.5.7", 15 | "injection-js": "2.2.1", 16 | "papercss": "1.6.0", 17 | "react": "16.4.1", 18 | "react-dom": "16.4.1", 19 | "tslib": "1.9.3" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "16.7.7", 23 | "@types/react-dom": "16.0.11", 24 | "parcel-bundler": "1.10.3", 25 | "typescript": "3.1.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _If there is a linked issue, mention it here._ 2 | 3 | - [ ] Bug 4 | - [ ] Feature 5 | 6 | ## Requirements 7 | 8 | - [ ] Read the [contribution guidelines](./CONTRIBUTING.md). 9 | - [ ] Wrote tests. 10 | - [ ] Updated docs and upgrade instructions, if necessary. 11 | 12 | ## Rationale 13 | 14 | _Why is this PR necessary?_ 15 | 16 | ## Implementation 17 | 18 | _Why have you implemented it this way? Did you try any other methods?_ 19 | 20 | ## Open questions 21 | 22 | _Are there any open questions about this implementation that need answers?_ 23 | 24 | ## Other 25 | 26 | _Is there anything else we should know? Delete this section if you don't need it._ 27 | 28 | ## Tasks 29 | 30 | _List any tasks you need to do here, if any. Delete this section if you don't need it._ 31 | 32 | - [ ] _Example task._ 33 | -------------------------------------------------------------------------------- /examples/github-user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-user-example", 3 | "version": "1.0.0", 4 | "main": "src/index.html", 5 | "author": "Martin Hochel ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "parcel ./src/index.html", 9 | "build": "rm -rf dist && parcel build src/index.html" 10 | }, 11 | "dependencies": { 12 | "@abraham/reflection": "0.4.2", 13 | "@martin_hotell/axios-http": "0.3.1", 14 | "@martin_hotell/rea-di": "1.0.0", 15 | "axios": "0.18.0", 16 | "core-js": "2.5.7", 17 | "injection-js": "2.2.1", 18 | "papercss": "1.6.0", 19 | "react": "16.4.1", 20 | "react-dom": "16.4.1", 21 | "tslib": "1.9.3" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "16.7.7", 25 | "@types/react-dom": "16.0.11", 26 | "parcel-bundler": "1.10.3", 27 | "typescript": "3.1.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": [ 7 | "dom", 8 | "es2018" 9 | ], 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "jsx": "react", 15 | "sourceMap": true, 16 | "outDir": "dist/esm5", 17 | "declaration": true, 18 | "declarationDir": "dist/types", 19 | "declarationMap": true, 20 | "stripInternal": true, 21 | "importHelpers": true, 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | "resolveJsonModule": true 25 | }, 26 | "include": [ 27 | "./src" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "dist" 32 | ], 33 | "compileOnSave": false, 34 | "buildOnSave": false 35 | } 36 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/messages/messages.css: -------------------------------------------------------------------------------- 1 | /* MessagesComponent's private CSS styles */ 2 | .messages { 3 | margin: 2em; 4 | } 5 | 6 | .messages h2 { 7 | color: red; 8 | font-family: Arial, Helvetica, sans-serif; 9 | font-weight: lighter; 10 | } 11 | .messages button.clear { 12 | font-family: Arial; 13 | background-color: #eee; 14 | border: none; 15 | padding: 5px 10px; 16 | border-radius: 4px; 17 | cursor: pointer; 18 | cursor: hand; 19 | } 20 | .messages button:hover { 21 | background-color: #cfd8dc; 22 | } 23 | .messages button:disabled { 24 | background-color: #eee; 25 | color: #aaa; 26 | cursor: auto; 27 | } 28 | .messages button.clear { 29 | color: #888; 30 | margin-bottom: 12px; 31 | } 32 | 33 | /* 34 | Copyright 2017-2018 Google Inc. All Rights Reserved. 35 | Use of this source code is governed by an MIT-style license that 36 | can be found in the LICENSE file at http://angular.io/license 37 | */ 38 | -------------------------------------------------------------------------------- /examples/github-user/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { registerHttpClientProviders } from '@martin_hotell/axios-http' 2 | import { DependencyProvider } from '@martin_hotell/rea-di' 3 | import { Component, createElement, Fragment } from 'react' 4 | 5 | import { Profile } from './components/profile' 6 | import SearchUser from './components/search-user' 7 | import { GithubUserService } from './user.service' 8 | 9 | export class App extends Component { 10 | render() { 11 | return ( 12 |
13 |

GitHub User Search 👀

14 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/hero-search.css: -------------------------------------------------------------------------------- 1 | /* HeroSearch private styles */ 2 | .hero-search { 3 | } 4 | 5 | .hero-search [name='searchBox'] { 6 | width: 200px; 7 | padding: 5px; 8 | } 9 | 10 | .search-result { 11 | width: 200px; 12 | margin-top: 0; 13 | padding-left: 0; 14 | } 15 | 16 | .search-result li { 17 | border: 1px solid gray; 18 | border-top: none; 19 | padding: 5px; 20 | background-color: white; 21 | cursor: pointer; 22 | list-style-type: none; 23 | } 24 | 25 | .search-result li:hover { 26 | background-color: #607d8b; 27 | } 28 | 29 | .search-result li a { 30 | color: #888; 31 | display: block; 32 | text-decoration: none; 33 | } 34 | 35 | .search-result li a:hover, 36 | .search-result li a:active { 37 | color: white; 38 | } 39 | 40 | /* 41 | Copyright 2017-2018 Google Inc. All Rights Reserved. 42 | Use of this source code is governed by an MIT-style license that 43 | can be found in the LICENSE file at http://angular.io/license 44 | */ 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## Bug report 6 | 7 | - rea-di version: _x.x.x_ () 8 | - Affected browsers (and versions): _IE 10_ 9 | 10 | ### Current behaviour 11 | 12 | 13 | 14 | ```ts 15 | // put code here 16 | ``` 17 | 18 | 19 | 20 | [issue demo](https://codesandbox.io/) 21 | 22 | ### Expected behaviour 23 | 24 | _Please explain how you'd expect it to behave._ 25 | 26 | 27 | 28 | 29 | 30 | ## Feature request 31 | 32 | ### Use case(s) 33 | 34 | _Explain the rationale for this feature._ 35 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tour-of-heroes", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Martin Hochel ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "concurrently \"npm:server:app\" \"npm:server:api\"", 9 | "server:app": "parcel ./src/index.html", 10 | "server:api": "json-server --watch src/db.json" 11 | }, 12 | "dependencies": { 13 | "@abraham/reflection": "0.4.2", 14 | "@martin_hotell/rea-di": "1.0.0", 15 | "axios": "0.18.0", 16 | "core-js": "2.5.7", 17 | "injection-js": "2.2.1", 18 | "react": "16.4.1", 19 | "react-dom": "16.4.1", 20 | "react-router-dom": "4.3.1", 21 | "rxjs": "6.3.3", 22 | "tslib": "1.9.3" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "16.7.7", 26 | "@types/react-dom": "16.0.11", 27 | "@types/react-router-dom": "4.3.1", 28 | "concurrently": "4.1.0", 29 | "json-server": "0.14.0", 30 | "parcel-bundler": "1.10.3", 31 | "typescript": "3.1.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/app/counter.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | import { Component, createElement, Fragment } from 'react' 3 | 4 | import { CounterService } from './counter.service' 5 | 6 | type Props = { 7 | counterService: CounterService 8 | } 9 | export class Counter extends Component { 10 | render() { 11 | const { counterService } = this.props 12 | 13 | return ( 14 | 15 |

16 | Open you browser devtools console... and start clicking on buttons ;) 17 |

18 |

19 | Clicked: {counterService.value} times{' '} 20 | {' '} 21 | {' '} 22 | {' '} 25 | 28 |

29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react' 2 | 3 | import { reflection } from '../facade/lang' 4 | import { Constructor } from '../types' 5 | 6 | export const getComponentDisplayName =

(Component: ComponentType

) => 7 | Component.displayName || 8 | Component.name || 9 | (Component.constructor && Component.constructor.name) || 10 | 'Component' 11 | 12 | export const createHOCName =

( 13 | Wrapper: ComponentType, 14 | WrappedComponent: ComponentType

15 | ) => 16 | `${getComponentDisplayName(Wrapper)}(${getComponentDisplayName( 17 | WrappedComponent 18 | )})` 19 | 20 | // tslint:disable-next-line:no-empty 21 | export const noop = () => {} 22 | 23 | export const tuple = (...args: T): T => args 24 | 25 | export const metadataKey = '__metadata__' 26 | export const optional = (token: T) => { 27 | const withOptionalIdentity = () => token 28 | reflection.defineMetadata( 29 | metadataKey, 30 | { optional: true }, 31 | withOptionalIdentity 32 | ) 33 | 34 | return (withOptionalIdentity as any) as T | null 35 | } 36 | -------------------------------------------------------------------------------- /examples/github-user/src/app/components/repos.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createElement } from 'react' 2 | 3 | import { GithubUserRepo } from '../repo.model' 4 | 5 | type Props = { repos: GithubUserRepo[]; username: string } 6 | export class Repos extends Component { 7 | render() { 8 | const { repos, username } = this.props 9 | 10 | return ( 11 |

12 |

@{username} Repos

13 |
14 | {repos.map((repo) => ( 15 |
16 |
17 | {repo.html_url && ( 18 |

19 | {repo.name} 20 |

21 | )} 22 | {repo.description && ( 23 |

{repo.description}

24 | )} 25 |
26 |
27 | ))} 28 |
29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/counter.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | 3 | import { Inject } from '@martin_hotell/rea-di' 4 | import { Component, createElement, Fragment } from 'react' 5 | 6 | import { CounterService } from './counter.service' 7 | 8 | type Props = {} 9 | export class Counter extends Component { 10 | render() { 11 | return ( 12 | 13 | {(counterService) => ( 14 | 15 |

16 | Clicked: {counterService.value} times{' '} 17 | {' '} 18 | {' '} 19 | {' '} 22 | 25 |

26 |
27 | )} 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/github-user/src/app/components/profile.tsx: -------------------------------------------------------------------------------- 1 | import { Inject } from '@martin_hotell/rea-di' 2 | import { Component, createElement } from 'react' 3 | 4 | import { GithubUserService } from '../user.service' 5 | import { Repos } from './repos' 6 | import { UserProfile } from './user-profile' 7 | 8 | export class Profile extends Component { 9 | render() { 10 | return ( 11 | 12 | {(userService) => { 13 | const { username, repos, bio } = userService.state 14 | 15 | if (bio && repos) { 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | ) 26 | } 27 | 28 | if (username) { 29 | return `Loading... ${username}` 30 | } 31 | }} 32 |
33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Martin Hochel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/counter/src/app/counter.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | // tslint:disable:no-shadowed-variable 3 | import { Inject } from '@martin_hotell/rea-di' 4 | import { Component, createElement } from 'react' 5 | 6 | import { CounterService } from './counter.service' 7 | 8 | import './counter.css' 9 | 10 | export class Counter extends Component { 11 | render() { 12 | return ( 13 | 14 | {(counterService) => ( 15 |
16 |

Clicked: {counterService.state.count} times

17 |

18 | 19 | 20 | 23 | 26 |

27 |
28 | )} 29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/github-user/src/app/components/user-profile.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createElement } from 'react' 2 | 3 | import { GithubUser } from '../user.model' 4 | 5 | type Props = { username: string; bio: GithubUser } 6 | export class UserProfile extends Component { 7 | render() { 8 | const { bio } = this.props 9 | 10 | return ( 11 |
12 | {bio.avatar_url && ( 13 | 14 | )} 15 | 16 |
    17 | {bio.name &&
  • Name: {bio.name}
  • } 18 | {bio.login &&
  • Username: {bio.login}
  • } 19 | {bio.email &&
  • Email: {bio.email}
  • } 20 | {bio.location &&
  • Location: {bio.location}
  • } 21 | {bio.company &&
  • Company: {bio.company}
  • } 22 | {bio.followers &&
  • Followers: {bio.followers}
  • } 23 | {bio.following &&
  • Following: {bio.following}
  • } 24 | {bio.public_repos &&
  • Public Repos: {bio.public_repos}
  • } 25 | {bio.blog && ( 26 |
  • 27 | Blog: {bio.blog} 28 |
  • 29 | )} 30 |
31 |
32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/messages/messages.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | 3 | import { Inject } from '@martin_hotell/rea-di' 4 | import React, { Component } from 'react' 5 | 6 | import './messages.css' 7 | 8 | import { MessageService } from './messages.service' 9 | 10 | export class Messages extends Component { 11 | render() { 12 | return ( 13 |
14 |

Messages

15 |
16 | 17 | {(messageService) => 18 | messageService.state.messages.length ? ( 19 | <> 20 | 26 | {messageService.state.messages.map((message, idx) => ( 27 |
{message}
28 | ))} 29 | 30 | ) : ( 31 |

There are no messages in store...

32 | ) 33 | } 34 |
35 |
36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | camelCaseToDash, 3 | dashToCamelCase, 4 | toUpperCase, 5 | pascalCase, 6 | normalizePackageName, 7 | getOutputFileName, 8 | } 9 | 10 | /** 11 | * 12 | * @param {string} myStr 13 | */ 14 | function camelCaseToDash(myStr) { 15 | return myStr.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() 16 | } 17 | 18 | /** 19 | * 20 | * @param {string} myStr 21 | */ 22 | function dashToCamelCase(myStr) { 23 | return myStr.replace(/-([a-z])/g, (g) => g[1].toUpperCase()) 24 | } 25 | 26 | /** 27 | * 28 | * @param {string} myStr 29 | */ 30 | function toUpperCase(myStr) { 31 | return `${myStr.charAt(0).toUpperCase()}${myStr.substr(1)}` 32 | } 33 | 34 | /** 35 | * 36 | * @param {string} myStr 37 | */ 38 | function pascalCase(myStr) { 39 | return toUpperCase(dashToCamelCase(myStr)) 40 | } 41 | 42 | /** 43 | * 44 | * @param {string} rawPackageName 45 | */ 46 | function normalizePackageName(rawPackageName) { 47 | const scopeEnd = rawPackageName.indexOf('/') + 1 48 | 49 | return rawPackageName.substring(scopeEnd) 50 | } 51 | 52 | /** 53 | * 54 | * @param {string} fileName 55 | * @param {boolean?} isProd 56 | */ 57 | function getOutputFileName(fileName, isProd = false) { 58 | return isProd ? fileName.replace(/\.js$/, '.min.js') : fileName 59 | } 60 | -------------------------------------------------------------------------------- /scripts/copy.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync, copyFileSync } = require('fs') 2 | const { resolve } = require('path') 3 | const packageJson = require('../package.json') 4 | 5 | main() 6 | 7 | function main() { 8 | const projectRoot = resolve(__dirname, '..') 9 | const distPath = resolve(projectRoot, 'dist') 10 | const distPackageJson = createDistPackageJson(packageJson) 11 | 12 | copyFileSync( 13 | resolve(projectRoot, 'README.md'), 14 | resolve(distPath, 'README.md') 15 | ) 16 | copyFileSync( 17 | resolve(projectRoot, 'CHANGELOG.md'), 18 | resolve(distPath, 'CHANGELOG.md') 19 | ) 20 | copyFileSync( 21 | resolve(projectRoot, 'LICENSE.md'), 22 | resolve(distPath, 'LICENSE.md') 23 | ) 24 | copyFileSync( 25 | resolve(projectRoot, '.npmignore'), 26 | resolve(distPath, '.npmignore') 27 | ) 28 | writeFileSync(resolve(distPath, 'package.json'), distPackageJson) 29 | } 30 | 31 | /** 32 | * @param {typeof packageJson} packageConfig 33 | * @return {string} 34 | */ 35 | function createDistPackageJson(packageConfig) { 36 | const { 37 | devDependencies, 38 | scripts, 39 | engines, 40 | config, 41 | husky, 42 | 'lint-staged': lintStaged, 43 | ...distPackageJson 44 | } = packageConfig 45 | 46 | return JSON.stringify(distPackageJson, null, 2) 47 | } 48 | -------------------------------------------------------------------------------- /examples/github-user/src/app/user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@martin_hotell/axios-http' 2 | import { Injectable } from 'injection-js' 3 | 4 | import { Stateful } from '@martin_hotell/rea-di' 5 | import { GithubUserRepo } from './repo.model' 6 | import { GithubUser } from './user.model' 7 | 8 | const endpointPath = 'users' 9 | 10 | type State = Readonly 11 | const initialState = { 12 | username: '', 13 | bio: null as GithubUser | null, 14 | repos: null as GithubUserRepo[] | null, 15 | } 16 | 17 | @Injectable() 18 | export class GithubUserService extends Stateful { 19 | readonly state: State = initialState 20 | constructor(private http: HttpClient) { 21 | super() 22 | } 23 | setActiveUser(user: Partial) { 24 | this.setState((prevState) => ({ ...prevState, ...user })) 25 | } 26 | getRepos(username: string) { 27 | return this.http.get(`${endpointPath}/${username}/repos`) 28 | } 29 | 30 | getUserInfo(username: string) { 31 | return this.http.get(`${endpointPath}/${username}`) 32 | } 33 | 34 | getGithubInfo(username: string) { 35 | return Promise.all([ 36 | this.getRepos(username), 37 | this.getUserInfo(username), 38 | ]).then(([repos, bio]) => ({ repos: repos.data, bio: bio.data })) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/guards.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Type, TypeProvider } from 'injection-js' 2 | 3 | import { Nullable } from '../types' 4 | 5 | export const isBlank = (value: T): value is Nullable => value == null 6 | 7 | export const isPresent = (value: T): value is NonNullable => value != null 8 | 9 | // tslint:disable-next-line:ban-types 10 | export const isFunction = (value: any): value is Function => 11 | typeof value === 'function' 12 | 13 | export const isString = (value: any): value is string => 14 | typeof value === 'string' 15 | 16 | export const isJsLikeObject = (value: any): value is T => 17 | isPresent(value) && typeof value === 'object' 18 | 19 | export const isArray = (value: any): value is Array => 20 | Array.isArray(value) 21 | 22 | export const isObject = (value: T): value is T extends object ? T : never => 23 | isJsLikeObject(value) && !isArray(value) 24 | 25 | // ======================= 26 | // library specific guards 27 | // ======================= 28 | 29 | // @TODO create PR to expose this API - injection-js 30 | export const isType = (value: any): value is Type => 31 | isFunction(value) 32 | 33 | export const isProvider = ( 34 | value: any 35 | ): value is Exclude => { 36 | return isPresent(value) && isObject(value) && 'provide' in value 37 | } 38 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'injection-js' 2 | import { ComponentClass, ComponentType } from 'react' 3 | 4 | export type StateCallback = (state: T) => Partial 5 | 6 | export type TypeMap< 7 | T extends { [key: string]: Type } = { [key: string]: Type } 8 | > = { [K in keyof T]: T[K] } 9 | export type NullableTypeMap< 10 | T extends { [key: string]: Type | null } = { 11 | [key: string]: Type | null 12 | } 13 | > = { [K in keyof T]: T[K] | null } 14 | 15 | export type StringMap = { [key: string]: T } 16 | 17 | export type Values = T extends { [k: string]: infer V } 18 | ? V 19 | : never 20 | 21 | export type HoC< 22 | P, 23 | OriginalComponent extends ComponentType 24 | > = ComponentClass

& { 25 | WrappedComponent: OriginalComponent 26 | } 27 | 28 | export type InstanceTypes = { 29 | [P in keyof T]: T[P] extends Constructor ? U : any 30 | } 31 | 32 | export type NullableInstanceTypes = { 33 | [P in keyof T]: T[P] extends Constructor 34 | ? U 35 | : T[P] extends Constructor | null 36 | ? I | null 37 | : any 38 | } 39 | 40 | export type Constructor = new (...args: any[]) => T 41 | 42 | export type Nullable = T extends null | undefined ? T : never 43 | 44 | export type Omit = Pick> 45 | export type Subtract = Omit 46 | -------------------------------------------------------------------------------- /src/components/provider.hoc.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ComponentType } from 'react' 2 | 3 | import { HoC, Subtract } from '../types' 4 | import { createHOCName } from '../utils/helpers' 5 | import { DependencyProvider } from './provider' 6 | 7 | type ProvidersSetup = DependencyProvider['props']['providers'] 8 | 9 | /** 10 | * While this may give better performance, because provide array will be created only once, 11 | * prefer to use standard ... within you render tree 12 | * 13 | * @param providers - providers configuration to be registered within injector 14 | */ 15 | export const withDependencyProvider = ( 16 | ...providers: T 17 | ) => >( 18 | Cmp: ComponentType 19 | ): HoC => { 20 | class WithDependencyProvider extends Component { 21 | static displayName: string = createHOCName(WithDependencyProvider, Cmp) 22 | 23 | static readonly WrappedComponent = Cmp 24 | 25 | render() { 26 | const { ...rest } = this.props as object /* FIXED in ts 3.2 */ 27 | 28 | return ( 29 | 30 | 31 | 32 | ) 33 | } 34 | } 35 | 36 | return WithDependencyProvider 37 | } 38 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DashboardComponent's private CSS styles */ 2 | .dashboard [class*='col-'] { 3 | display: block; 4 | margin-right: 10px; 5 | margin-bottom: 20px; 6 | } 7 | .dashboard [class*='col-']:last-of-type { 8 | margin-right: 0px; 9 | } 10 | .dashboard a { 11 | text-decoration: none; 12 | } 13 | .dashboard *, 14 | .dashboard *:after, 15 | .dashboard *:before { 16 | -webkit-box-sizing: border-box; 17 | -moz-box-sizing: border-box; 18 | box-sizing: border-box; 19 | } 20 | .dashboard h3 { 21 | text-align: center; 22 | margin-bottom: 0; 23 | } 24 | .dashboard h4 { 25 | position: relative; 26 | } 27 | .dashboard .grid { 28 | margin: 0; 29 | display: flex; 30 | flex-wrap: wrap; 31 | } 32 | .dashboard .col-1-4 { 33 | flex: 1 0 auto; 34 | } 35 | .dashboard .module { 36 | padding: 20px; 37 | text-align: center; 38 | color: #eee; 39 | max-height: 120px; 40 | min-width: 120px; 41 | background-color: #607d8b; 42 | border-radius: 2px; 43 | cursor: pointer; 44 | } 45 | .dashboard .module:hover { 46 | background-color: #eee; 47 | color: #607d8b; 48 | } 49 | .dashboard .grid-pad { 50 | padding: 10px 0; 51 | } 52 | 53 | @media (max-width: 600px) { 54 | .dashboard .module { 55 | font-size: 10px; 56 | max-height: 75px; 57 | } 58 | } 59 | @media (max-width: 1024px) { 60 | .dashboard .grid { 61 | margin: 0; 62 | } 63 | .dashboard .module { 64 | min-width: 60px; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/counter-with-logger/src/app/counter.service.ts: -------------------------------------------------------------------------------- 1 | import { Stateful } from '@martin_hotell/rea-di' 2 | import { Injectable } from 'injection-js' 3 | 4 | import { Logger } from './logger.service' 5 | 6 | type State = Readonly 7 | const initialState = { 8 | count: 0, 9 | } 10 | 11 | @Injectable() 12 | export class CounterService extends Stateful { 13 | readonly state = initialState 14 | 15 | constructor(private logger: Logger) { 16 | super() 17 | } 18 | 19 | get value() { 20 | return this.state.count 21 | } 22 | 23 | onIncrement() { 24 | this.setState((prevState) => ({ count: prevState.count + 1 })) 25 | this.logger.log( 26 | `CounterService: increment called and count set to: ${this.state.count}` 27 | ) 28 | } 29 | onDecrement() { 30 | this.setState((prevState) => ({ count: prevState.count - 1 })) 31 | this.logger.log( 32 | `CounterService: decrement called and count set to: ${this.state.count}` 33 | ) 34 | } 35 | incrementIfOdd() { 36 | const ODD_NUMBER = 2 37 | if (this.state.count % ODD_NUMBER !== 0) { 38 | this.onIncrement() 39 | 40 | return 41 | } 42 | 43 | this.logger.warn( 44 | `CounterService: count is not Odd number, skipping state increment!` 45 | ) 46 | } 47 | 48 | incrementAsync() { 49 | const DELAY = 1000 50 | this.logger.warn(`CounterService: I'll increment state after 1 second !`) 51 | setTimeout(() => this.onIncrement(), DELAY) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/styles.css: -------------------------------------------------------------------------------- 1 | /* Master Styles */ 2 | h1 { 3 | color: #369; 4 | font-family: Arial, Helvetica, sans-serif; 5 | font-size: 250%; 6 | } 7 | h2, 8 | h3 { 9 | color: #444; 10 | font-family: Arial, Helvetica, sans-serif; 11 | font-weight: lighter; 12 | } 13 | body { 14 | margin: 2em; 15 | } 16 | body, 17 | input[text], 18 | button { 19 | color: #888; 20 | font-family: Cambria, Georgia; 21 | } 22 | a { 23 | cursor: pointer; 24 | cursor: hand; 25 | } 26 | button { 27 | font-family: Arial; 28 | background-color: #eee; 29 | border: none; 30 | padding: 5px 10px; 31 | border-radius: 4px; 32 | cursor: pointer; 33 | cursor: hand; 34 | } 35 | button:hover { 36 | background-color: #cfd8dc; 37 | } 38 | button:disabled { 39 | background-color: #eee; 40 | color: #aaa; 41 | cursor: auto; 42 | } 43 | 44 | /* Navigation link styles */ 45 | nav a { 46 | padding: 5px 10px; 47 | text-decoration: none; 48 | margin-right: 10px; 49 | margin-top: 10px; 50 | display: inline-block; 51 | background-color: #eee; 52 | border-radius: 4px; 53 | } 54 | nav a:visited, 55 | a:link { 56 | color: #607d8b; 57 | } 58 | nav a:hover { 59 | color: #039be5; 60 | background-color: #cfd8dc; 61 | } 62 | nav a.active { 63 | color: #039be5; 64 | } 65 | 66 | /* everywhere else */ 67 | * { 68 | font-family: Arial, Helvetica, sans-serif; 69 | } 70 | 71 | /* 72 | Copyright 2017-2018 Google Inc. All Rights Reserved. 73 | Use of this source code is governed by an MIT-style license that 74 | can be found in the LICENSE file at http://angular.io/license 75 | */ 76 | -------------------------------------------------------------------------------- /src/components/inject.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from 'react' 2 | 3 | import { reflection } from '../facade/lang' 4 | import { Context, ContextApi } from '../services/injector-context' 5 | import { Constructor, NullableInstanceTypes } from '../types' 6 | import { metadataKey } from '../utils/helpers' 7 | 8 | type InjectProps> = { 9 | values: C 10 | children: (...injectables: NullableInstanceTypes) => ReactNode 11 | } 12 | 13 | export class Inject> extends Component< 14 | InjectProps 15 | > { 16 | private injectMappedProviders = ({ injector }: ContextApi) => { 17 | const injectables = this.props.values.map((nextInjectableRef) => { 18 | if (!nextInjectableRef) { 19 | return null 20 | } 21 | 22 | const annotationsMeta = reflection.getMetadata( 23 | metadataKey, 24 | nextInjectableRef 25 | ) as undefined | { optional: boolean } 26 | 27 | if (!annotationsMeta) { 28 | return injector.get(nextInjectableRef) 29 | } 30 | 31 | const isOptional = annotationsMeta && annotationsMeta.optional 32 | const notFoundValue = isOptional ? null : undefined 33 | const wrappedInjectableRef = ((nextInjectableRef as any) as () => Constructor)() 34 | 35 | return injector.get(wrappedInjectableRef, notFoundValue) 36 | }) as NullableInstanceTypes 37 | 38 | return this.props.children(...(injectables as any)) 39 | } 40 | render() { 41 | return {this.injectMappedProviders} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/multiply-counter.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, InjectionToken, Optional } from 'injection-js' 2 | 3 | import { CounterService } from './counter.service' 4 | import { getClassName } from './helpers' 5 | import { Logger } from './logger.service' 6 | 7 | const defaultConfig = { 8 | multiplyBy: 2, 9 | } 10 | export type MultiplyCounterConfig = typeof defaultConfig 11 | export const MultiplyCounterConfig = new InjectionToken( 12 | 'MultiplyCounterConfig' 13 | ) 14 | 15 | @Injectable() 16 | export class MultiplyCounterService extends CounterService { 17 | constructor( 18 | @Optional() 19 | @Inject(MultiplyCounterConfig) 20 | private config: MultiplyCounterConfig, 21 | public logger: Logger 22 | ) { 23 | super(logger) 24 | this.config = config || defaultConfig 25 | } 26 | onIncrement() { 27 | this.setState((prevState) => { 28 | const newCount = 29 | prevState.count === 0 30 | ? (prevState.count + 1) * this.config.multiplyBy 31 | : prevState.count * this.config.multiplyBy 32 | 33 | return { count: newCount } 34 | }) 35 | 36 | this.logger.log( 37 | `${getClassName(this)}: increment called and count set to: ${ 38 | this.state.count 39 | }` 40 | ) 41 | } 42 | 43 | onDecrement() { 44 | this.setState((prevState) => ({ 45 | count: prevState.count / this.config.multiplyBy, 46 | })) 47 | this.logger.log( 48 | `${getClassName(this)}: decrement called and count set to: ${ 49 | this.state.count 50 | }` 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import './dashboard.css' 2 | 3 | import React, { Component } from 'react' 4 | import { Link } from 'react-router-dom' 5 | 6 | import { Hero } from '../hero' 7 | import { HeroService } from '../hero.service' 8 | import { HeroSearch } from './hero-search' 9 | 10 | type Props = { 11 | heroService: HeroService 12 | } 13 | type State = Readonly 14 | 15 | const initialState = { 16 | heroes: [] as Hero[], 17 | } 18 | 19 | export class Dashboard extends Component { 20 | readonly state = initialState 21 | render() { 22 | const { heroes } = this.state 23 | 24 | return ( 25 | <> 26 |

27 |

Top Heroes

28 |
29 | {heroes.map((hero) => ( 30 | 35 |
36 |

{hero.name}

37 |
38 | 39 | ))} 40 |
41 | 42 | 43 |
44 | 45 | ) 46 | } 47 | 48 | componentDidMount() { 49 | this.props.heroService 50 | .getHeroes() 51 | // This getHeroes reduces the number of heroes displayed to four (2nd, 3rd, 4th, and 5th). 52 | // tslint:disable-next-line:no-magic-numbers 53 | .then((heroes) => this.setState({ heroes: heroes.slice(1, 5) })) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/counter.service.ts: -------------------------------------------------------------------------------- 1 | import { Stateful } from '@martin_hotell/rea-di' 2 | import { Injectable } from 'injection-js' 3 | 4 | import { getClassName } from './helpers' 5 | import { Logger } from './logger.service' 6 | 7 | type State = Readonly 8 | const initialState = { 9 | count: 0, 10 | } 11 | 12 | @Injectable() 13 | export class CounterService extends Stateful { 14 | readonly state = initialState 15 | 16 | constructor(public logger: Logger) { 17 | super() 18 | } 19 | 20 | get value() { 21 | return this.state.count 22 | } 23 | 24 | onIncrement() { 25 | this.setState((prevState) => ({ count: prevState.count + 1 })) 26 | this.logger.log( 27 | `${getClassName(this)}: increment called and count set to: ${ 28 | this.state.count 29 | }` 30 | ) 31 | } 32 | onDecrement() { 33 | this.setState((prevState) => ({ count: prevState.count - 1 })) 34 | this.logger.log( 35 | `${getClassName(this)}: decrement called and count set to: ${ 36 | this.state.count 37 | }` 38 | ) 39 | } 40 | incrementIfOdd() { 41 | const ODD_NUMBER = 2 42 | if (this.state.count % ODD_NUMBER !== 0) { 43 | this.onIncrement() 44 | 45 | return 46 | } 47 | 48 | this.logger.warn( 49 | `${getClassName( 50 | this 51 | )}: count is not Odd number, skipping state increment!` 52 | ) 53 | } 54 | 55 | incrementAsync() { 56 | const DELAY = 1000 57 | 58 | this.logger.warn( 59 | `${getClassName(this)}: I'll increment state after 1 second !` 60 | ) 61 | 62 | setTimeout(() => this.onIncrement(), DELAY) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/heroes.css: -------------------------------------------------------------------------------- 1 | /* HeroesComponent's private CSS styles */ 2 | .heroes { 3 | margin: 0 0 2em 0; 4 | list-style-type: none; 5 | padding: 0; 6 | width: 15em; 7 | } 8 | .heroes li { 9 | position: relative; 10 | cursor: pointer; 11 | background-color: #eee; 12 | margin: 0.5em; 13 | padding: 0.3em 0; 14 | height: 1.6em; 15 | border-radius: 4px; 16 | } 17 | 18 | .heroes li:hover { 19 | color: #607d8b; 20 | background-color: #ddd; 21 | left: 0.1em; 22 | } 23 | 24 | .heroes a { 25 | color: #888; 26 | text-decoration: none; 27 | position: relative; 28 | display: block; 29 | width: 250px; 30 | } 31 | 32 | .heroes a:hover { 33 | color: #607d8b; 34 | } 35 | 36 | .heroes .badge { 37 | display: inline-block; 38 | font-size: small; 39 | color: white; 40 | padding: 0.8em 0.7em 0 0.7em; 41 | background-color: #607d8b; 42 | line-height: 1em; 43 | position: relative; 44 | left: -1px; 45 | top: -4px; 46 | height: 1.8em; 47 | min-width: 16px; 48 | text-align: right; 49 | margin-right: 0.8em; 50 | border-radius: 4px 0 0 4px; 51 | } 52 | 53 | button { 54 | background-color: #eee; 55 | border: none; 56 | padding: 5px 10px; 57 | border-radius: 4px; 58 | cursor: pointer; 59 | cursor: hand; 60 | font-family: Arial; 61 | } 62 | 63 | button:hover { 64 | background-color: #cfd8dc; 65 | } 66 | 67 | button.delete { 68 | position: relative; 69 | left: 194px; 70 | top: -32px; 71 | background-color: gray !important; 72 | color: white; 73 | } 74 | 75 | /* 76 | Copyright 2017-2018 Google Inc. All Rights Reserved. 77 | Use of this source code is governed by an MIT-style license that 78 | can be found in the LICENSE file at http://angular.io/license 79 | */ 80 | -------------------------------------------------------------------------------- /src/components/async-pipe.tsx: -------------------------------------------------------------------------------- 1 | import { Component, PureComponent, ReactNode } from 'react' 2 | 3 | type Props = { 4 | value: Promise 5 | children(api: ReturnType['getApi']>): ReactNode 6 | } 7 | type State = Readonly<{ 8 | resolvedValue: T 9 | isLoading: boolean 10 | }> 11 | export function asyncPipe(value: Promise, componentInstance: Component) { 12 | return value.then((resolved) => { 13 | componentInstance.forceUpdate(() => { 14 | console.log('force update') 15 | }) 16 | 17 | return resolved 18 | }) 19 | } 20 | 21 | export class AsyncPipe extends PureComponent, State> { 22 | readonly state = { 23 | isLoading: true, 24 | resolvedValue: (null as any) as T, 25 | } 26 | private getApi() { 27 | const { isLoading, resolvedValue: resolved } = this.state 28 | 29 | return { 30 | resolved, 31 | isLoading, 32 | } 33 | } 34 | render() { 35 | // console.log('render with', this.getApi()) 36 | const { children } = this.props 37 | 38 | return children(this.getApi()) 39 | } 40 | componentDidMount() { 41 | this.resolvePromise() 42 | } 43 | // componentDidUpdate(prevProps: AsyncPipeProps) { 44 | // const promiseChanged = prevProps.value !== this.props.value 45 | // console.log('componentDidUpdate changed?', promiseChanged) 46 | // if (promiseChanged) { 47 | // this.setState(() => ({ isLoading: true }), () => this.resolvePromise()) 48 | // } 49 | // } 50 | private resolvePromise() { 51 | this.props.value 52 | .then((resolved) => { 53 | this.setState((prevState) => ({ 54 | isLoading: false, 55 | resolvedValue: resolved, 56 | })) 57 | }) 58 | .catch(console.error) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thanks for your interest in contributing to the rea-di! 🎉 4 | 5 | PRs are the preferred way to spike ideas and address issues, if you have time. If you plan on contributing frequently, please feel free to ask to become a maintainer; the more the merrier. 🤙 6 | 7 | ## Technical overview 8 | 9 | This library uses following libraries for development: 10 | 11 | - [typescript](http://www.typescriptlang.org/) for typed JavaScript and transpilation 12 | - [jest](https://jestjs.io/) for unit testing 13 | - run `yarn test:watch` during development 14 | - [webpack](https://webpack.js.org/) for creating UMD bundles 15 | - [yarn](https://yarnpkg.com/lang/en/) for package management 16 | - [npm scripts](https://docs.npmjs.com/misc/scripts) for executing tasks 17 | 18 | ## Getting started 19 | 20 | ### Creating a Pull Request 21 | 22 | If you've never submitted a Pull request before please visit http://makeapullrequest.com/ to learn everything you need to know. 23 | 24 | #### Setup 25 | 26 | 1. Fork the repo. 27 | 1. `git clone` your fork. 28 | 1. Make a `git checkout -b branch-name` branch for your change. 29 | 1. Run `yarn install` (make sure you have node and npm installed first) 30 | Updates 31 | 32 | 1. make sure to add unit tests 33 | 1. If there is a `*.spec.ts` file, update it to include a test for your change, if needed. If this file doesn't exist, please create it. 34 | 1. Run `yarn test` or `yarn test:watch` to make sure all tests are working, regardless if a test was added. 35 | 36 | ### Commit Message Format 37 | 38 | We use https://conventionalcommits.org/ message format. you can use `yarn commit` to invoke a CLI tool which will guide you through the process. 39 | 40 | ## License 41 | 42 | By contributing your code to the {library-name} GitHub Repository, you agree to license your contribution under the MIT license. 43 | -------------------------------------------------------------------------------- /src/__tests__/components/__snapshots__/provide-inject.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Provide/Inject should properly inject 1`] = ` 4 | 5 |
6 | 14 | 17 |
18 |

19 | count module 20 |

21 | 29 | 41 |
44 |

45 | Counter 46 |

47 |

48 | Count: 49 | 0 50 |

51 | 56 | 61 |
64 | Hello projection 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | `; 75 | -------------------------------------------------------------------------------- /examples/github-user/src/app/components/search-user.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | import { withInjectables } from '@martin_hotell/rea-di' 3 | import { Component, createElement, createRef } from 'react' 4 | 5 | import { GithubUserService } from '../user.service' 6 | 7 | type Props = { 8 | userService: GithubUserService 9 | } 10 | export class SearchUser extends Component { 11 | private usernameRef = createRef() 12 | private submitBtnRef = createRef() 13 | 14 | render() { 15 | return ( 16 |
17 |
this.handleSubmit(ev)} noValidate> 18 |
19 | 25 |
26 |
27 | 34 |
35 |
36 |
37 | ) 38 | } 39 | private handleSubmit(ev: import('react').FormEvent) { 40 | ev.preventDefault() 41 | const username = this.usernameRef.current! 42 | const btn = this.submitBtnRef.current! 43 | 44 | btn.disabled = true 45 | username.disabled = true 46 | 47 | // first we set just username 48 | this.props.userService.setActiveUser({ username: username.value }) 49 | 50 | // with that we can initiate HTTP Get 51 | this.props.userService 52 | .getGithubInfo(username.value) 53 | .then(({ bio, repos }) => { 54 | this.props.userService.setActiveUser({ bio, repos }) 55 | 56 | btn.disabled = false 57 | username.disabled = false 58 | username.value = '' 59 | }) 60 | } 61 | } 62 | 63 | export default withInjectables({ userService: GithubUserService })(SearchUser) 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | 7 | # [1.0.0](https://github.com/Hotell/rea-di/compare/v0.2.0...v1.0.0) (2018-12-06) 8 | 9 | ### Features 10 | 11 | - **api:** use tuples for injection on both Provider and Inject ([#9](https://github.com/Hotell/rea-di/issues/9)) ([6364f80](https://github.com/Hotell/rea-di/commit/6364f80)) 12 | - **inject:** implement optional ([#10](https://github.com/Hotell/rea-di/issues/10)) ([7060c65](https://github.com/Hotell/rea-di/commit/7060c65)), closes [#8](https://github.com/Hotell/rea-di/issues/8) 13 | 14 | ### BREAKING CHANGES 15 | 16 | - **api:** - Previously providers registration used dictionary as well as Inject. Now both 17 | components (DependencyProvider, Inject) use arrays to both register providers as well as inject instances via token. 18 | 19 | * New minimal required TS version is 3.1 20 | * renamed: 21 | - Provide -> DependencyProvider 22 | - withProvider -> withDependencyProvider 23 | * Provider used previously `provider` prop -> `providers` 24 | * Inject used previously `providers` prop -> `values` 25 | * withDependencyProviders accepts n-ary arguments 26 | 27 | 28 | 29 | # [0.2.0](https://www.github.com/Hotell/rea-di/compare/v0.1.0...v0.2.0) (2018-07-27) 30 | 31 | ### Bug Fixes 32 | 33 | - **build:** don't bundle peer deps wihin bundle ([790885e](https://www.github.com/Hotell/rea-di/commit/790885e)) 34 | 35 | ### Features 36 | 37 | - **hoc:** properly expose WrappedComponent type (#7) ([883074c](https://www.github.com/Hotell/rea-di/commit/883074c)) 38 | 39 | 40 | 41 | # [0.1.0](https://www.github.com/Hotell/rea-di/compare/v0.0.1...v0.1.0) (2018-07-23) 42 | 43 | ### Features 44 | 45 | - **hoc:** implement HoC alternatives for Provide and Inject ([54115b6](https://www.github.com/Hotell/rea-di/commit/54115b6)) 46 | 47 | 48 | 49 | ## 0.0.1 (2018-07-16) 50 | 51 | ### Features 52 | 53 | - add initial implementation with react wrappers ([1c784fc](https://www.github.com/Hotell/read-di/commit/1c784fc)) 54 | -------------------------------------------------------------------------------- /config/global.d.ts: -------------------------------------------------------------------------------- 1 | // ============================ 2 | // ts-jest types require 'babel__core' 3 | // ============================ 4 | declare module 'babel__core' { 5 | interface TransformOptions {} 6 | } 7 | 8 | // ============================ 9 | // Rollup plugins without types 10 | // ============================ 11 | type RollupPluginImpl = import('rollup').PluginImpl< 12 | O 13 | > 14 | 15 | declare module 'rollup-plugin-json' { 16 | export interface Options { 17 | /** 18 | * All JSON files will be parsed by default, but you can also specifically include/exclude files 19 | */ 20 | include?: string | string[] 21 | exclude?: string | string[] 22 | /** 23 | * for tree-shaking, properties will be declared as variables, using either `var` or `const` 24 | * @default false 25 | */ 26 | preferConst?: boolean 27 | /** 28 | * specify indentation for the generated default export — defaults to '\t' 29 | * @default '\t' 30 | */ 31 | indent?: string 32 | } 33 | const plugin: RollupPluginImpl 34 | export default plugin 35 | } 36 | declare module 'rollup-plugin-sourcemaps' { 37 | const plugin: RollupPluginImpl 38 | export default plugin 39 | } 40 | declare module 'rollup-plugin-node-resolve' { 41 | const plugin: RollupPluginImpl 42 | export default plugin 43 | } 44 | declare module 'rollup-plugin-commonjs' { 45 | const plugin: RollupPluginImpl 46 | export default plugin 47 | } 48 | declare module 'rollup-plugin-replace' { 49 | const plugin: RollupPluginImpl 50 | export default plugin 51 | } 52 | declare module 'rollup-plugin-uglify' { 53 | const uglify: RollupPluginImpl 54 | export { uglify } 55 | } 56 | declare module 'rollup-plugin-terser' { 57 | const terser: RollupPluginImpl 58 | export { terser } 59 | } 60 | 61 | // ===================== 62 | // missing library types 63 | // ===================== 64 | declare module '@commitlint/core' { 65 | interface Config { 66 | extends: string[] 67 | } 68 | } 69 | declare module 'sort-object-keys' { 70 | const sortPackageJson: ( 71 | object: T, 72 | sortWith?: (...args: any[]) => any 73 | ) => T 74 | export = sortPackageJson 75 | } 76 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/http-client.service.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | AxiosInterceptorManager, 4 | AxiosPromise, 5 | AxiosRequestConfig, 6 | AxiosResponse, 7 | } from 'axios' 8 | import { Inject, Injectable, InjectionToken, Optional } from 'injection-js' 9 | 10 | type AxiosClient = Pick 11 | 12 | export type HttpClientConfig = Readonly 13 | export const HttpClientConfig = new InjectionToken('HttpClientConfig') 14 | 15 | const defaultConfig = {} as AxiosRequestConfig 16 | 17 | @Injectable() 18 | export class HttpClient implements AxiosClient { 19 | defaults: AxiosRequestConfig 20 | interceptors: { 21 | request: AxiosInterceptorManager 22 | response: AxiosInterceptorManager> 23 | } 24 | 25 | private _provider: AxiosInstance 26 | constructor( 27 | @Optional() 28 | @Inject(HttpClientConfig) 29 | config: HttpClientConfig 30 | ) { 31 | this._provider = axios.create({ ...config, ...defaultConfig }) 32 | this.interceptors = this._provider.interceptors 33 | this.defaults = this._provider.defaults 34 | } 35 | request(config: AxiosRequestConfig): AxiosPromise { 36 | return this._provider.request(config) 37 | } 38 | get(url: string, config?: AxiosRequestConfig): AxiosPromise { 39 | return this._provider.get(url, config) 40 | } 41 | delete(url: string, config?: AxiosRequestConfig): AxiosPromise { 42 | return this._provider.delete(url, config) 43 | } 44 | head(url: string, config?: AxiosRequestConfig): AxiosPromise { 45 | return this._provider.head(url, config) 46 | } 47 | post( 48 | url: string, 49 | data?: any, 50 | config?: AxiosRequestConfig 51 | ): AxiosPromise { 52 | return this._provider.post(url, data, config) 53 | } 54 | put( 55 | url: string, 56 | data?: any, 57 | config?: AxiosRequestConfig 58 | ): AxiosPromise { 59 | return this._provider.put(url, data, config) 60 | } 61 | patch( 62 | url: string, 63 | data?: any, 64 | config?: AxiosRequestConfig 65 | ): AxiosPromise { 66 | return this._provider.patch(url, data, config) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/hero-search.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | 3 | import React, { Component, createRef } from 'react' 4 | import { Link } from 'react-router-dom' 5 | import { Subject } from 'rxjs' 6 | import { 7 | debounceTime, 8 | distinctUntilChanged, 9 | switchMap, 10 | takeUntil, 11 | } from 'rxjs/operators' 12 | 13 | import { Hero } from '../hero' 14 | import { HeroService } from '../hero.service' 15 | 16 | import './hero-search.css' 17 | 18 | type Props = { 19 | heroService: HeroService 20 | } 21 | type State = Readonly 22 | 23 | const initialState = { 24 | heroes: [] as Hero[], 25 | } 26 | 27 | const DEBOUNCE_TIME = 300 28 | export class HeroSearch extends Component { 29 | readonly state: State = initialState 30 | 31 | private readonly searchResultRef = createRef() 32 | private readonly searchTerms = new Subject() 33 | private readonly onUnmount$ = new Subject() 34 | private readonly heroes$ = this.searchTerms.pipe( 35 | takeUntil(this.onUnmount$), 36 | // wait 300ms after each keystroke before considering the term 37 | debounceTime(DEBOUNCE_TIME), 38 | 39 | // ignore new term if same as previous term 40 | distinctUntilChanged(), 41 | 42 | // switch to new search observable each time the term changes 43 | switchMap((term: string) => this.props.heroService.searchHeroes(term)) 44 | ) 45 | 46 | render() { 47 | const { heroes } = this.state 48 | 49 | return ( 50 |
51 |

Hero Search

52 | 53 | this.search(this.searchResultRef.current!.value)} 59 | /> 60 | 61 |
    62 | {heroes.map((hero) => ( 63 |
  • 64 | {hero.name} 65 |
  • 66 | ))} 67 |
68 |
69 | ) 70 | } 71 | 72 | componentDidMount() { 73 | this.heroes$.subscribe((heroes) => { 74 | this.setState({ heroes }) 75 | }) 76 | } 77 | componentWillUnmount() { 78 | this.onUnmount$.complete() 79 | } 80 | 81 | // Push a search term into the observable stream. 82 | private search(term: string) { 83 | this.searchTerms.next(term) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/counter-with-multiple-injectors/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { DependencyProvider } from '@martin_hotell/rea-di' 2 | import { Component, createElement, Fragment } from 'react' 3 | 4 | import { Counter } from './counter' 5 | import { CounterService } from './counter.service' 6 | import { EnhancedLogger } from './enhanced-logger.service' 7 | import { Logger } from './logger.service' 8 | import { 9 | MultiplyCounterConfig, 10 | MultiplyCounterService, 11 | } from './multiply-counter.service' 12 | 13 | export class App extends Component { 14 | render() { 15 | return ( 16 |
17 |

Counter app

18 |

19 | Open you browser devtools console... and start clicking on buttons ;) 20 |

21 | 22 | 23 |

Counter component with CounterService instance

24 | 25 | 26 |
27 | 28 | 34 | 35 |
36 |

37 | Counter component with MultiplyCounterService instance 38 |

39 |

40 | multiply by 2 and uses enhanced logger 41 |

42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 | 55 | 56 |
57 |

58 | Counter component with MultiplyCounterService instance 59 |

60 |

multiply by 4

61 |
62 | 63 |
64 |
65 |
66 |
67 |
68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-react", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | // tslint-react rules 9 | "jsx-no-lambda": true, 10 | "jsx-no-string-ref": true, 11 | "jsx-self-close": true, 12 | "jsx-boolean-value": [ 13 | true, 14 | "never" 15 | ], 16 | // core ts-lint rules 17 | "await-promise": true, 18 | "no-unused-variable": true, 19 | "forin": true, 20 | "no-bitwise": true, 21 | "no-console": [ 22 | true, 23 | "debug", 24 | "info", 25 | "time", 26 | "timeEnd", 27 | "trace" 28 | ], 29 | "no-construct": true, 30 | "no-debugger": true, 31 | "no-shadowed-variable": true, 32 | "no-string-literal": true, 33 | "no-inferrable-types": [ 34 | true 35 | ], 36 | "no-unnecessary-initializer": true, 37 | "no-magic-numbers": true, 38 | "no-require-imports": true, 39 | "no-duplicate-super": true, 40 | "no-boolean-literal-compare": true, 41 | "no-namespace": [ 42 | true, 43 | "allow-declarations" 44 | ], 45 | "no-invalid-this": [ 46 | true, 47 | "check-function-in-method" 48 | ], 49 | "ordered-imports": [ 50 | true 51 | ], 52 | "interface-name": [ 53 | false 54 | ], 55 | "newline-before-return": true, 56 | "object-literal-shorthand": true, 57 | "arrow-return-shorthand": [ 58 | true 59 | ], 60 | "unified-signatures": true, 61 | "prefer-for-of": true, 62 | "match-default-export-name": true, 63 | "prefer-const": true, 64 | "ban-types": [ 65 | true, 66 | [ 67 | "Object", 68 | "Avoid using the `Object` type. Did you mean `object`?" 69 | ], 70 | [ 71 | "Function", 72 | "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." 73 | ], 74 | [ 75 | "Boolean", 76 | "Avoid using the `Boolean` type. Did you mean `boolean`?" 77 | ], 78 | [ 79 | "Number", 80 | "Avoid using the `Number` type. Did you mean `number`?" 81 | ], 82 | [ 83 | "String", 84 | "Avoid using the `String` type. Did you mean `string`?" 85 | ], 86 | [ 87 | "Symbol", 88 | "Avoid using the `Symbol` type. Did you mean `symbol`?" 89 | ], 90 | [ 91 | "Array", 92 | "Avoid using the `Array` type. Use 'type[]' instead." 93 | ] 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/__tests__/utils/guards.spec.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from 'injection-js' 2 | import { 3 | isFunction, 4 | isJsLikeObject, 5 | isObject, 6 | isProvider, 7 | isType, 8 | } from '../../utils/guards' 9 | 10 | // tslint:disable:no-magic-numbers 11 | 12 | jest.mock('../../environment.ts', () => ({ 13 | IS_DEV: true, 14 | IS_PROD: false, 15 | })) 16 | 17 | // tslint:disable-next-line:no-empty 18 | const noop = () => {} 19 | const emptyArr = [] as any[] 20 | const emptyObj = {} 21 | 22 | describe(`guards`, () => { 23 | describe(`isJsLikeObject`, () => { 24 | it(`should return true if value is JS like object`, () => { 25 | expect(isJsLikeObject(123)).toBe(false) 26 | expect(isJsLikeObject('hello')).toBe(false) 27 | expect(isJsLikeObject(undefined)).toBe(false) 28 | expect(isJsLikeObject(true)).toBe(false) 29 | expect(isJsLikeObject(noop)).toBe(false) 30 | expect(isJsLikeObject(null)).toBe(false) 31 | 32 | expect(isJsLikeObject(emptyArr)).toBe(true) 33 | expect(isJsLikeObject(emptyObj)).toBe(true) 34 | }) 35 | }) 36 | 37 | describe(`isObject`, () => { 38 | it(`should return false if value is not an object map`, () => { 39 | expect(isObject(123)).toBe(false) 40 | expect(isObject('hello')).toBe(false) 41 | expect(isObject(null)).toBe(false) 42 | expect(isObject(undefined)).toBe(false) 43 | expect(isObject(true)).toBe(false) 44 | expect(isObject(noop)).toBe(false) 45 | expect(isObject(emptyArr)).toBe(false) 46 | 47 | expect(isObject(emptyObj)).toBe(true) 48 | expect(isObject({ one: 1 })).toBe(true) 49 | }) 50 | }) 51 | 52 | describe(`isFunction`, () => { 53 | it(`should return true if value is function`, () => { 54 | expect(isFunction(emptyArr)).toBe(false) 55 | expect(isFunction(emptyObj)).toBe(false) 56 | 57 | expect(isFunction(noop)).toBe(true) 58 | }) 59 | }) 60 | 61 | describe(`isType`, () => { 62 | it(`should check if value is a class`, () => { 63 | expect(isType(class Test {})).toBe(true) 64 | }) 65 | }) 66 | 67 | describe(`isProvider`, () => { 68 | it(`should check if value is a provider config`, () => { 69 | class Service {} 70 | const providerConfig: Provider = { 71 | provide: Service, 72 | useClass: Service, 73 | } 74 | 75 | expect(isProvider(providerConfig)).toBe(true) 76 | 77 | expect( 78 | isProvider({ 79 | useClass: Service, 80 | }) 81 | ).toBe(false) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/hero-detail.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | import React, { Component, SyntheticEvent } from 'react' 3 | import { RouteComponentProps } from 'react-router-dom' 4 | 5 | import './hero-detail.css' 6 | 7 | import { Hero } from '../hero' 8 | import { HeroService } from '../hero.service' 9 | import { uppercase } from '../shared' 10 | 11 | type Props = { 12 | hero?: Hero | null 13 | heroService: HeroService 14 | } & RouteComponentProps<{ 15 | id: string 16 | }> 17 | type State = Readonly> 18 | type FormFieldKeys = 'name' 19 | 20 | const getInitialState = ({ hero = null }: Props) => ({ 21 | hero, 22 | }) 23 | 24 | export class HeroDetail extends Component { 25 | readonly state = getInitialState(this.props) 26 | 27 | private handleChange = (event: SyntheticEvent) => { 28 | const { value } = event.currentTarget 29 | const name = event.currentTarget.name as FormFieldKeys 30 | 31 | this.setState((prevState) => ({ 32 | hero: { ...prevState.hero!, [name]: value }, 33 | })) 34 | } 35 | render() { 36 | const { hero } = this.state 37 | 38 | return ( 39 |
40 | {hero ? ( 41 |
42 |

{uppercase(hero.name)} Details

43 |
44 | id: 45 | {hero.id} 46 |
47 |
48 | 57 |
58 | 59 | 60 |
61 | ) : ( 62 | 'getting Hero detail...' 63 | )} 64 |
65 | ) 66 | } 67 | componentDidMount() { 68 | this.getHero() 69 | } 70 | 71 | private save(hero: Hero) { 72 | this.props.heroService.updateHero(hero).then(() => this.goBack()) 73 | } 74 | private getHero(): void { 75 | if (this.state.hero) { 76 | return 77 | } 78 | 79 | const id = Number(this.props.match.params.id) 80 | this.props.heroService.getHero(id).then((hero) => this.setState({ hero })) 81 | } 82 | private goBack() { 83 | this.props.history.goBack() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/components/heroes.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | import React, { Component, createRef } from 'react' 3 | import { Link } from 'react-router-dom' 4 | 5 | import { Hero } from '../hero' 6 | import { uppercase } from '../shared' 7 | 8 | import { HeroService } from '../hero.service' 9 | import { HeroDetail } from './hero-detail' 10 | import './heroes.css' 11 | 12 | type Props = { 13 | heroService: HeroService 14 | } 15 | type State = Readonly 16 | const initialState = { 17 | heroes: [] as Hero[], 18 | } 19 | 20 | export class Heroes extends Component { 21 | readonly state = initialState 22 | 23 | private heroNameRef = createRef() 24 | 25 | render() { 26 | const { heroes } = this.state 27 | 28 | return ( 29 | <> 30 |

My Heroes

31 | 32 |
33 | 37 | {/* (click) passes input value to add() and then clears the input */} 38 | 41 |
42 | 43 |
    44 | {heroes.map((hero) => ( 45 |
  • 46 | 47 | {hero.id} {hero.name} 48 | 49 | 56 |
  • 57 | ))} 58 |
59 | 60 | ) 61 | } 62 | componentDidMount() { 63 | this.props.heroService.getHeroes().then((heroes) => { 64 | this.setState({ heroes }) 65 | }) 66 | } 67 | 68 | private delete(hero: Hero) { 69 | const heroesWithoutRemoved = this.state.heroes.filter((h) => h !== hero) 70 | 71 | this.props.heroService.deleteHero(hero).then(() => { 72 | this.setState((prevState) => ({ 73 | heroes: [...heroesWithoutRemoved], 74 | })) 75 | }) 76 | } 77 | 78 | private add(name: string) { 79 | name = name.trim() 80 | if (!name) { 81 | return 82 | } 83 | 84 | const newHero = { name } as Hero 85 | this.props.heroService.addHero(newHero).then((hero) => { 86 | this.setState( 87 | (prevState) => ({ 88 | heroes: [...prevState.heroes, hero], 89 | }), 90 | () => (this.heroNameRef.current!.value = '') 91 | ) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:jsx-no-lambda 2 | 3 | import { DependencyProvider, Inject } from '@martin_hotell/rea-di' 4 | import React, { Component } from 'react' 5 | import { 6 | BrowserRouter as Router, 7 | NavLink, 8 | Redirect, 9 | Route, 10 | Switch, 11 | } from 'react-router-dom' 12 | 13 | import { Dashboard, HeroDetail, Heroes } from './components' 14 | import { HeroService } from './hero.service' 15 | import { HttpClient, HttpClientConfig } from './http-client.service' 16 | import { Messages, MessageService } from './messages' 17 | 18 | export class App extends Component { 19 | title = 'Tour of Heroes' 20 | render() { 21 | return ( 22 |
23 | 38 | 39 | <> 40 | 44 | 45 | ( 48 | 49 | {(heroService) => } 50 | 51 | )} 52 | /> 53 | ( 56 | 57 | {(heroService) => } 58 | 59 | )} 60 | /> 61 | ( 64 | 65 | {(heroService) => ( 66 | 67 | )} 68 | 69 | )} 70 | /> 71 | } /> 72 | 73 | 74 | 75 | 76 | 77 |
78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/debug.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Provider as ProviderConfig, 3 | ReflectiveInjector, 4 | Type, 5 | } from 'injection-js' 6 | import React, { ReactElement, ReactNode } from 'react' 7 | 8 | import { rootInjector } from '../services/injector-context' 9 | import { isProvider, isType } from '../utils/guards' 10 | 11 | export const Debug = (props: { 12 | enable: boolean 13 | parentInjector: ReflectiveInjector 14 | children: ReactNode 15 | registeredProviders: ProviderConfig[] 16 | label?: string 17 | }) => { 18 | const { children, enable, label, registeredProviders, parentInjector } = props 19 | 20 | if (!enable) { 21 | return children as ReactElement 22 | } 23 | 24 | const isRoot = parentInjector === rootInjector 25 | const injectorLabel = label || isRoot ? 'Root Injector' : 'Child Injector' 26 | const bgColor = isRoot ? 'red' : '#388e3c' 27 | const styling = { 28 | container: { border: `2px solid ${bgColor}`, padding: '.5rem' }, 29 | header: { backgroundColor: bgColor, padding: `.5rem .25rem` }, 30 | title: { margin: 0, backgroundColor: bgColor }, 31 | } 32 | 33 | const registeredProvidersNames: string[] = getRegisteredProvidersNames( 34 | registeredProviders 35 | ) 36 | 37 | return ( 38 |
39 |
40 |

{injectorLabel}

41 |
42 |           Registered Providers: {json(registeredProvidersNames)}
43 |         
44 |
45 | {children} 46 |
47 | ) 48 | 49 | function json(value: T) { 50 | // tslint:disable-next-line:no-magic-numbers 51 | return JSON.stringify(value, null, 2) 52 | } 53 | 54 | function getRegisteredProvidersNames(providers: ProviderConfig[]): string[] { 55 | return providers.reduce((acc, next) => { 56 | if (isType(next)) { 57 | return [...(acc as string[]), next.name] 58 | } 59 | 60 | if (isProvider(next)) { 61 | const [registrationKey] = Object.keys(next).filter( 62 | (val) => val !== 'provide' 63 | ) 64 | 65 | const registrationValue = (next as { 66 | [key: string]: any 67 | })[registrationKey] as Type | object 68 | 69 | const providerName = { 70 | provide: next.provide.name ? next.provide.name : next.provide._desc, 71 | as: isType(registrationValue) 72 | ? registrationValue.name 73 | : JSON.stringify(registrationValue), 74 | } 75 | 76 | return [ 77 | ...(acc as string[]), 78 | `{provide: ${providerName.provide}, ${registrationKey}: ${ 79 | providerName.as 80 | } }`, 81 | ] 82 | } 83 | 84 | return acc 85 | }, []) as string[] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/inject.hoc.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ComponentType } from 'react' 2 | 3 | import { Type } from 'injection-js' 4 | import { HoC, NullableInstanceTypes, NullableTypeMap, Subtract } from '../types' 5 | import { createHOCName } from '../utils/helpers' 6 | import { Inject } from './inject' 7 | 8 | /** 9 | * If you need to access injected service instances outside of render, you can use this high order component. 10 | * It will give you also a little performance boost because providers map is gonna be created only once, during definition. 11 | * This can be mitigated by extracting providers to instance property when standard is used. 12 | * 13 | * @param tokenMap - provider instances from injector to be mapped to wrapped component props 14 | */ 15 | export const withInjectables = ( 16 | tokenMap: TokenMap 17 | ) => < 18 | OriginalProps extends NullableInstanceTypes, 19 | ResolvedProps = Subtract 20 | >( 21 | Cmp: ComponentType 22 | ): HoC => { 23 | const injectablePropsKeys: Array = Object.keys(tokenMap) 24 | const tokenRefs = injectablePropsKeys.map((propKey) => tokenMap[propKey]) 25 | 26 | class WithInjectables extends Component { 27 | static displayName: string = createHOCName(WithInjectables, Cmp) 28 | static readonly WrappedComponent = Cmp 29 | 30 | /** 31 | * 32 | * @param providersMap - map config (key to Class type) 33 | * @param propsKeys - array of mapped keys 34 | * @param injectables - injectables are nullable tuples 35 | */ 36 | private injectTokensMap( 37 | providersMap: TokenMap, 38 | propsKeys: Array, 39 | injectables: NullableInstanceTypes>> 40 | ) { 41 | const injectProps = propsKeys.reduce((acc, nextPropKey) => { 42 | // token is always present. We just trick TS to always be an Nullable type to get proper inference within children 43 | const token = providersMap[nextPropKey] as Type 44 | const injectable = 45 | injectables.find( 46 | (injectableInstance) => injectableInstance instanceof token 47 | ) || null 48 | 49 | return { ...acc, [nextPropKey]: injectable } 50 | }, {}) as NullableInstanceTypes 51 | 52 | return injectProps 53 | } 54 | 55 | render() { 56 | const { ...rest } = this.props as object /* FIXED in ts 3.2 */ 57 | 58 | return ( 59 | 60 | {(...injectables) => ( 61 | 69 | )} 70 | 71 | ) 72 | } 73 | } 74 | 75 | return WithInjectables 76 | } 77 | -------------------------------------------------------------------------------- /src/__tests__/components/__snapshots__/provide-inject.hoc.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hoc wrappers should should return wrapped component with proper prop annotation 1`] = ` 4 | 7 | `; 8 | 9 | exports[`Hoc wrappers should should return wrapped component with proper prop annotation 2`] = ` 10 | 21 | `; 22 | 23 | exports[`Hoc wrappers should work with HoC 1`] = ` 24 | 25 |
26 | 29 | 37 | 40 |
41 |

42 | count module 43 |

44 | 45 | 53 | 65 |
68 |

69 | Counter 70 |

71 |

72 | Count: 73 | 0 74 |

75 | 80 | 85 |
88 | Hello projection 89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | `; 101 | -------------------------------------------------------------------------------- /src/__tests__/setup/components.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ErrorInfo, ReactChild, ReactNode } from 'react' 2 | import { CounterService, Logger } from './services' 3 | 4 | export class Counter extends Component<{ 5 | counterService: CounterService 6 | logger: Logger 7 | children?: ReactNode 8 | }> { 9 | render() { 10 | // tslint:disable:jsx-no-lambda 11 | const { counterService, children } = this.props 12 | 13 | return ( 14 |
15 |

Counter

16 |

Count: {counterService.state.count}

17 | 18 | 19 |
{children}
20 |
21 | ) 22 | } 23 | componentDidMount() { 24 | const { logger } = this.props 25 | 26 | logger.log('Counter Logged') 27 | } 28 | } 29 | 30 | export class CounterForHoc extends Component<{ 31 | injectables: [Logger, CounterService] 32 | children?: ReactNode 33 | }> { 34 | render() { 35 | // tslint:disable:jsx-no-lambda 36 | const { 37 | injectables: [, counterService], 38 | children, 39 | } = this.props 40 | 41 | return ( 42 |
43 |

Counter

44 |

Count: {counterService.state.count}

45 | 46 | 47 |
{children}
48 |
49 | ) 50 | } 51 | componentDidMount() { 52 | const { 53 | injectables: [logger], 54 | } = this.props 55 | 56 | logger.log('Counter Logged') 57 | } 58 | } 59 | 60 | type ErrorBoundaryState = typeof errorBoundaryInitialState 61 | const errorBoundaryInitialState = { 62 | hasError: false, 63 | error: null as null | Error, 64 | errorInfo: null as null | ErrorInfo, 65 | } 66 | 67 | export class ErrorBoundary extends Component< 68 | { children: ReactChild }, 69 | ErrorBoundaryState 70 | > { 71 | readonly state = errorBoundaryInitialState 72 | 73 | // static getDerivedStateFromError( 74 | // error: any 75 | // ): Partial | null { 76 | // // Update state so the next render will show the fallback UI. 77 | // return { hasError: true } 78 | // } 79 | 80 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 81 | // You can also log the error to an error reporting service 82 | // console.error('Error catched!:', error, info) 83 | // Catch errors in any components below and re-render with error message 84 | this.setState({ 85 | error, 86 | errorInfo, 87 | }) 88 | } 89 | 90 | render() { 91 | if (this.state.errorInfo) { 92 | // Error path 93 | return ( 94 |
95 |

Something went wrong.

96 |
97 | {this.state.error && this.state.error.toString()} 98 |
99 | {this.state.errorInfo.componentStack} 100 |
101 |
102 | ) 103 | } 104 | 105 | // Normally, just render children 106 | return this.props.children 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import sourceMaps from 'rollup-plugin-sourcemaps' 3 | import nodeResolve from 'rollup-plugin-node-resolve' 4 | import json from 'rollup-plugin-json' 5 | import commonjs from 'rollup-plugin-commonjs' 6 | import replace from 'rollup-plugin-replace' 7 | import { uglify } from 'rollup-plugin-uglify' 8 | import { terser } from 'rollup-plugin-terser' 9 | import { getIfUtils, removeEmpty } from 'webpack-config-utils' 10 | 11 | import pkg from '../package.json' 12 | const { 13 | pascalCase, 14 | normalizePackageName, 15 | getOutputFileName, 16 | } = require('./helpers') 17 | 18 | /** 19 | * @typedef {import('./types').RollupConfig} Config 20 | */ 21 | /** 22 | * @typedef {import('./types').RollupPlugin} Plugin 23 | */ 24 | 25 | const env = process.env.NODE_ENV || 'development' 26 | const { ifProduction } = getIfUtils(env) 27 | 28 | const LIB_NAME = pascalCase(normalizePackageName(pkg.name)) 29 | const ROOT = resolve(__dirname, '..') 30 | const DIST = resolve(ROOT, 'dist') 31 | 32 | /** 33 | * Object literals are open-ended for js checking, so we need to be explicit 34 | * @type {{entry:{esm5: string, esm2015: string},bundles:string}} 35 | */ 36 | const PATHS = { 37 | entry: { 38 | esm5: resolve(DIST, 'esm5'), 39 | esm2015: resolve(DIST, 'esm2015'), 40 | }, 41 | bundles: resolve(DIST, 'bundles'), 42 | } 43 | 44 | /** 45 | * @type {string[]} 46 | */ 47 | const external = Object.keys(pkg.peerDependencies) || [] 48 | 49 | /** 50 | * @type {Plugin[]} 51 | */ 52 | const plugins = /** @type {Plugin[]} */ ([ 53 | // Allow json resolution 54 | json(), 55 | 56 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 57 | commonjs(), 58 | 59 | // Allow node_modules resolution, so you can use 'external' to control 60 | // which external modules to include in the bundle 61 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 62 | nodeResolve(), 63 | 64 | // Resolve source maps to the original source 65 | sourceMaps(), 66 | 67 | // properly set process.env.NODE_ENV within `./environment.ts` 68 | replace({ 69 | exclude: 'node_modules/**', 70 | 'process.env.NODE_ENV': JSON.stringify(env), 71 | }), 72 | ]) 73 | 74 | /** 75 | * @type {Config} 76 | */ 77 | const CommonConfig = { 78 | input: {}, 79 | output: {}, 80 | inlineDynamicImports: true, 81 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 82 | external, 83 | } 84 | 85 | /** 86 | * @type {Config} 87 | */ 88 | const UMDconfig = { 89 | ...CommonConfig, 90 | input: resolve(PATHS.entry.esm5, 'index.js'), 91 | output: { 92 | file: getOutputFileName( 93 | resolve(PATHS.bundles, 'index.umd.js'), 94 | ifProduction() 95 | ), 96 | format: 'umd', 97 | name: LIB_NAME, 98 | sourcemap: true, 99 | }, 100 | plugins: removeEmpty( 101 | /** @type {Plugin[]} */ ([...plugins, ifProduction(uglify())]) 102 | ), 103 | } 104 | 105 | /** 106 | * @type {Config} 107 | */ 108 | const FESMconfig = { 109 | ...CommonConfig, 110 | input: resolve(PATHS.entry.esm2015, 'index.js'), 111 | output: [ 112 | { 113 | file: getOutputFileName( 114 | resolve(PATHS.bundles, 'index.esm.js'), 115 | ifProduction() 116 | ), 117 | format: 'es', 118 | sourcemap: true, 119 | }, 120 | ], 121 | plugins: removeEmpty( 122 | /** @type {Plugin[]} */ ([...plugins, ifProduction(terser())]) 123 | ), 124 | } 125 | 126 | export default [UMDconfig, FESMconfig] 127 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | `Read-di` is distributed with a few examples in its source code. Most of these examples are also on `CodeSandbox`. 4 | 5 | All examples use `parcel-js` as this is the easiest way how to boot any demo app ;). 6 | 7 | ## Counter 8 | 9 | Run the [Counter](./counter) example: 10 | 11 | ``` 12 | git clone https://github.com/hotell/rea-di.git 13 | 14 | cd rea-di/examples/counter 15 | yarn install 16 | yarn start 17 | ``` 18 | 19 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter). 20 | 21 | This is the most basic example of using Rea-di for handling state on Service layer with React. 22 | 23 | This example includes tests. 24 | 25 | ## Counter With Logger 26 | 27 | Run the [Counter with Logger app](.) example: 28 | 29 | ``` 30 | git clone https://github.com/hotell/rea-di.git 31 | 32 | cd rea-di/examples/counter-with-logger 33 | yarn install 34 | yarn start 35 | ``` 36 | 37 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter-with-logger). 38 | 39 | This builds on previous counter example and adds `Logger` service which is injected to `CounterService`. With that we get logs into console on every action. 40 | 41 | ## Counter With multiple Injectors and hierarchies 42 | 43 | Run the [Counter with Multiple Injectors and hierarchies app](.) example: 44 | 45 | ``` 46 | git clone https://github.com/hotell/rea-di.git 47 | 48 | cd rea-di/examples/counter-with-multiple-injectors 49 | yarn install 50 | yarn start 51 | ``` 52 | 53 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter-with-multiple-injectors). 54 | 55 | This builds on previous **counter with logger** example and demonstrates multiple child injectors resolution and aliasing by using one common `Counter` component with different service instances injected by the same token resolved via tree hierarchy. Also it adds configurable `MultiplyCounterService`. 56 | 57 | Try to do this without DI framework... Good luck with that 😇 58 | 59 | ## Github User Search 60 | 61 | Run the [Github User Search](./github-user) example: 62 | 63 | ``` 64 | git clone https://github.com/hotell/rea-di.git 65 | 66 | cd rea-di/examples/github-user 67 | yarn install 68 | yarn start 69 | ``` 70 | 71 | Or check out the [sandbox](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/github-user). 72 | 73 | This demonstrates simple implementation of DI and shows usage of both `` and **Hoc** `withInjectables()` to wire up DI to React app component tree. 74 | 75 | ## Tour of Heroes 76 | 77 | Run the [Tour of Heroes](./tour-of-heroes) example: 78 | 79 | ``` 80 | git clone https://github.com/hotell/rea-di.git 81 | 82 | cd rea-di/examples/tour-of-heroes 83 | yarn install 84 | yarn start 85 | ``` 86 | 87 | Or check out the [sandbox](https://codesanbox.io/). 88 | 89 | This is the best example to get a complete understanding of how to register and work with real life DI container powered by `Rea-DI` within React application. In a nutshel this app is "just" an [Angular Tour of Heroes Tutorial](https://angular.io/tutorial) rewrite to React with `Rea-DI`. 90 | 91 | This example includes tests. 92 | 93 | ## Tour of Heroes ( with Redux and Redux-Observable ) 94 | 95 | > @TODO 96 | 97 | Run the [Tour of Heroes](./tour-of-heroes) example: 98 | 99 | ``` 100 | git clone https://github.com/hotell/rea-di.git 101 | 102 | cd rea-di/examples/tour-of-heroes-redux-redux-observable 103 | yarn install 104 | yarn start 105 | ``` 106 | 107 | Or check out the [sandbox](https://codesanbox.io/). 108 | -------------------------------------------------------------------------------- /examples/tour-of-heroes/src/app/hero.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from 'injection-js' 2 | 3 | import { Hero } from './hero' 4 | import { HttpClient } from './http-client.service' 5 | import { MessageService } from './messages' 6 | 7 | @Injectable() 8 | export class HeroService { 9 | private heroesUrl = '/heroes' 10 | constructor( 11 | private http: HttpClient, 12 | private messageService: MessageService 13 | ) {} 14 | 15 | getHeroes(): Promise { 16 | return this.http 17 | .get(this.heroesUrl) 18 | .then((response) => { 19 | this.messageService.add('HeroService: fetched heroes') 20 | 21 | return response.data 22 | }) 23 | .catch(this.handleError('getHeroes', [] as Hero[])) 24 | } 25 | 26 | getHero(id: number): Promise { 27 | return this.http 28 | .get(`${this.heroesUrl}/${id}`) 29 | .then((response) => { 30 | this.messageService.add(`HeroService: fetched hero id=${id}`) 31 | 32 | return response.data 33 | }) 34 | .catch(this.handleError(`getHero id=${id}`)) 35 | } 36 | 37 | /** PUT: update the hero on the server */ 38 | updateHero(hero: Hero) { 39 | return this.http 40 | .put(`${this.heroesUrl}/${hero.id}`, hero) 41 | .then((response) => { 42 | this.messageService.add(`HeroService: updated hero id=${hero.id}`) 43 | 44 | return response 45 | }) 46 | .catch(this.handleError('updateHero')) 47 | } 48 | 49 | /** POST: add a new hero to the server */ 50 | addHero(hero: Hero): Promise { 51 | return this.http 52 | .post(this.heroesUrl, hero) 53 | .then((response) => { 54 | this.messageService.add(`HeroService: added hero w/ id=${hero.id}`) 55 | 56 | return response.data 57 | }) 58 | .catch(this.handleError('addHero')) 59 | } 60 | 61 | /** DELETE: delete the hero from the server */ 62 | deleteHero(hero: Hero | number): Promise { 63 | const id = typeof hero === 'number' ? hero : hero.id 64 | const url = `${this.heroesUrl}/${id}` 65 | 66 | return this.http 67 | .delete(url) 68 | .then((response) => { 69 | this.messageService.add(`HeroService: deleted hero id=${id}`) 70 | 71 | return response.data 72 | }) 73 | .catch(this.handleError('deleteHero')) 74 | } 75 | 76 | /* GET heroes whose name contains search term */ 77 | searchHeroes(term: string): Promise { 78 | if (!term.trim()) { 79 | // if not search term, return empty hero array. 80 | return Promise.resolve([]) 81 | } 82 | 83 | return this.http 84 | .get(`${this.heroesUrl}/?q=${term}`) 85 | .then((response) => { 86 | this.messageService.add(`HeroService: found heroes matching "${term}"`) 87 | 88 | return response.data 89 | }) 90 | .catch(this.handleError('searchHeroes', [])) 91 | } 92 | 93 | /** 94 | * Handle Http operation that failed. 95 | * Let the app continue. 96 | * @param operation - name of the operation that failed 97 | * @param result - optional value to return as the observable result 98 | */ 99 | private handleError(operation = 'operation', result?: T) { 100 | return (error: any): Promise => { 101 | // TODO: send the error to remote logging infrastructure 102 | console.error(error) // log to console instead 103 | 104 | // TODO: better job of transforming error for user consumption 105 | this.messageService.add( 106 | `HeroService: ${operation} failed: ${error.message}` 107 | ) 108 | 109 | // Let the app keep running by returning an empty result. 110 | return Promise.resolve(result as T) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/provider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Provider as ProviderConfig, 3 | ReflectiveInjector, 4 | Type, 5 | } from 'injection-js' 6 | import React, { PureComponent, ReactElement } from 'react' 7 | 8 | import { IS_PROD } from '../environment' 9 | import { Context, ContextApi } from '../services/injector-context' 10 | import { StateCallback } from '../types' 11 | import { isProvider, isType } from '../utils/guards' 12 | import { Debug } from './debug' 13 | 14 | type Props = { 15 | children: ReactElement 16 | providers: ProviderConfig[] 17 | } 18 | type State = ContextApi 19 | 20 | /** 21 | * 22 | */ 23 | export class DependencyProvider extends PureComponent { 24 | private static _debugMode = { 25 | on: false, 26 | } 27 | static enableDebugMode() { 28 | if (IS_PROD) { 29 | // tslint:disable-next-line:no-console 30 | console.info('👉 DEBUG MODE IS AVAILABLE ONLY IN NON PROD ENV') 31 | 32 | return 33 | } 34 | 35 | this._debugMode.on = true 36 | } 37 | 38 | private injector?: ReflectiveInjector 39 | readonly state: State = {} as State 40 | private providersRegistered = false 41 | 42 | private monkeyPatchStateProviders( 43 | injector: ReflectiveInjector, 44 | providers: ProviderConfig[] 45 | ) { 46 | type TypeWithState = Type & { setState(...args: any[]): any } 47 | 48 | providers.reduce((acc, next) => { 49 | let stateKey: string 50 | let provideClass!: TypeWithState 51 | if (isType(next)) { 52 | stateKey = next.name 53 | provideClass = next as TypeWithState 54 | } 55 | if (isProvider(next)) { 56 | const { provide } = next 57 | stateKey = provide.name 58 | provideClass = provide 59 | } 60 | 61 | const hasState = 62 | isType(provideClass) && 'setState' in provideClass.prototype 63 | 64 | if (hasState) { 65 | const instance = injector.get(provideClass) 66 | const originalSetStateFn = instance.setState.bind(instance) 67 | 68 | const newSetStateFn = (state: StateCallback) => { 69 | // call service setState 70 | const newState = originalSetStateFn(state) as object 71 | 72 | // update context provider state 73 | this.setState( 74 | (prevState) => { 75 | return { [stateKey]: newState } 76 | } 77 | // () => originalSetStateFn(state) 78 | ) 79 | } 80 | 81 | instance.setState = newSetStateFn 82 | } 83 | 84 | return acc 85 | }, {}) 86 | } 87 | 88 | private get contextApi(): ContextApi { 89 | return { 90 | injector: this.injector, 91 | ...this.state, 92 | } 93 | } 94 | private renderProvider = ({ injector: parentInjector }: ContextApi) => { 95 | const isInDebugMode = !IS_PROD && DependencyProvider._debugMode.on 96 | 97 | this.injector = 98 | this.injector || 99 | parentInjector.resolveAndCreateChild(this.props.providers) 100 | 101 | if (!this.providersRegistered) { 102 | this.monkeyPatchStateProviders(this.injector, this.props.providers) 103 | } 104 | 105 | this.providersRegistered = true 106 | 107 | if (isInDebugMode) { 108 | return ( 109 | 110 | 115 | {this.props.children} 116 | 117 | 118 | ) 119 | } 120 | 121 | return ( 122 | 123 | {this.props.children} 124 | 125 | ) 126 | } 127 | 128 | render() { 129 | return {this.renderProvider} 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/__tests__/components/provide-inject.hoc.spec.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-magic-numbers 2 | // tslint:disable:no-use-before-declare 3 | 4 | import { mount } from 'enzyme' 5 | import React, { Component } from 'react' 6 | 7 | import { Injectable, Optional, ReflectiveInjector } from 'injection-js' 8 | import { withInjectables } from '../../components/inject.hoc' 9 | import { withDependencyProvider } from '../../components/provider.hoc' 10 | import { noop, optional } from '../../utils/helpers' 11 | import { Counter } from '../setup/components' 12 | import { CounterService, Logger } from '../setup/services' 13 | 14 | class CounterModule extends Component<{ title: string }> { 15 | render() { 16 | return ( 17 |
18 |

{this.props.title}

19 | Hello projection 20 |
21 | ) 22 | } 23 | } 24 | const CounterModuleEnhanced = withDependencyProvider(Logger, CounterService)( 25 | CounterModule 26 | ) 27 | 28 | const CounterEnhanced = withInjectables({ 29 | logger: Logger, 30 | counterService: CounterService, 31 | })(Counter) 32 | 33 | const App = () => { 34 | return ( 35 |
36 | 37 |
38 | ) 39 | } 40 | 41 | describe('Hoc wrappers', () => { 42 | it(`should work with HoC`, () => { 43 | const tree = mount() 44 | 45 | const counter = tree.find(Counter) 46 | const counterChildren = counter.prop('children') 47 | 48 | expect(counterChildren!.toString()).toBe('Hello projection') 49 | 50 | expect(tree).toBeTruthy() 51 | expect(tree).toMatchSnapshot() 52 | 53 | tree.unmount() 54 | }) 55 | 56 | it(`should properly update state on stateful service and reflect it on component tree`, () => { 57 | const tree = mount() 58 | 59 | const counter = tree.find(Counter) 60 | const counterLogger = counter.props().logger 61 | const incButton = counter.find('button').at(0) 62 | const decButton = counter.find('button').at(1) 63 | const countParagraph = counter.find('p') 64 | 65 | jest.spyOn(counterLogger, 'log').mockImplementation(noop) 66 | 67 | expect(countParagraph.text()).toBe('Count: 0') 68 | expect(counterLogger.log).not.toHaveBeenCalled() 69 | 70 | incButton.simulate('click') 71 | 72 | expect(countParagraph.text()).toBe('Count: 1') 73 | expect(counterLogger.log).toHaveBeenCalledTimes(1) 74 | 75 | decButton.simulate('click') 76 | decButton.simulate('click') 77 | 78 | expect(countParagraph.text()).toBe('Count: -1') 79 | expect(counterLogger.log).toHaveBeenCalledTimes(3) 80 | 81 | tree.unmount() 82 | }) 83 | 84 | it(`should should return wrapped component with proper prop annotation`, () => { 85 | const injector = ReflectiveInjector.resolveAndCreate([ 86 | Logger, 87 | CounterService, 88 | ]) 89 | const logger: Logger = injector.get(Logger) 90 | const counter: CounterService = injector.get(CounterService) 91 | 92 | expect(CounterModuleEnhanced.WrappedComponent).toBe(CounterModule) 93 | expect(CounterEnhanced.WrappedComponent).toBe(Counter) 94 | 95 | expect( 96 | 97 | ).toMatchSnapshot() 98 | expect( 99 | 103 | ).toMatchSnapshot() 104 | }) 105 | 106 | it(`should should create proper displayName`, () => { 107 | expect(CounterModuleEnhanced.displayName).toBe( 108 | 'WithDependencyProvider(CounterModule)' 109 | ) 110 | expect(CounterEnhanced.displayName).toBe('WithInjectables(Counter)') 111 | }) 112 | 113 | describe(`@Optional/optional()`, () => { 114 | @Injectable() 115 | class Engine { 116 | type?: string 117 | } 118 | 119 | @Injectable() 120 | class Car { 121 | engine: Engine | null 122 | constructor(@Optional() engine: Engine) { 123 | this.engine = engine ? engine : null 124 | } 125 | } 126 | const InjectConsumer = (props: { car: Car; engine: Engine | null }) => { 127 | return
{JSON.stringify(props)}
128 | } 129 | 130 | const InjectConsumerEnhanced = withInjectables({ 131 | car: Car, 132 | engine: optional(Engine), 133 | })(InjectConsumer) 134 | 135 | it(`should properly resolve optional injection on component level`, () => { 136 | const Test = withDependencyProvider(Car)(() => { 137 | return ( 138 |
139 | 140 |
141 | ) 142 | }) 143 | 144 | expect(() => mount()).not.toThrow() 145 | 146 | const tree = mount() 147 | 148 | expect(tree.text()).toEqual(`{"car":{"engine":null},"engine":null}`) 149 | expect(tree.find(InjectConsumer).prop('car').engine).toBe(null) 150 | expect(tree.find(InjectConsumer).prop('engine')).toBe(null) 151 | }) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@martin_hotell/rea-di", 3 | "version": "1.0.0", 4 | "description": "Dependency injection for React done right. Hierarchical injection on both component and service layer powered by injection-js (Angular DI framework)", 5 | "main": "./bundles/index.umd.js", 6 | "module": "./esm5/index.js", 7 | "es2015": "./esm2015/index.js", 8 | "typings": "./types/index.d.ts", 9 | "sideEffects": false, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://www.github.com/Hotell/rea-di" 13 | }, 14 | "author": "Martin Hochel", 15 | "license": "MIT", 16 | "engines": { 17 | "node": ">=8.5" 18 | }, 19 | "scripts": { 20 | "cleanup": "shx rm -rf dist", 21 | "prebuild": "npm run cleanup && npm run verify", 22 | "build": "tsc && tsc --target es2018 --outDir dist/esm2015 && rollup -c config/rollup.config.js && rollup -c config/rollup.config.js --environment NODE_ENV:production", 23 | "postbuild": "node scripts/copy.js && npm run size", 24 | "docs": "typedoc -p . --theme minimal --target 'es6' --excludeNotExported --excludePrivate --ignoreCompilerErrors --exclude \"**/src/**/__tests__/*.*\" --out docs src/", 25 | "test": "jest -c ./config/jest.config.js", 26 | "test:watch": "npm t -- --watch", 27 | "test:coverage": "npm t -- --coverage", 28 | "test:ci": "npm t -- --ci", 29 | "validate-js": "tsc -p ./config && tsc -p ./scripts", 30 | "verify": "npm run validate-js && npm run style && npm run test:ci", 31 | "commit": "git-cz", 32 | "style": "npm run format -- --list-different && npm run lint", 33 | "style:fix": "npm run format:fix && npm run lint:fix", 34 | "format": "prettier --config config/prettier.config.js \"**/*.{ts,tsx,js,jsx,css,scss,sass,less,md}\"", 35 | "format:fix": "npm run format -- --write", 36 | "lint": "tslint --project tsconfig.json --format codeFrame", 37 | "lint:fix": "npm run lint -- --fix", 38 | "prerelease": "npm run build", 39 | "release": "standard-version", 40 | "postrelease": "node scripts/copy.js && npm run release:github && npm run release:npm", 41 | "release:github": "git push --no-verify --follow-tags origin master", 42 | "release:npm": "cd dist && npm publish", 43 | "release:preflight": "cd dist && npm pack", 44 | "size": "npm run size:umd && npm run size:fesm", 45 | "size:umd": "shx echo \"Gzipped+minified UMD bundle Size:\" && cross-var strip-json-comments --no-whitespace \"./dist/bundles/index.umd.min.js\" | gzip-size", 46 | "size:fasm": "shx echo \"Gzipped FESM bundle Size:\" && strip-json-comments --no-whitespace \"./dist/fesm/index.js\" | gzip-size", 47 | "size:fesm": "shx echo \"Gzipped+minified FESM bundle Size:\" && strip-json-comments --no-whitespace \"./dist/bundles/index.esm.min.js\" | gzip-size" 48 | }, 49 | "config": { 50 | "commitizen": { 51 | "path": "./node_modules/cz-conventional-changelog" 52 | } 53 | }, 54 | "husky": { 55 | "hooks": { 56 | "commit-msg": "commitlint --config config/commitlint.config.js -E HUSKY_GIT_PARAMS", 57 | "pre-commit": "lint-staged", 58 | "pre-push": "npm run style && npm test -- --bail --onlyChanged" 59 | } 60 | }, 61 | "lint-staged": { 62 | "**/*.{ts,tsx,js,jsx,css,scss,sass,less,md}": [ 63 | "prettier --config config/prettier.config.js --write", 64 | "git add" 65 | ], 66 | "src/**/*.{ts,tsx}": [ 67 | "npm run lint:fix", 68 | "git add" 69 | ] 70 | }, 71 | "peerDependencies": { 72 | "injection-js": ">=2.2.1", 73 | "react": ">=15", 74 | "tslib": ">=1.9.0" 75 | }, 76 | "dependencies": {}, 77 | "devDependencies": { 78 | "@abraham/reflection": "0.4.2", 79 | "@commitlint/cli": "7.2.1", 80 | "@commitlint/config-conventional": "7.1.2", 81 | "@types/chokidar": "1.7.5", 82 | "@types/enzyme": "3.1.15", 83 | "@types/enzyme-adapter-react-16": "1.0.3", 84 | "@types/jest": "23.3.10", 85 | "@types/node": "10.12.12", 86 | "@types/prettier": "1.15.2", 87 | "@types/react": "16.7.13", 88 | "@types/react-dom": "16.0.11", 89 | "@types/webpack-config-utils": "2.3.0", 90 | "awesome-typescript-loader": "5.2.1", 91 | "commitizen": "3.0.5", 92 | "cross-var": "1.1.0", 93 | "cz-conventional-changelog": "2.1.0", 94 | "enzyme": "3.7.0", 95 | "enzyme-adapter-react-16": "1.7.0", 96 | "enzyme-to-json": "3.3.5", 97 | "gzip-size-cli": "3.0.0", 98 | "husky": "1.2.0", 99 | "injection-js": "2.2.1", 100 | "jest": "23.6.0", 101 | "jest-watch-typeahead": "0.2.0", 102 | "lint-staged": "8.1.0", 103 | "prettier": "1.15.3", 104 | "react": "16.6.3", 105 | "react-dom": "16.6.3", 106 | "reflect-metadata": "0.1.12", 107 | "rollup": "0.67.4", 108 | "rollup-plugin-commonjs": "9.2.0", 109 | "rollup-plugin-json": "3.1.0", 110 | "rollup-plugin-node-resolve": "3.4.0", 111 | "rollup-plugin-replace": "2.1.0", 112 | "rollup-plugin-sourcemaps": "0.4.2", 113 | "rollup-plugin-terser": "3.0.0", 114 | "rollup-plugin-uglify": "6.0.0", 115 | "shx": "0.3.2", 116 | "standard-version": "4.4.0", 117 | "strip-json-comments-cli": "1.0.1", 118 | "ts-jest": "23.10.5", 119 | "tslib": "1.9.3", 120 | "tslint": "5.11.0", 121 | "tslint-config-prettier": "1.17.0", 122 | "tslint-config-standard": "8.0.1", 123 | "tslint-etc": "1.2.7", 124 | "tslint-react": "3.6.0", 125 | "typedoc": "0.13.0", 126 | "typescript": "3.1.6", 127 | "webpack-config-utils": "2.3.1" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/__tests__/components/provide-inject.spec.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-magic-numbers 2 | // tslint:disable:jsx-no-lambda 3 | // tslint:disable:no-shadowed-variable 4 | import React, { Component } from 'react' 5 | 6 | import { getMetadata } from '@abraham/reflection' 7 | import { mount } from 'enzyme' 8 | import { Injectable, Optional } from 'injection-js' 9 | import { Inject } from '../../components/inject' 10 | import { DependencyProvider } from '../../components/provider' 11 | import { metadataKey, noop, optional, tuple } from '../../utils/helpers' 12 | import { Counter } from '../setup/components' 13 | import { CounterService, Logger } from '../setup/services' 14 | import { select } from '../setup/utils' 15 | 16 | class CounterModule extends Component<{ title: string }> { 17 | render() { 18 | return ( 19 |
20 |

{this.props.title}

21 | 22 | {(logger, counterService) => { 23 | return ( 24 | 25 | Hello projection 26 | 27 | ) 28 | }} 29 | 30 |
31 | ) 32 | } 33 | } 34 | 35 | const App = () => { 36 | return ( 37 |
38 | 39 | 40 | 41 |
42 | ) 43 | } 44 | 45 | describe(`Provide/Inject`, () => { 46 | it(`should properly inject`, () => { 47 | const tree = mount() 48 | 49 | const counter = tree.find(Counter) 50 | const counterChildren = counter.prop('children') 51 | 52 | expect(counterChildren!.toString()).toBe('Hello projection') 53 | 54 | expect(tree).toBeTruthy() 55 | expect(tree).toMatchSnapshot() 56 | 57 | tree.unmount() 58 | }) 59 | 60 | it(`should properly update state on stateful service and reflect it on component tree`, () => { 61 | const tree = mount() 62 | 63 | const counter = tree.find(Counter) 64 | const counterLogger = counter.props().logger 65 | const incButton = counter.find('button').at(0) 66 | const decButton = counter.find('button').at(1) 67 | const countParagraph = counter.find('p') 68 | 69 | jest.spyOn(counterLogger, 'log').mockImplementation(noop) 70 | 71 | expect(countParagraph.text()).toBe('Count: 0') 72 | expect(counterLogger.log).not.toHaveBeenCalled() 73 | 74 | incButton.simulate('click') 75 | 76 | expect(countParagraph.text()).toBe('Count: 1') 77 | expect(counterLogger.log).toHaveBeenCalledTimes(1) 78 | 79 | decButton.simulate('click') 80 | decButton.simulate('click') 81 | 82 | expect(countParagraph.text()).toBe('Count: -1') 83 | expect(counterLogger.log).toHaveBeenCalledTimes(3) 84 | 85 | tree.unmount() 86 | }) 87 | 88 | it(`should properly create hierarchical injectors`, () => { 89 | class LoggerMock implements Logger { 90 | log = jest.fn((...args: any[]) => void 0) 91 | } 92 | // tslint:disable:prefer-const 93 | let parentLoggerInstance!: LoggerMock 94 | let childLoggerInstance!: LoggerMock 95 | 96 | const App = () => { 97 | return ( 98 | 101 |
102 | 103 | {(fromParentLogger) => { 104 | parentLoggerInstance = fromParentLogger as LoggerMock 105 | 106 | return ( 107 |
108 | 115 | 118 |
119 | 120 | {(fromChildLogger) => { 121 | childLoggerInstance = fromChildLogger as LoggerMock 122 | 123 | return ( 124 |
125 | 132 | 139 |
140 | ) 141 | }} 142 |
143 |
144 |
145 |
146 | ) 147 | }} 148 |
149 |
150 |
151 | ) 152 | } 153 | 154 | const tree = mount() 155 | 156 | const parentBtn = tree.find(`${select('parentInjector')} button`).at(0) 157 | const childrenButtons = tree.find(`${select('childInjector')} button`) 158 | const childrenBtnThatLogsParent = childrenButtons.at(0) 159 | const childrenBtnThatLogsChild = childrenButtons.at(1) 160 | 161 | expect(parentLoggerInstance.log).not.toHaveBeenCalled() 162 | 163 | parentBtn.simulate('click') 164 | 165 | expect(parentLoggerInstance.log).toHaveBeenCalledWith('from parent') 166 | 167 | childrenBtnThatLogsParent.simulate('click') 168 | 169 | expect(parentLoggerInstance.log).toHaveBeenLastCalledWith( 170 | 'parent from child' 171 | ) 172 | expect(parentLoggerInstance.log).toHaveBeenCalledTimes(2) 173 | 174 | expect(childLoggerInstance.log).not.toHaveBeenCalled() 175 | 176 | childrenBtnThatLogsChild.simulate('click') 177 | 178 | expect(parentLoggerInstance.log).toHaveBeenCalledTimes(2) 179 | expect(childLoggerInstance.log).toHaveBeenCalledTimes(1) 180 | expect(childLoggerInstance.log).toHaveBeenLastCalledWith('from child') 181 | }) 182 | 183 | describe(`@Optional/optional()`, () => { 184 | @Injectable() 185 | class Engine { 186 | type?: string 187 | } 188 | 189 | @Injectable() 190 | class Car { 191 | engine: Engine | null 192 | constructor(@Optional() engine: Engine) { 193 | this.engine = engine ? engine : null 194 | } 195 | } 196 | 197 | @Injectable() 198 | class CarWillCrashWithoutEngine { 199 | constructor(public engine: Engine) {} 200 | } 201 | 202 | it(`should add metadata only once via wrapper`, () => { 203 | expect(getMetadata(metadataKey, Engine)).toEqual(undefined) 204 | 205 | const DecoratedEngine = optional(Engine) 206 | const OriginalEngine = ((DecoratedEngine as any) as () => typeof Engine)() 207 | 208 | expect(getMetadata(metadataKey, DecoratedEngine!)).toEqual({ 209 | optional: true, 210 | }) 211 | expect(getMetadata(metadataKey, Engine)).toEqual(undefined) 212 | expect(getMetadata(metadataKey, OriginalEngine)).toEqual(undefined) 213 | expect(OriginalEngine).toBe(Engine) 214 | }) 215 | 216 | it(`should properly resolve optional injection on component level`, () => { 217 | const InjectConsumer = (props: { car: Car }) => { 218 | return
{JSON.stringify(props.car)}
219 | } 220 | 221 | const App = () => { 222 | return ( 223 | 224 | 225 | {( 226 | car /*$ExpectType Car*/, 227 | engine /* $ExpectType Engine | null*/ 228 | ) => { 229 | return ( 230 |
231 |
{JSON.stringify(engine)}
232 | 233 |
234 | ) 235 | }} 236 |
237 |
238 | ) 239 | } 240 | 241 | expect(() => mount()).not.toThrow() 242 | 243 | const tree = mount() 244 | expect(tree.find('pre').text()).toEqual(`null`) 245 | expect(tree.find(InjectConsumer).prop('car').engine).toBe(null) 246 | }) 247 | 248 | it(`should properly resolve @Optional injection`, () => { 249 | const InjectConsumer = (props: { car: Car }) => { 250 | return
{JSON.stringify(props.car)}
251 | } 252 | 253 | const App = () => { 254 | return ( 255 | 256 | 257 | {(car) => { 258 | return 259 | }} 260 | 261 | 262 | ) 263 | } 264 | 265 | const tree = mount() 266 | 267 | expect(tree.text()).toEqual(`{"engine":null}`) 268 | expect(tree.find(InjectConsumer).prop('car').engine).toBe(null) 269 | 270 | function willThrow() { 271 | const App = () => ( 272 | 273 | 274 | {(_) => JSON.stringify(_)} 275 | 276 | 277 | ) 278 | 279 | mount() 280 | } 281 | 282 | expect(willThrow).toThrow( 283 | 'No provider for Engine! (CarWillCrashWithoutEngine -> Engine)' 284 | ) 285 | }) 286 | }) 287 | }) 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rea-di 2 | 3 | > Dependency injection for React done right. Hierarchical injection on both component and service layer powered by [injection-js](https://github.com/mgechev/injection-js) (Angular DI framework without Angular dependency 💪) 🖖 4 | > 5 | > rea-di [pronounced "Ready" 🤙] 6 | 7 | > **Enjoying/Using rea-di ? 💪✅** 8 | > 9 | > 10 | 11 | [![Greenkeeper badge](https://badges.greenkeeper.io/Hotell/rea-di.svg)](https://greenkeeper.io/) 12 | 13 | [![Build Status](https://travis-ci.org/Hotell/rea-di.svg?branch=master)](https://travis-ci.org/Hotell/rea-di) 14 | [![NPM version](https://img.shields.io/npm/v/%40martin_hotell%2Frea-di.svg)](https://www.npmjs.com/package/@martin_hotell/rea-di) 15 | ![Downloads](https://img.shields.io/npm/dm/@martin_hotell/rea-di.svg) 16 | [![Standard Version](https://img.shields.io/badge/release-standard%20version-brightgreen.svg)](https://github.com/conventional-changelog/standard-version) 17 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 18 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 19 | 20 | ## Installing 21 | 22 | ```sh 23 | yarn add @martin_hotell/rea-di 24 | 25 | # install peer dependencies 26 | yarn add react injection-js tslib 27 | 28 | # install Reflect API polyfill 29 | yarn add @abraham/reflection 30 | ``` 31 | 32 | > **Note:** 33 | > 34 | > You need a polyfill for the [Reflect API](http://www.ecma-international.org/ecma-262/6.0/#sec-reflection). 35 | > 36 | > We highly recommend tiny [reflection](https://www.npmjs.com/package/@abraham/reflection) polyfill ( 3kB only ! ) 37 | > 38 | > Also for TypeScript you will need to enable `experimentalDecorators` and `emitDecoratorMetadata` flags within your `tsconfig.json` 39 | 40 | ## Getting started 41 | 42 | Let's demonstrate simple usage with old good Counter example: 43 | 44 | [![Edit counter-example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/Hotell/rea-di/tree/master/examples/counter) 45 | 46 | ```tsx 47 | import React, { Component } from 'react' 48 | import { render } from 'react-dom' 49 | 50 | import { Injectable } from 'injection-js' 51 | import { DependencyProvider, Stateful } from '@martin_hotell/rea-di' 52 | 53 | // we create injectable and state aware service 54 | 55 | type State = Readonly 56 | const initialState = { 57 | count: 0, 58 | } 59 | 60 | @Injectable() 61 | export class CounterService extends Stateful { 62 | readonly state = initialState 63 | 64 | increment() { 65 | this.setState((prevState) => ({ count: prevState.count + 1 })) 66 | } 67 | decrement() { 68 | this.setState((prevState) => ({ count: prevState.count - 1 })) 69 | } 70 | incrementIfOdd() { 71 | if (this.state.count % 2 !== 0) { 72 | this.increment() 73 | } 74 | } 75 | 76 | incrementAsync() { 77 | setTimeout(() => this.increment(), 1000) 78 | } 79 | } 80 | 81 | export class Counter extends Component { 82 | render() { 83 | return ( 84 | // We request to inject CounterService instance, which will be provided by closest parent DependencyProvider(Injector) (in our case we created parent ) 85 | // ☝️ `values` prop is an tuple of Tokens(tokens used to register provider to injector) 86 | // Now our injectables will be available within render prop, via positional arguments, `[CounterService] -> ((counterService) => (...)` 87 | 88 | {(counterService) => ( 89 |

90 | Clicked: {counterService.state.count} times 91 | 92 | 93 | 96 | 99 |

100 | )} 101 |
102 | ) 103 | } 104 | } 105 | 106 | render( 107 | // We create Parent Injector via Provider component which will resolve CounterService and thus will make it available within whole app tree 108 | 109 | 110 | , 111 | document.getElementById('root') 112 | ) 113 | ``` 114 | 115 | For more examples, see the following examples section 👀 116 | 117 | ## Examples 118 | 119 | Go checkout [examples](./examples) ! 120 | 121 | ## API 122 | 123 | > rea-di API is tiny 👌. It starts and ends with components and javascript, 2 core things that we love React for ❤️ 124 | 125 | There are 2 components for registering and injecting services and 2 HoC(High order components) which just leverage former under the hood (if that's your preferred way of composition) and 1 service abstract class to make services state aware. 126 | 127 | ### `DependencyProvider<{providers: Provider[], children: ReactElement}>` 128 | 129 | **Example:** 130 | 131 | ```tsx 132 | 133 | ...your tree... 134 | 135 | ``` 136 | 137 | #### `DependencyProvider.enableDebugMode(): void` 138 | 139 | - renders injector tree with registered providers in your view 140 | 141 | ### `Inject<{values: Type[], children(...injectables)=>ReactNode}>` 142 | 143 | ```tsx 144 | {(serviceOne)=>...} 145 | ``` 146 | 147 | > **NOTE:** 148 | > if you need inject multiple providers you have to use `tuple` as TS won't properly infer array to strictly typed tuple: 149 | > 150 | > ```tsx 151 | > 152 | > {(serviceOne, serviceTwo) => <>...} 153 | > 154 | > ``` 155 | 156 | ### `withDependencyProvider(...providers:T): React.ComponentClass` 157 | 158 | ```tsx 159 | class Root extends Component { 160 | /*...*/ 161 | } 162 | 163 | const EnhancedRoot = withDependencyProvider(ServiceOne, ServiceTwo)( 164 | MyParentComponent 165 | ) 166 | ``` 167 | 168 | ### `withInjectables(tokenMap): React.ComponentClass` 169 | 170 | ```tsx 171 | // you can see that injectValuesMap is config object for withInjectables HoC, 172 | // which will map those keys to your component props with proper instance registered by provided Token within injector 173 | const injectValuesMap = { serviceOne: ServiceOne } 174 | 175 | class MyComponentWithInjectables extends Component { 176 | /*...*/ 177 | } 178 | 179 | const EnhancedComponent = withInjectables(injectValuesMap)( 180 | MyComponentWithInjectables 181 | ) 182 | 183 | const Tree = () => 184 | ``` 185 | 186 | ### `Stateful` 187 | 188 | Abstract class which implements `setState` on your service class. If you wanna handle state within your service you need to extend from this Base class and implement `state`, exactly like you would with `React.Component` 189 | 190 | ```ts 191 | const initialState = { 192 | count: 0, 193 | } 194 | 195 | @Injectable() 196 | class CounterService extends Stateful { 197 | readonly state = initialState 198 | inc() { 199 | this.setState((prevState) => ({ count: prevState.count + 1 })) 200 | } 201 | dec() { 202 | this.setState((prevState) => ({ count: prevState.count - 1 })) 203 | } 204 | } 205 | ``` 206 | 207 | ### `tuple(...args: T): T` 208 | 209 | - helper function to be used within `` if you need to inject more than 1 injectable 210 | 211 | Following will produce type errors as TypeScript will create array of unions instead of tuple type: 212 | 213 | ```tsx 214 | 215 | {/* TS Error */} 216 | {( 217 | serviceOne /* $ExpectType ServiceOne | ServiceTwo */, 218 | serviceTwo /* $ExpectType ServiceTwo | ServiceTwo */ 219 | ) => <>...} 220 | 221 | ``` 222 | 223 | By using `tuple` identity helper, everything works as expected 224 | 225 | ```tsx 226 | 227 | {( 228 | serviceOne /* $ExpectType ServiceOne */, 229 | serviceTwo /* $ExpectType ServiceTwo */ 230 | ) => <>...} 231 | 232 | ``` 233 | 234 | ### `optional(Token: T): T | null` 235 | 236 | - like `@Optional` but for component level injection 237 | 238 | Marks token as injectable so if it provider with current token will not be registered on component tree it will not throw but return `null` 239 | 240 | ```tsx 241 | 242 | {( 243 | serviceOne /* $ExpectType ServiceOne */, 244 | serviceTwo /* $ExpectType: ServiceTwo | null */ 245 | ) => <>...} 246 | 247 | ``` 248 | 249 | > **NOTE:** 250 | > 251 | > don't try to do anything tricky with this. Under the hood original token is being wrapped within an identity function that gets metadata via Reflect.API so we can properly resolve injection without trowing any errors. We just trick type-system to get proper DX and inference for consumer 252 | > 👉 `expect(optional(ServiceTwo)).toEqual(()=>ServiceTwo)` 253 | 254 | ## Guides 255 | 256 |
257 | Building a github user search 258 | 259 | Let's build a simple github user search app, by leveraging `rea-di`. 260 | 261 | This is what we're gonna build: ![github User Search app](./examples/img/github-user-search.gif) 262 | 263 | And this is how DI tree will look like ![github User Search app DI tree](./examples/img/github-search-di.png) 264 | 265 | > For complete implementation/demo checkout [examples](./examples/github-user) 266 | 267 | 1. Implementing GithubUserService 268 | 269 | We need implement our service, which is a pure javascript class with to encapsulate logic for fetching user data from github. To make it work with `rea-di` and `injection-js` we need to annotate our class with `@Injectable()` decorator. Now we can leverage dependency injection via constructor injection, and inject an `HttpClient` [axios-http](https://github.com/Hotell/axios-http), which will be used for XHR. 270 | 271 | We will implement 3 methods, for getting user info, user repos and one aggregated method for getting both. 272 | 273 | ```tsx 274 | // user.service.ts 275 | import { HttpClient } from '@martin_hotell/axios-http' 276 | import { Injectable } from 'injection-js' 277 | 278 | import { GithubUserRepo } from './repo.model' 279 | import { GithubUser } from './user.model' 280 | 281 | const endpointPath = 'users' 282 | 283 | @Injectable() 284 | export class GithubUserService { 285 | constructor(private http: HttpClient) {} 286 | getRepos(username: string) { 287 | return this.http.get(`${endpointPath}/${username}/repos`) 288 | } 289 | 290 | getUserInfo(username: string) { 291 | return this.http.get(`${endpointPath}/${username}`) 292 | } 293 | 294 | getGithubInfo(username: string) { 295 | return Promise.all([ 296 | this.getRepos(username), 297 | this.getUserInfo(username), 298 | ]).then(([repos, bio]) => ({ repos: repos.data, bio: bio.data })) 299 | } 300 | } 301 | ``` 302 | 303 | 1. Wiring our app DI capabilities to React component tree via `rea-di` 304 | 305 | Now let's wire our service with our React component tree: 306 | 307 | ```tsx 308 | // app.tsx 309 | import React, { Component } from 'react' 310 | import { registerHttpClientProviders } from '@martin_hotell/axios-http' 311 | import { DependencyProvider } from '@martin_hotell/rea-di' 312 | 313 | import { Profile } from './components/profile' 314 | import SearchUser from './components/search-user' 315 | import { GithubUserService } from './user.service' 316 | 317 | export class App extends Component { 318 | render() { 319 | return ( 320 |
321 |

GitHub User Search 👀

322 | 328 | <> 329 | 330 | 331 | 332 | 333 |
334 | ) 335 | } 336 | } 337 | ``` 338 | 339 | Quite a lot happening there, let's go step by step 340 | 341 | So we are using `` component which has one prop, `provide`. We need to pass here all providers that we wanna make available for all descendant components on the tree from our injector. 342 | 343 | In our case we need to register 2 Providers: 344 | 345 | - `registerHttpClientProviders` - function provided by axios-http, which registers all internal providers and makes `HttpClient` injectable 346 | - `GithubUserService` - our injectable service class 347 | 348 | ```tsx 349 | 355 | {/*...*/} 356 | 357 | ``` 358 | 359 | With that solved, we can inject service instances anywhere in our component tree via `` component or via `withInjectables()` High order component. 360 | 361 | 3. Implementing SearchUser component 362 | 363 | This component will handle our search form. On submit it will call methods from `GithubUserService` instance. 364 | 365 | With that said, we need to inject `GithubUserService` to our component. We could use `` within our render but for this case we wanna use `GithubUserService` outside `render` so HoC is a great candidate for this use case. And of course it's gonna be "injected" via React component injection, which is nothing else than old good React props ✌️. 366 | 367 | ```tsx 368 | type Props = { 369 | userService: GithubUserService 370 | } 371 | 372 | export class SearchUser extends React.Component { 373 | private usernameRef = createRef() 374 | private submitBtnRef = createRef() 375 | 376 | render() { 377 | return ( 378 |
this._handleSubmit(ev)}> 379 | 384 | 387 |
388 | ) 389 | } 390 | 391 | _handleSubmit(ev: SyntheticEvent) { 392 | ev.preventDefault() 393 | const username = this.usernameRef.current! 394 | const btn = this.submitBtnRef.current! 395 | 396 | // disable form on submit 397 | btn.disabled = true 398 | username.disabled = true 399 | 400 | // now we can fetch bio and repos of selected user by calling injected userService.getGithubInfo 401 | this.props.userService 402 | .getGithubInfo(username.value) 403 | .then(({ bio, repos }) => { 404 | btn.disabled = false 405 | username.disabled = false 406 | username.value = '' 407 | }) 408 | } 409 | } 410 | 411 | // last step is to wire our SearchUser to DI container 412 | export default withInjectables({ userService: GithubUserService })(SearchUser) 413 | ``` 414 | 415 | Hmm but something is missing here right ? We wanna save our fetched data... somewhere ! we could indeed store it within parent component or even in this one, but because we're already using DI, we can make our `GithubUserService` stateful. Let's do that first! 416 | 417 | All we need to do to make injectable service stateful, is to extend it with `Stateful` generic abstract class, which implements `setState` method (the same like React.Component) 418 | 419 | ```tsx 420 | // user.service.ts 421 | import { Injectable } from 'injection-js' 422 | import { Stateful } from '@martin_hotell/rea-di' 423 | 424 | // (1) we define State from implementation (the same pattern as you're used to from React) 425 | type State = Readonly 426 | const initialState = { 427 | username: '', 428 | bio: null as GithubUser | null, 429 | repos: null as GithubUserRepo[] | null, 430 | } 431 | 432 | // (2) now we extend our class WithState 433 | @Injectable() 434 | export class GithubUserService extends Stateful { 435 | // (3) we set service our state 436 | readonly state = initialState 437 | 438 | constructor(private http: HttpClient) { 439 | super() 440 | } 441 | 442 | // (4) and we implement `setActiveUser` method which will update our internal service state 443 | setActiveUser(user: Partial) { 444 | this.setState((prevState) => ({ ...prevState, ...user })) 445 | } 446 | 447 | getRepos(username: string) { 448 | /*...*/ 449 | } 450 | 451 | getUserInfo(username: string) { 452 | /*...*/ 453 | } 454 | 455 | getGithubInfo(username: string) { 456 | /*...*/ 457 | } 458 | } 459 | ``` 460 | 461 | With our stateful `GithubUserService` we can update `SearchUser._handleSubmit` method: 462 | 463 | ```tsx 464 | // search-user.tsx 465 | export class SearchUser extends Component { 466 | _handleSubmit(ev: React.FormEvent) { 467 | ev.preventDefault() 468 | const username = this.usernameRef.current! 469 | const btn = this.submitBtnRef.current! 470 | 471 | // disable form on submit 472 | btn.disabled = true 473 | username.disabled = true 474 | 475 | // first we set just username to our service state 476 | // this will trigger re-render on every component that injects userService 477 | this.props.userService.setActiveUser({ username: username.value }) 478 | 479 | // now we can fetch bio and repos of selected user by calling injected userService.getGithubInfo 480 | this.props.userService 481 | .getGithubInfo(username.value) 482 | .then(({ bio, repos }) => { 483 | // we store resolved data (bio and repos) to our service state 484 | this.props.userService.setActiveUser({ bio, repos }) 485 | 486 | // we enable our form again 487 | btn.disabled = false 488 | username.disabled = false 489 | username.value = '' 490 | }) 491 | } 492 | } 493 | ``` 494 | 495 | Now we need to implement the last part of our app. Rendering the User Profile Bio and Repos. 496 | 497 | 4. Implementing Profile component 498 | 499 | Our `GithubUserService` is stateful, so all we need to do is to inject it within our `Profile` component. This time we don't need to access `userService` outside `render` so using `` is the perfect candidate for wiring up Profile with our DI tree. 500 | 501 | ```tsx 502 | // profile.tsx 503 | import { Inject } from '@martin_hotell/rea-di' 504 | import React, { Component } from 'react' 505 | 506 | import { GithubUserService } from '../user.service' 507 | import { Repos } from './repos' 508 | import { UserProfile } from './user-profile' 509 | 510 | export class Profile extends Component { 511 | render() { 512 | return ( 513 | // (1) we specify token tuple/array `GithubUserService`, with which we're saying what instance is gonna be injected within children function arguments 514 | 515 | {(userService) => { 516 | // (2) we got our userService, and we use destructuring on its state 517 | const { username, repos, bio } = userService.state 518 | 519 | // (3) we render only when both bio and repos have been fetched and stored within our service instance 520 | if (bio && repos) { 521 | return ( 522 |
523 |
524 | 525 |
526 |
527 | 528 |
529 |
530 | ) 531 | } 532 | 533 | // if username only is set, that means we are in submitting phase 534 | if (username) { 535 | return `Loading... ${username}` 536 | } 537 | }} 538 |
539 | ) 540 | } 541 | } 542 | ``` 543 | 544 | And that's it! 545 | 546 | For complete implementation/demo checkout [examples](./examples/github-user) 547 | 548 |
549 | 550 | --- 551 | 552 | ### State management within service layer 553 | 554 | For developers with Angular background, storing state within Service is a must have. While that makes sense in Angular ( because handling state within Angular component is a mess ) in React this abstraction isn't needed that much as React component state is mostly sufficient for that purpose. 555 | 556 | With `rea-di`, you can handle state on service layer although we encourage you to handle state internally in `Component.state` or via some store state management library ( like Redux ). 557 | 558 | > For those familiar with `Unstated`, with `rea-di`, you got all unstated library power at your disposal within service layer and much more 🌻. 559 | 560 | Ok let's look at our previous example. We handle users array state within `Users` Component. We can make our `UserService` state aware and make it handle our state and with that remove any state from our components. 561 | 562 | ```tsx 563 | // app/services.ts 564 | import { Stateful } from 'rea-di' 565 | 566 | // (1) we define State type and initialState which needs to be implemented when we extend WithState 567 | type State = typeof Readonly 568 | const initialState = { 569 | users: null as User[] | null, 570 | } 571 | 572 | @Injectable() 573 | // (2) WithState is a generic base class which provides `protected setState()` method and forces you to implement state within your service 574 | export class UserService extends Stateful { 575 | // constructor Injection 576 | constructor(private httpClient: HttpClient, private logger: Logger) { 577 | // (3) we need to call super() as we are extending BaseClass 578 | super() 579 | } 580 | 581 | // (4) we implement our service state 582 | readonly state: State = initialState 583 | 584 | getUsers(): Promise { 585 | this.logger.log('get users fetch started') 586 | 587 | return this.httpClient.get('api/users').then((response)=>{ 588 | // (5) when http finishes, we update our service state. 589 | // This state will work exactly like React state and will re-render components where it's used 590 | this.setState(()=>({users:response})) 591 | }) 592 | } 593 | } 594 | ``` 595 | 596 | With that implemented, we can update our `Users` component ( emove state handling from it) 597 | 598 | ```tsx 599 | // app/users.tsx 600 | type Props = { 601 | service: UserService 602 | } 603 | 604 | class Users extends Component { 605 | render() { 606 | const { service } = this.props 607 | return ( 608 |
609 | {service.state.users ? ( 610 | 'Loading users...' 611 | ) : ( 612 | 613 | )} 614 |
615 | ) 616 | } 617 | componentDidMount() { 618 | // we only trigger HTTP call via our injected service. State will be handled and updated internally in that service 619 | this.props.service.getUsers() 620 | } 621 | } 622 | ``` 623 | 624 | ### Writing tests 625 | 626 | Testing belongs to one of the main areas where DI framework shines! 627 | 628 | How to test our components with rea-di ? 629 | 630 | You just provide mocks of your services for both unit and integration tests and you're good to go 👌. Old good React ❤️ 631 | 632 | ```tsx 633 | import { DependencyProvider } from 'rea-di' 634 | 635 | const DATA: Users[] = [{ name: 'Martin' }, { name: 'John' }] 636 | 637 | class UserServiceMock extends UserService { 638 | getUsers = jest.fn(() => this.setState(() => ({ users: DATA }))) 639 | } 640 | 641 | describe(' Unit Test', () => { 642 | it('should fetch users and render them', () => { 643 | const service = new UserServiceMock() 644 | const wrapper = mount() 645 | 646 | expect(service.getUsers).toHaveBeenCalled() 647 | expect(service.state).toEqual({ users: DATA }) 648 | expect(wrapper.find(UserList)).toBe(true) 649 | }) 650 | }) 651 | 652 | describe(' Integration Test', () => { 653 | it('should fetch users and render them', () => { 654 | const wrapper = mount( 655 | // we create new ChildInjector with same token, just changing the Implementation that's gonna be instantiated ;) 656 | 659 | 660 | 661 | ) 662 | 663 | expect(service.getUsers).toHaveBeenCalled() 664 | expect(service.state).toEqual({ users: DATA }) 665 | expect(wrapper.find(UserList)).toBe(true) 666 | }) 667 | }) 668 | ``` 669 | 670 | --- 671 | 672 | ## Publishing 673 | 674 | Execute `yarn release` which will handle following tasks: 675 | 676 | - bump package version and git tag 677 | - update/(create if it doesn't exist) CHANGELOG.md 678 | - push to github master branch + push tags 679 | - publish build packages to npm 680 | 681 | > releases are handled by awesome [standard-version](https://github.com/conventional-changelog/standard-version) 682 | 683 | ### Pre-release 684 | 685 | - To get from `1.1.2` to `1.1.2-0`: 686 | 687 | `yarn release --prerelease` 688 | 689 | - **Alpha**: To get from `1.1.2` to `1.1.2-alpha.0`: 690 | 691 | `yarn release --prerelease alpha` 692 | 693 | - **Beta**: To get from `1.1.2` to `1.1.2-beta.0`: 694 | 695 | `yarn release --prerelease beta` 696 | 697 | ### Dry run mode 698 | 699 | See what commands would be run, without committing to git or updating files 700 | 701 | `yarn release --dry-run` 702 | 703 | ### Check what files are gonna be published to npm 704 | 705 | - `yarn pack` OR `yarn release:preflight` which will create a tarball with everything that would get published to NPM 706 | 707 | ## Tests 708 | 709 | Test are written and run via Jest 💪 710 | 711 | ``` 712 | yarn test 713 | # OR 714 | yarn test:watch 715 | ``` 716 | 717 | ## Style guide 718 | 719 | Style guides are enforced by robots, I meant prettier and tslint of course 🤖 , so they'll let you know if you screwed something, but most of the time, they'll autofix things for you. Magic right ? 720 | 721 | ### Style guide npm scripts 722 | 723 | ```sh 724 | #Format and fix lint errors 725 | yarn ts:style:fix 726 | ``` 727 | 728 | ## Generate documentation 729 | 730 | `yarn docs` 731 | 732 | ## Commit ( via commitizen ) 733 | 734 | - this is preferred way how to create conventional-changelog valid commits 735 | - if you prefer your custom tool we provide a commit hook linter which will error out, it you provide invalid commit message 736 | - if you are in rush and just wanna skip commit message validation just prefix your message with `WIP: something done` ( if you do this please squash your work when you're done with proper commit message so standard-version can create Changelog and bump version of your library appropriately ) 737 | 738 | `yarn commit` - will invoke [commitizen CLI](https://github.com/commitizen/cz-cli) 739 | 740 | ### Troubleshooting 741 | 742 | ## Licensing 743 | 744 | [MIT](./LICENSE.md) as always 745 | --------------------------------------------------------------------------------