├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.d.ts ├── package.json ├── src ├── StoreContainer.ts ├── StoreProvider.tsx ├── index.ts ├── inject.ts └── utils.ts ├── test ├── index.ts └── tsconfig.json ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── webpack.test.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "latest"], 3 | "compact": false 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | npm-debug.log 5 | lib 6 | test-build -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | src 4 | test 5 | test-build 6 | node_modules 7 | tsconfig.json 8 | tslint.json 9 | typings.json 10 | npm-debug.log 11 | webpack.test.config.js 12 | webpack.config.js 13 | .babelrc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 Pavel Sokolov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mobx-react-inject 2 | Implementation of store injection to react component with mobx, typescript and decorator metadata 3 | 4 | ## Installation 5 | ```bash 6 | npm i --save mobx-react-inject reflect-metadata 7 | ``` 8 | 9 | ## Preparations 10 | ```json 11 | //tsconfig.json 12 | { 13 | "compilerOptions": { 14 | //... 15 | "emitDecoratorMetadata": true, 16 | "experimentalDecorators": true, 17 | //... 18 | } 19 | } 20 | ``` 21 | ```ts 22 | //your-main-index.ts 23 | import "reflect-metadata" 24 | ``` 25 | ## Usage 26 | ### Create store provider 27 | ```ts 28 | import {StoreProvider} from "mobx-react-inject" 29 | 30 | class App extends React.Component<{}, void> { 31 | 32 | render() { 33 | return 34 | ... 35 | 36 | } 37 | } 38 | ``` 39 | ### Inject your store to component 40 | ```ts 41 | class MyStore { 42 | public hello() { 43 | return "Hello" 44 | } 45 | } 46 | // ---- 47 | import {inject} from "mobx-react-inject" 48 | class MyComponent extends React.Component<{}, void> { 49 | 50 | @inject 51 | private myStore: MyStore 52 | 53 | render() { 54 | return {this.myStore.hello()} 55 | } 56 | } 57 | ``` 58 | ### Store-to-store injection 59 | ```ts 60 | class MyStoreDep { 61 | public word() { 62 | return "word" 63 | } 64 | } 65 | class MyStore { 66 | 67 | constructor(@inject private myStoreDep: MyStoreDep) 68 | 69 | public hello() { 70 | return `Hello ${this.myStoreDep.word()}` 71 | } 72 | } 73 | ``` 74 | ### Nested store providers 75 | ```ts 76 | 77 | import {StoreConstructor, StoreInstance} from "mobx-react-inject" 78 | 79 | class App extends React.Component<{}, void> { 80 | 81 | private stores: Map 82 | 83 | componentWillMount() { 84 | this.stores = new Map([ 85 | [MyStore, new MyStore()] 86 | ]) 87 | } 88 | 89 | render() { 90 | return 91 | 92 | ... 93 | 94 | 95 | } 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function inject(target: any, propertyName: string, propertyIndex?: number): any 4 | 5 | type StoreConstructor = new (...args: any[]) => I 6 | 7 | interface StoreContainerInstance { 8 | has(constructor: StoreConstructor): boolean 9 | get(constructor: StoreConstructor): I 10 | resolve(constructor: StoreConstructor): I 11 | } 12 | 13 | interface StoreContainerConstructor { 14 | new (stores?: Iterable<[StoreConstructor, I]>, parentStore?: StoreContainerInstance): StoreContainerInstance 15 | } 16 | 17 | export const StoreContainer: StoreContainerConstructor 18 | 19 | export interface StoreProviderProps { 20 | storeContainer?: StoreContainerInstance 21 | stores?: Iterable<[StoreConstructor, any]> 22 | } 23 | 24 | export class StoreProvider extends React.Component {} 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx-react-inject", 3 | "version": "0.0.3", 4 | "author": "Pavel Sokolov", 5 | "license": "MIT", 6 | "description": "", 7 | "main": "./lib", 8 | "typings": "index", 9 | "scripts": { 10 | "build": "webpack", 11 | "test": "webpack --config webpack.test.config.js && mocha test-build/index.js" 12 | }, 13 | "keywords": [ 14 | "mobx", 15 | "typescript", 16 | "di", 17 | "inject", 18 | "react", 19 | "reactjs" 20 | ], 21 | "devDependencies": { 22 | "@types/mocha": "^2.2.38", 23 | "@types/power-assert": "^1.4.29", 24 | "@types/react": "^0.14.55", 25 | "@types/react-dom": "^0.14.19", 26 | "@types/reflect-metadata": "0.0.5", 27 | "babel-core": "^6.22.1", 28 | "babel-loader": "^6.2.10", 29 | "babel-preset-es2015": "^6.22.0", 30 | "babel-preset-latest": "^6.22.0", 31 | "babel-preset-react": "^6.22.0", 32 | "babel-preset-stage-0": "^6.22.0", 33 | "mocha": "^3.2.0", 34 | "power-assert": "^1.4.2", 35 | "react": "^15.4.1", 36 | "react-dom": "^15.4.1", 37 | "ts-loader": "^2.0.0", 38 | "tslint": "^4.1.1", 39 | "typescript": "^2.1.4", 40 | "webpack": "^1.14.0", 41 | "webpack-node-externals": "^1.5.4", 42 | "reflect-metadata": "^0.1.9" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/StoreContainer.ts: -------------------------------------------------------------------------------- 1 | import {StoreConstructor} from "../index" 2 | import {resolveDependencies} from "./utils" 3 | 4 | export class StoreContainer { 5 | 6 | private map: Map, any> 7 | 8 | private parentStore: StoreContainer 9 | 10 | public constructor(stores: Iterable<[StoreConstructor, any]> = [], parentStore?: StoreContainer) { 11 | this.map = new Map(stores) 12 | this.map.set(StoreContainer, this) 13 | if (parentStore) { 14 | this.parentStore = parentStore 15 | } 16 | } 17 | 18 | private hasInParentStore(constructor: StoreConstructor) { 19 | return this.parentStore && this.parentStore.has(constructor) 20 | } 21 | 22 | public has(constructor: StoreConstructor) { 23 | return this.map.has(constructor) || this.hasInParentStore(constructor) 24 | } 25 | 26 | public get(constructor: StoreConstructor) { 27 | if (this.hasInParentStore(constructor)) { 28 | return this.parentStore.get(constructor) 29 | } 30 | 31 | if (!this.map.has(constructor)) { 32 | this.map.set(constructor, this.resolve(constructor)) 33 | } 34 | 35 | return this.map.get(constructor) 36 | } 37 | 38 | public resolve(constructor: StoreConstructor, ...args: any[]) { 39 | const resolvedDependencies = resolveDependencies(constructor, this).map((dependency) => this.get(dependency)) 40 | return new constructor(...resolvedDependencies, ...args) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/StoreProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import {StoreContainerInstance, StoreProviderProps} from "../index" 3 | import {StoreContainer} from "./StoreContainer" 4 | 5 | export class StoreProvider extends React.Component { 6 | 7 | public static defaultProps = { 8 | stores: [], 9 | } 10 | 11 | public static contextTypes = { 12 | storeContainer: React.PropTypes.instanceOf(StoreContainer), 13 | } 14 | 15 | public static childContextTypes = { 16 | storeContainer: React.PropTypes.instanceOf(StoreContainer).isRequired, 17 | } 18 | 19 | private storeContainer: StoreContainerInstance 20 | 21 | public componentWillMount() { 22 | this.storeContainer = this.props.storeContainer 23 | ? this.props.storeContainer 24 | : new StoreContainer(this.props.stores, this.context.storeContainer) 25 | } 26 | 27 | public getChildContext() { 28 | return {storeContainer: this.storeContainer} 29 | } 30 | 31 | public render() { 32 | return React.Children.only(this.props.children) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {StoreContainer} from "./StoreContainer" 2 | export {StoreProvider} from "./StoreProvider" 3 | export {inject} from "./inject" 4 | -------------------------------------------------------------------------------- /src/inject.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import {StoreContainer} from "./StoreContainer" 3 | import {ArgumentPositions, checkValidDependency, metadataKey, throwError} from "./utils" 4 | 5 | function propertyDecorator(target: any, propertyName: string) { 6 | if (!(target instanceof React.Component)) { 7 | throwError("Injection store can implement only in React.Component", target) 8 | } 9 | const targetConstructor = target.constructor 10 | const storeConstructor = Reflect.getMetadata("design:type", target, propertyName) 11 | 12 | checkValidDependency(target, storeConstructor) 13 | 14 | if (targetConstructor.contextTypes == null) { 15 | targetConstructor.contextTypes = {} 16 | } 17 | 18 | if (targetConstructor.contextTypes.storeContainer == null) { 19 | targetConstructor.contextTypes.storeContainer = React.PropTypes.instanceOf(StoreContainer).isRequired 20 | } 21 | 22 | Object.defineProperty(target, propertyName, { 23 | get() { 24 | return this.context.storeContainer.get(storeConstructor) 25 | }, 26 | }) 27 | } 28 | 29 | function parameterDecorator(target: any, parameterIndex: number) { 30 | const injectParameters: ArgumentPositions = Reflect.getMetadata(metadataKey, target) || [] 31 | const parametersTypes = Reflect.getMetadata("design:paramtypes", target) 32 | checkValidDependency(target, parametersTypes[parameterIndex]) 33 | injectParameters.push(parameterIndex) 34 | Reflect.defineMetadata(metadataKey, injectParameters, target) 35 | } 36 | 37 | export function inject(target: any, propertyName: string, propertyIndex?: number): any { 38 | if (propertyName && propertyIndex === void 0) { 39 | return propertyDecorator(target, propertyName) 40 | } else if (!propertyName && propertyIndex !== void 0) { 41 | return parameterDecorator(target, propertyIndex) 42 | } 43 | throwError("Decorator is to be applied to property in React.Component, or to a store constructor argument", target) 44 | } 45 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {StoreConstructor, StoreContainerInstance} from "../index" 2 | 3 | export type ArgumentPositions = number[] 4 | 5 | const nativeExp = /\{\s*\[native code\]\s*\}/ 6 | 7 | export const metadataKey = Symbol() 8 | 9 | export function resolveDependencies( 10 | constructor: StoreConstructor, 11 | container: StoreContainerInstance, 12 | parentDependencies = new Set>(), 13 | ) { 14 | const argumentPositions: ArgumentPositions = Reflect.getOwnMetadata(metadataKey, constructor) || [] 15 | const constructorDependencies: any[] = Reflect.getMetadata("design:paramtypes", constructor) || [] 16 | const resolvedDependencies = new Array(constructorDependencies.length) 17 | 18 | parentDependencies.add(constructor) 19 | 20 | argumentPositions.forEach((position) => { 21 | const dependency = constructorDependencies[position] 22 | 23 | if (!container.has(dependency)) { 24 | detectCircularDependencies(parentDependencies, dependency) 25 | resolveDependencies(dependency, container, parentDependencies) 26 | } 27 | 28 | resolvedDependencies[position] = dependency 29 | }) 30 | 31 | parentDependencies.delete(constructor) 32 | 33 | return resolvedDependencies 34 | } 35 | 36 | function isNative(fn: Function) { 37 | return nativeExp.test("" + fn) 38 | } 39 | 40 | export function throwError(message: string, target?: any) { 41 | throw new Error(`${message}.${target ? ` Error occurred in ${target.name}` : ""}`) 42 | } 43 | 44 | export function detectCircularDependencies(dependencies: Set>, constructor: StoreConstructor) { 45 | if (dependencies.has(constructor)) { 46 | const chains = Array.from(dependencies.values()).map((dependency) => dependency.name).join(" -> ") 47 | throwError(`Circular dependencies are found in the following chain "${chains}"`) 48 | } 49 | } 50 | 51 | export function checkValidDependency(target: any, dependency: Function) { 52 | if (!dependency || !("constructor" in dependency)) { 53 | throwError("Dependency must have a constructor", target) 54 | } 55 | if (isNative(dependency)) { 56 | throwError("Dependency may not be native implementation", target) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert" 2 | import "reflect-metadata" 3 | import {inject, StoreContainer} from "../src" 4 | 5 | describe("Dependency injection", () => { 6 | class NoDependencyStore {} 7 | 8 | class ValidDependencyStore { 9 | constructor(@inject public validDependencyStore: NoDependencyStore) {} 10 | } 11 | 12 | class CyclicDependencyStore { 13 | constructor(@inject public cyclicDependencyStore: CyclicDependencyStore) {} 14 | } 15 | 16 | it("should create store with valid dependency", () => { 17 | let store: ValidDependencyStore 18 | assert.doesNotThrow(() => { 19 | store = (new StoreContainer()).get(ValidDependencyStore) as ValidDependencyStore 20 | }) 21 | assert(store.validDependencyStore instanceof NoDependencyStore) 22 | }) 23 | 24 | it("should throw exception if inject no valid dependency in store", () => { 25 | assert.throws(() => { 26 | class NoValidDependencyStore { 27 | constructor(@inject public noValidDependencyStore: string) {} 28 | } 29 | }) 30 | }) 31 | 32 | it("should resolve nested store containers", () => { 33 | const storeContainerParent = new StoreContainer() 34 | const storeContainerChild = new StoreContainer([], storeContainerParent) 35 | assert(storeContainerChild.get(NoDependencyStore) !== storeContainerParent.get(NoDependencyStore)) 36 | assert(storeContainerParent.get(ValidDependencyStore) === storeContainerChild.get(ValidDependencyStore)) 37 | }) 38 | 39 | it("should resolve new instance store", () => { 40 | const storeContainer = new StoreContainer() 41 | const store = storeContainer.get(NoDependencyStore) 42 | const storeResolved = storeContainer.resolve(NoDependencyStore) 43 | assert(store !== storeResolved) 44 | }) 45 | 46 | it("should resolve dependencies with correct sequence", () => { 47 | class CorrectDependencyStore { 48 | public noDependencyStore: NoDependencyStore 49 | public validDependencyStore: ValidDependencyStore 50 | constructor(@inject noDependencyStore: NoDependencyStore, @inject validDependencyStore: ValidDependencyStore){ 51 | this.noDependencyStore = noDependencyStore 52 | this.validDependencyStore = validDependencyStore 53 | } 54 | } 55 | 56 | const storeContainer = new StoreContainer() 57 | const store = storeContainer.get(CorrectDependencyStore) 58 | assert(store.noDependencyStore instanceof NoDependencyStore) 59 | assert(store.validDependencyStore instanceof ValidDependencyStore) 60 | }) 61 | 62 | it("should throw exception if inject dependency in property store", () => { 63 | assert.throws(() => { 64 | class NoConstructorDependencyStore { 65 | @inject 66 | public noConstructorDependencyStore: NoConstructorDependencyStore 67 | } 68 | }) 69 | }) 70 | 71 | it("should throw exception when detect cyclic dependency", () => { 72 | assert.throws(() => { 73 | (new StoreContainer()).get(CyclicDependencyStore) 74 | }) 75 | }) 76 | 77 | it("should create store with the substitution of dependency", () => { 78 | const storeContainer = new StoreContainer([[NoDependencyStore, new Date()]]) 79 | const store = storeContainer.resolve(ValidDependencyStore) 80 | assert(store.validDependencyStore instanceof Date) 81 | }) 82 | 83 | }) 84 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "sourceMap": false, 5 | "module": "commonjs", 6 | "noImplicitReturns": true, 7 | "noImplicitUseStrict": true, 8 | "noImplicitAny": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "es2015", "es2016", "es2017"] 13 | }, 14 | "files": [ 15 | "index.ts", 16 | "../index.d.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "sourceMap": false, 5 | "module": "commonjs", 6 | "removeComments": true, 7 | "strictNullChecks": true, 8 | "noImplicitReturns": true, 9 | "noImplicitUseStrict": true, 10 | "noImplicitAny": false, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "jsx": "preserve", 14 | "lib": ["dom", "es2015", "es2016", "es2017"] 15 | }, 16 | "files": [ 17 | "src/index.ts", 18 | "index.d.ts" 19 | ] 20 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "interface-name": false, 5 | "max-line-length": [true, 140], 6 | "member-ordering": false, 7 | "object-literal-sort-keys": false, 8 | "no-console": false, 9 | "no-default-export": true, 10 | "no-string-literal": false, 11 | "no-var-requires": false, 12 | "no-namespace": false, 13 | "semicolon": [true, "never"], 14 | "max-classes-per-file": false, 15 | "array-type": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | module.exports = { 5 | entry: { 6 | main: './src/index' 7 | }, 8 | output: { 9 | filename: './lib/index.js', 10 | libraryTarget: "umd", 11 | umdNamedDefine: true 12 | }, 13 | resolve: { 14 | extensions: ['', '.ts', '.tsx', '.js', '.jsx'], 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.tsx?$/, 20 | loaders: [ 21 | 'babel-loader', 22 | 'ts-loader' 23 | ] 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.optimize.DedupePlugin() 29 | ], 30 | externals: [nodeExternals()] 31 | } -------------------------------------------------------------------------------- /webpack.test.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | module.exports = { 5 | target: 'node', 6 | entry: { 7 | main: './test/index' 8 | }, 9 | output: { 10 | filename: './test-build/index.js' 11 | }, 12 | resolve: { 13 | extensions: ['', '.ts', '.tsx', '.js', '.jsx'], 14 | modulesDirectories: ["node_modules", "bower_components"], 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.tsx?$/, 20 | loaders: [ 21 | 'babel-loader', 22 | 'ts-loader' 23 | ], 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.optimize.DedupePlugin() 29 | ], 30 | externals: [nodeExternals()] 31 | } --------------------------------------------------------------------------------