├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets └── logo.png ├── examples └── router │ ├── .babelrc │ ├── .gitignore │ ├── devServer │ ├── devServer.js │ └── index.js │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.js │ ├── Home.js │ ├── InjectableHeader.js │ ├── PageOne.js │ ├── PageTwo.js │ └── index.js │ └── webpack.config.babel.js ├── lib └── react-injectables.js ├── package.json ├── src ├── Injectable.js ├── InjectablesProvider.js ├── Injector.js ├── index.js └── utils.js ├── test ├── Injectable.test.js ├── InjectablesProvider.test.js ├── Injector.test.js ├── integration.test.js ├── jsdom.js └── setup.js ├── wallaby.conf.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-1", "react" ], 3 | "ignore": [ 4 | "/node_modules/" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "mocha": true, 8 | "node": true 9 | }, 10 | "ecmaFeatures": { 11 | "defaultParams": true 12 | }, 13 | "rules": { 14 | "comma-dangle": 0, 15 | "func-names": 0, 16 | "indent": [2, 2, { "SwitchCase": 1 }], 17 | "new-cap": 0, 18 | "no-lone-blocks": 0, 19 | // until we have glob based rules we have to disable this rule, as our 20 | // assert library uses expressions in this format. 21 | // follow - https://github.com/eslint/eslint/issues/3611 22 | "no-unused-expressions": 0, 23 | "prefer-arrow-callback": 0, 24 | "quotes": [2, "backtick", "avoid-escape"], 25 | "space-infix-ops": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Webpack stats file output used for bundle optimisation. 2 | lib/stats.json 3 | 4 | #####=== Node ===##### 5 | 6 | # Logs 7 | logs 8 | *.log 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 32 | node_modules 33 | 34 | # Debug log from npm 35 | npm-debug.log 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | branches: 7 | only: 8 | - master 9 | notifications: 10 | email: false 11 | node_js: 12 | - '4' 13 | before_install: 14 | - npm i -g npm@^3.0.0 15 | before_script: 16 | - npm prune 17 | script: 18 | - npm run test:coverage 19 | - npm run build 20 | after_success: 21 | - npm run report-coverage 22 | - npm run semantic-release 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sean Matheson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 💀 ___DEPRECATED___ 💀 2 | 3 | I have given up on this library. Sorry. 4 | 5 | Good news though, it sounds like portals may become a first class citizen when React Fiber lands. 🎉 6 | 7 | --- 8 | 9 |

10 | 11 |

Explicitly inject Components to any part of your React render tree

12 |

13 | 14 | [![Travis](https://img.shields.io/travis/ctrlplusb/react-injectables.svg?style=flat-square)](https://travis-ci.org/ctrlplusb/react-injectables) 15 | [![npm](https://img.shields.io/npm/v/react-injectables.svg?style=flat-square)](http://npm.im/react-injectables) 16 | [![MIT License](https://img.shields.io/npm/l/react-injectables.svg?style=flat-square)](http://opensource.org/licenses/MIT) 17 | [![Codecov](https://img.shields.io/codecov/c/github/ctrlplusb/react-injectables.svg?style=flat-square)](https://codecov.io/github/ctrlplusb/react-injectables) 18 | [![Maintenance](https://img.shields.io/maintenance/yes/2016.svg?style=flat-square)]() 19 | 20 | * Uses React's natural render cycles, no DOM hacks. 21 | * Injections are handled synchronously, no double renders. 22 | * Supports props pass through to injected Components - behaviour++. 23 | * No magic strings in your code. Explicitly define source and target Components. 24 | * Works with React 0.14 and 15. 25 | * Extensive test coverage. 26 | * Micro library. Gzip it to nothingness. 27 | 28 | _Note: There have been a lot of releases recently, however, based on usage within my production cases I am happy with where the API is. Please consider the API in a long term stable condition. I shall make every attempt from now on to avoid any breaking changes._ 29 | 30 | ## What is this for? 31 | 32 | Content Placeholders, Modals, etc. 33 | 34 | ## Overview 35 | 36 | Envision you have the following component tree: 37 | 38 | ```html 39 | 40 | 41 |
42 |
43 | {renderedRouteContent} 44 |
46 |
47 | ``` 48 | 49 | A fairly standard configuration, essentially you have a master application template which each of your routes get rendered in to. This is handy as you get to share things like your Header, Menu, Footer across all your rendered routes without having to repeat all that code. But what if you wanted to extend your base template with additional content that is specific to one of the routes being rendered? 50 | 51 | For example, you'll notice the base template holds a handy little `Sidebar` component. Perhaps you would like a `MyBasketSummary` to be rendered in there whilst the user is viewing the `ProductsRoute`? Or maybe you would like to inject a new `Button` or `Image` into the `Header` for one of your routes? 52 | 53 | How could you solve these seemingly simple problems? 54 | 55 | One option would be to use react-routers native capability to pass down multiple named components for each of the routes into the base template. For the simple cases we recommend this approach, however, with complex applications having deeply nested routes and component structures this approach may be difficult to manage and could end up in complex props pass-throughs. 56 | 57 | Enter Injectables. 58 | 59 | Injectables aims to provide you with a mechanism to explicity define `Injectable` and `Injector` Components. An `Injector` produces a Component that gets injected into an `Injectable`. 60 | 61 | With Injectables you can easily inject a Component into the `Sidebar` when your `ProductPage` Component gets mounted. Here is a partial example of this: 62 | 63 | ```javascript 64 | import { SidebarInjector } from '../InjectableSidebar'; 65 | import MyBasket from '../MyBasket'; 66 | 67 | const MyBasketSidebarInjection = SidebarInjector(MyBasket); 68 | 69 | class ProductPage extends Component { 70 | render() { 71 | return ( 72 |
73 | {/* MyBasket will get injected into Sidebar! */} 74 | 75 | 76 |

Product Page

77 | ... 78 |
79 | ); 80 | } 81 | } 82 | 83 | export default ProductPage; 84 | ``` 85 | 86 | Now every time the `ProductPage` Component is mounted the `MyBasket` Component will be injected into `Sidebar` Component. Ok, there is a bit of additional setup required in the `Sidebar` Component, but the above is a basic overview of how easy it is to define your injections after the initial configuration. 87 | 88 | Yes, a bit of fairy dust is involved, but we attempt to keep things as un-magical as possible, pushing for explictness whilst maintaining ease of use. 89 | 90 | 91 | ## Usage 92 | 93 | First install the library. 94 | 95 | npm install react-injectables 96 | 97 | ### Quick Start 98 | 99 | To get going there are only 3 steps: 100 | 101 | 1. Wrap your application with our `InjectablesProvider`. 102 | 2. Wrap a Component you would like to _receive_ injected content with our `Injectable` helper. e.g. `Injectable(MainScreen)` 103 | 4. Wrap a Component you would like to _inject_ with our `Injector` helper. e.g.: `Injector({ into: MainScreen })(MyModal)` 104 | 105 | ### Full Tutorial 106 | 107 | Ok, here's a more detailed run through with example code. 108 | 109 | __Step 1__ 110 | 111 | Somewhere very low down in your application wrap your component tree with our `InjectablesProvider`. This is the engine that will do all the heavy lifting for you. For example: 112 | 113 | ```javascript 114 | import React from 'react'; 115 | import ReactDOM from 'react-dom'; 116 | import { InjectablesProvider } from 'react-injectables'; 117 | 118 | ReactDOM.render(( 119 | 120 | 121 | ... 122 | 123 | 124 | ), document.getElementById('app')); 125 | ``` 126 | 127 | __Step 2__ 128 | 129 | Now you need to create an `Injectable` Component. Let's say that you would like to create a `Sidebar` component that you could inject in to. You would do the following: 130 | 131 | ```javascript 132 | import React, { PropTypes } from 'react'; 133 | import { Injectable, Injector } from 'react-injectables'; 134 | 135 | // Note the 'injections' prop. This will contain any injected elements. 136 | function Sidebar({ injections }) { 137 | return ( 138 |
139 | {injections} 140 |
141 | ); 142 | } 143 | 144 | // We wrap our Sidebar component with Injectable. This does all the wiring up for us. 145 | const InjectableSidebar = Injectable(Sidebar); 146 | 147 | // Create a default Injector configuration for our injectable sidebar. 148 | // Our Components can use this helper to create injections for the sidebar. 149 | // NOTE: We are exporting this helper, it will come in handy for the next step. 150 | export const SidebarInjector = Injector({ into: InjectableSidebar }); 151 | 152 | export default InjectableSidebar; 153 | ``` 154 | 155 | Notice we also create a default `Injector` configuration for our `InjectableSidebar` called `SidebarInjector`. This has been exported to allow our other Components to easily import and use this pre-configured helper - it saves us having to repeat this configuration thereby reducing errors. 156 | 157 | We recommend naming your component files appropriately to indicate that it is indeed an injectable component. In the above case we named our component file as `InjectableSidebar.js`. Forming your own conventions around the naming of your injectables and injectors will help. 158 | 159 | __Step 3__ 160 | 161 | Ok, so now you have an `InjectableSidebar` Component and a `SidebarInjector` helper function. Next up you need to declare a Component that will cause an injection into the Sidebar to occur. 162 | 163 | ```javascript 164 | import React from 'react'; 165 | import { SidebarInjector } from './InjectableSidebar'; 166 | import MyBasketView from './MyBasketView'; 167 | 168 | // Use the SidebarInjector helper to create a Component that will inject the 169 | // MyBasketView Component into our InjectableSidebar Component. 170 | const MyBasketViewSidebarInjection = SidebarInjector(MyBasketView); 171 | 172 | class ProductPage extends Component { 173 | .... 174 | 175 | render() { 176 | return ( 177 |
178 | {/* The injection happens here, i.e. when the ProductPage gets mounted. 179 | Nothing actually gets rendered at this location, the Component gets sent to 180 | our target Injectable. In this case it means that MyBasketView will 181 | be injected into the Sidebar. 182 | Notice how you can also pass down props into your injected component too. */} 183 | 184 | 185 |

Product Page

186 | 187 | ... 188 |
189 | ); 190 | } 191 | } 192 | 193 | export default ProductPage; 194 | ``` 195 | 196 | And that's it. Any time the `ProductPage` is mounted it will inject the `MyBasketView` Component into the `Sidebar`. When the `ProductPage` unmounts, it's respective injected Component will be removed from the `Sidebar`. 197 | 198 | As you can see it's all explicit, so you can follow the import references to find out any relationships. 199 | 200 | ## Properties of Injectables and Injectors 201 | 202 | Here are a few basic properties you should be aware of: 203 | 204 | * All injection happens within the initial render cycle by react-dom. Injection does not cause a double render to occur on your `Injectable`s. This is a result of us trying to intentionally keep injection as "input to output" as possible. 205 | 206 | * You can have multiple instances of an `Injectable` rendered in your app. They will all recieve the same injected content from their respective `Injector`s. 207 | 208 | * You can create multiple `Injector`s Components targetting the same `Injectable` component. For example, you may want to pass in action buttons from different components into an InjectableActions component. 209 | 210 | * If an Component that is hosting `Injector` is unmounted then the injected Components will automatically be removed from the `Injectable` target. 211 | 212 | * Any new `Injector`s that are rendered into the tree will automatically have their injected Components passed into any existing `Injectable` targets. i.e. a props update. 213 | 214 | * `Injector`s are allowed to be mounted before any `Injectable`s. Once their target `Injectable` Component is mounted then any Components from existing `Injector`s will automatically be passed into the newly mounted `Injectable`. 215 | 216 | ## Examples 217 | 218 | At the moment there is only one example, using react-router. Check out the examples folder. I wouldn't recommend running it yet as I have yet to add any style to it, but it will execute if you try. :) 219 | 220 | 221 | ## Some other considerations. 222 | 223 | __I am using redux or another flux-like library__ 224 | 225 | Then perhaps you should try and use their respective action flows in order to control the "injection" of your content in a manner that follows their uni-directional flows. 226 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/react-injectables/092c912b99bf84a5387e77d81d6eb074267e8586/assets/logo.png -------------------------------------------------------------------------------- /examples/router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-1", "react" ], 3 | "ignore": [ 4 | "/node_modules/" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/router/.gitignore: -------------------------------------------------------------------------------- 1 | # Output folder for our webpack build. 2 | lib/ 3 | -------------------------------------------------------------------------------- /examples/router/devServer/devServer.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import webpack from 'webpack'; 4 | import config from '../webpack.config.babel'; 5 | 6 | const server = express(); 7 | const compiler = webpack(config); 8 | 9 | server.use(require(`webpack-dev-middleware`)(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | server.use(require(`webpack-hot-middleware`)(compiler)); 15 | 16 | server.get(`*`, (req, res) => { 17 | res.sendFile(path.resolve(__dirname, `../public/index.html`)); 18 | }); 19 | 20 | server.listen(3002, `localhost`, (err) => { 21 | if (err) { 22 | console.log(err); // eslint-disable-line no-console 23 | return; 24 | } 25 | 26 | console.log(`Listening at http://localhost:3002`); // eslint-disable-line no-console 27 | }); 28 | -------------------------------------------------------------------------------- /examples/router/devServer/index.js: -------------------------------------------------------------------------------- 1 | require(`babel-register`); 2 | require(`./devServer`); 3 | -------------------------------------------------------------------------------- /examples/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-injectables-examples-router", 3 | "version": "1.0.0", 4 | "description": "An example of using react-injectables with react-router.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "watch": "node ./devServer" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "babel-cli": "6.7.5", 13 | "babel-core": "6.7.6", 14 | "babel-loader": "6.2.4", 15 | "babel-preset-es2015": "6.6.0", 16 | "babel-preset-react": "6.5.0", 17 | "babel-preset-stage-1": "6.5.0", 18 | "babel-register": "6.7.2", 19 | "express": "4.13.4", 20 | "react": "15.0.1", 21 | "react-dom": "15.0.1", 22 | "react-router": "2.0.1", 23 | "webpack": "1.12.15", 24 | "webpack-dev-middleware": "1.6.1", 25 | "webpack-hot-middleware": "2.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/router/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Injectables - router example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/router/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import InjectableHeader from './InjectableHeader'; 3 | import { Link } from 'react-router'; 4 | 5 | // Our base application component. Which shall contain our injectable header. 6 | const App = ({ children }) => ( 7 |
8 | 9 |

Injectables Router Example

10 |
11 | {children} 12 |
13 |

14 | Click on the links below to load different pages: 15 |

16 | 21 |
22 | ); 23 | App.propTypes = { 24 | children: PropTypes.any 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /examples/router/src/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Home = () => ( 4 |
5 |

6 | Welcome to the demo page for using Injectables with react router. 7 |

8 |

9 | The index route doesn't do any injections into the header, so you should 10 | see a message indication this fact. 11 |

12 |
13 | ); 14 | 15 | export default Home; 16 | -------------------------------------------------------------------------------- /examples/router/src/InjectableHeader.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Injectable, Injector } from '../../../src/index.js'; 3 | 4 | // Prep a header component which we intend to make injectable. 5 | // Note the prop named 'injections'. This will contain any injected elements. 6 | const Header = ({ injections }) => ( 7 |
8 |

INJECTABLE HEADER

9 |
10 | {injections.length > 0 ? injections :
Nothing has been injected
} 11 |
12 |
13 | ); 14 | Header.propTypes = { 15 | injections: PropTypes.arrayOf(PropTypes.element) 16 | }; 17 | 18 | // Convert our header into an injectable! 19 | const InjectableHeader = Injectable(Header); 20 | 21 | // Create an Injector helper function for our InjectableHeader. 22 | export const HeaderInjector = Injector({ into: InjectableHeader }); 23 | 24 | export default InjectableHeader; 25 | -------------------------------------------------------------------------------- /examples/router/src/PageOne.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { HeaderInjector } from './InjectableHeader'; 3 | 4 | // Our component that we will inject. 5 | const InjectMe = ({ active }) => ( 6 |
7 |

Injection from Page One.

8 |

The active prop value is: {active ? `active` : `inactive`}

9 |
10 | ); 11 | InjectMe.propTypes = { 12 | active: PropTypes.bool.isRequired 13 | }; 14 | 15 | // Use the HeaderInjector helper to create an Injection. 16 | const HeaderInjection = HeaderInjector(InjectMe); 17 | 18 | /** 19 | * This is the component that when rendered will cause the injection to occur. 20 | */ 21 | class PageOne extends Component { 22 | state = { 23 | active: false 24 | } 25 | 26 | onClick = () => { 27 | this.setState({ active: !this.state.active }); 28 | } 29 | 30 | render() { 31 | const { active } = this.state; 32 | 33 | return ( 34 |
35 | {/* The injection! Nothing gets rendered here. */} 36 | 37 | 38 |
39 |

I am page one.

40 |

My State is {active ? `active` : `inactive`}

41 |
42 | 43 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | export default PageOne; 50 | -------------------------------------------------------------------------------- /examples/router/src/PageTwo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HeaderInjector } from './InjectableHeader'; 3 | 4 | // Our component that we will inject. 5 | const InjectMe = (props) => ( 6 |
7 |

Injection from Page Two.

8 |

9 | I also recieved these props:
10 | {Object.keys(props).join(`, `)} 11 |

12 |
13 | ); 14 | 15 | // Use the HeaderInjector helper to create an Injection. 16 | const HeaderInjection = HeaderInjector(InjectMe); 17 | 18 | /** 19 | * This is the component that when rendered will cause the injection to occur. 20 | */ 21 | const PageTwo = () => ( 22 |
23 | {/* The injection! Nothing actually gets rendered here, it gets sent to 24 | our target Injectable. */} 25 | 26 | 27 | I am page two. 28 |
29 | ); 30 | 31 | export default PageTwo; 32 | -------------------------------------------------------------------------------- /examples/router/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { InjectablesProvider } from '../../../src/index.js'; 4 | import { browserHistory, Router, Route, IndexRoute } from 'react-router'; 5 | 6 | import App from './App'; 7 | import Home from './Home'; 8 | import PageOne from './PageOne'; 9 | import PageTwo from './PageTwo'; 10 | 11 | const container = document.getElementById(`app`); 12 | 13 | ReactDOM.render(( 14 | 15 | 16 | 17 | 18 | 19 | } /> 20 | 21 | 22 | 23 | ), container); 24 | -------------------------------------------------------------------------------- /examples/router/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | 4 | const config = { 5 | entry: [ 6 | `webpack-hot-middleware/client`, 7 | path.resolve(__dirname, `./src/index.js`) 8 | ], 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | loaders: [`babel-loader`], 14 | include: [ 15 | path.resolve(__dirname, `./src`), 16 | path.resolve(__dirname, `../../src`) 17 | ], 18 | exclude: /node_modules/ 19 | } 20 | ] 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, `./lib`), 24 | filename: `router-example.js`, 25 | publicPath: `/assets/` 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | new webpack.optimize.OccurenceOrderPlugin(), 30 | new webpack.optimize.DedupePlugin(), 31 | new webpack.DefinePlugin({ 32 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || `development`) 33 | }) 34 | ] 35 | }; 36 | 37 | export default config; 38 | -------------------------------------------------------------------------------- /lib/react-injectables.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define("react-injectables",["react"],t):"object"==typeof exports?exports["react-injectables"]=t(require("react")):e["react-injectables"]=t(e.React)}(this,function(e){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0}),t.Injector=t.Injectable=t.InjectablesProvider=void 0;var i=n(5),o=r(i),c=n(4),u=r(c),a=n(6),s=r(a);t.InjectablesProvider=o.default,t.Injectable=u.default,t.Injector=s.default},function(t,n){t.exports=e},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);e.length>t;t++)n[t]=e[t];return n}return Array.from(e)}function o(){for(var e=arguments.length,t=Array(e),n=0;e>n;n++)t[n]=arguments[n];return function(){if(0===t.length)return arguments.length>0?arguments[0]:void 0;var e=t[t.length-1],n=t.slice(0,-1);return n.reduceRight(function(e,t){return t(e)},e.apply(void 0,arguments))}}function c(e){var t=e.children;return a.Children.only(t)}function u(e,t){var n=0;return t.map(function(t){return n++,s.default.createElement(c,{key:e+"_"+n},t)})}Object.defineProperty(t,"__esModule",{value:!0}),t.find=t.map=t.concatAll=t.containsUniq=t.uniqBy=t.withoutAll=t.without=t.all=t.filter=void 0,t.compose=o,t.keyedElements=u;var a=n(1),s=r(a),f=t.filter=function(e){return function(t){return t.filter(e)}},l=t.all=function(e){return function(t){for(var n=0;t.length>n;n++)if(!e(t[n]))return!1;return!0}},j=(t.without=function(e){return function(t){return f(function(t){return!Object.is(t,e)})(t)}},t.withoutAll=function(e){return function(t){return f(function(t){return l(function(e){return!Object.is(t,e)})(e)})(t)}});t.uniqBy=function(e){return function(t){var n=new Set,r=[];return t.forEach(function(t){var i=t[e];n.has(i)||(n.add(i),r.push(t))}),r}},t.containsUniq=function(e){return function(t){return j(e)(t).length>0}},t.concatAll=function(e){return e.reduce(function(e,t){return[].concat(i(e),i(t))},[])},t.map=function(e){return function(t){return t.map(e)}},t.find=function(e){return function(t){return t.find(e)}}},function(e,t,n){"use strict";var r=function(e,t,n,r,i,o,c,u){if(!e){var a;if(void 0===t)a=Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var s=[n,r,i,o,c,u],f=0;a=Error(t.replace(/%s/g,function(){return s[f++]})),a.name="Invariant Violation"}throw a.framesToPop=1,a}};e.exports=r},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function c(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},a=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),s=n(1),f=r(s),l=n(2),j=0,p=function(e){j++;var t="injectionId_"+j,n=function(n){function r(){var e,t,n,c;i(this,r);for(var u=arguments.length,a=Array(u),s=0;u>s;s++)a[s]=arguments[s];return t=n=o(this,(e=Object.getPrototypeOf(r)).call.apply(e,[this].concat(a))),n.state={injections:[]},n.consume=function(e){(e.length!==n.state.injections.length||(0,l.containsUniq)(n.state.injections,e))&&n.setState({injections:e})},c=t,o(n,c)}return c(r,n),a(r,[{key:"componentWillMount",value:function(){var e=this;this.context.registerInjectable({injectionId:t,injectableInstance:this,receive:function(t){return e.consume(t)}})}},{key:"componentWillUnmount",value:function(){this.context.removeInjectable({injectionId:t,injectableInstance:this})}},{key:"render",value:function(){var t=(0,l.keyedElements)("injections",this.state.injections);return f.default.createElement(e,u({injections:t},this.props))}}]),r}(s.Component);return n.injectionId=t,n.contextTypes={registerInjectable:s.PropTypes.func.isRequired,removeInjectable:s.PropTypes.func.isRequired},n};t.default=p},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);e.length>t;t++)n[t]=e[t];return n}return Array.from(e)}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function c(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function u(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),s=n(1),f=n(2),l=n(3),j=r(l),p=function(e){function t(e,n){o(this,t);var r=c(this,Object.getPrototypeOf(t).call(this,e,n));return r.registrations=[],r}return u(t,e),a(t,[{key:"getChildContext",value:function(){var e=this;return{registerInjector:function(t){return e.registerInjector(t)},removeInjector:function(t){return e.removeInjector(t)},updateInjector:function(t){return e.updateInjector(t)},registerInjectable:function(t){return e.registerInjectable(t)},removeInjectable:function(t){return e.removeInjectable(t)}}}},{key:"getRegistration",value:function(e){var t=e.injectionId,n=(0,f.find)(function(e){return e.injectionId===t})(this.registrations);return n||(n={injectionId:t,injectables:[],injections:[]},this.registrations.push(n)),n}},{key:"runInjections",value:function(e){var t=e.registration,n=t.injectables,r=t.injections,i=(0,f.compose)((0,f.filter)(function(e){return null!==e&&void 0!==e}),(0,f.map)(function(e){return e.inject()}),(0,f.uniqBy)("injectorId"))(r);n.forEach(function(e){e.receive(i)})}},{key:"removeRegistration",value:function(e){var t=e.registration;this.registrations=(0,f.without)(t)(this.registrations)}},{key:"findInjectable",value:function(e){var t=e.registration,n=e.injectableInstance,r=function(e){return Object.is(e.instance,n)};return(0,f.find)(r)(t.injectables)}},{key:"clearRegistrationIfEmpty",value:function(e){var t=e.registration;0===t.injectables.length&&0===t.injections.length&&this.removeRegistration({registration:t})}},{key:"registerInjectable",value:function(e){var t=e.injectionId,n=e.injectableInstance,r=e.receive,o=this.getRegistration({injectionId:t}),c=this.findInjectable({registration:o,injectableInstance:n});if((0,f.withoutAll)(o.injectables)([c]).length>0){var u={instance:n,receive:r};o.injectables=[].concat(i(o.injectables),[u]),this.runInjections({registration:o})}}},{key:"removeInjectable",value:function(e){var t=e.injectionId,n=e.injectableInstance,r=this.getRegistration({injectionId:t}),i=this.findInjectable({registration:r,injectableInstance:n});r.injectables=(0,f.without)(i)(r.injectables),this.clearRegistrationIfEmpty({registration:r})}},{key:"findInjection",value:function(e){var t=e.registration,n=e.injectorInstance,r=function(e){return Object.is(e.instance,n)};return(0,f.find)(r)(t.injections)}},{key:"findInjector",value:function(e){var t=e.registration,n=e.injectorId,r=function(e){return e.injectorId===n};return(0,f.find)(r)(t.injections)}},{key:"registerInjector",value:function(e){var t=e.injectionId,n=e.injectorId,r=e.injectorInstance,o=e.inject,c=this.getRegistration({injectionId:t}),u=this.findInjection({registration:c,injectorInstance:r});(0,j.default)(!u,"An Injector instance is being registered multiple times.");var a={injectorId:n,instance:r,inject:o};c.injections=[].concat(i(c.injections),[a]),this.runInjections({registration:c})}},{key:"updateInjector",value:function(e){var t=e.injectionId,n=e.injectorInstance,r=e.inject,i=this.getRegistration({injectionId:t}),o=this.findInjection({registration:i,injectorInstance:n});(0,j.default)(o,"Trying to update an Injector that is not registered"),o.inject=r,this.runInjections({registration:i})}},{key:"removeInjector",value:function(e){var t=e.injectionId,n=e.injectorInstance,r=this.getRegistration({injectionId:t}),i=this.findInjection({registration:r,injectorInstance:n});(0,j.default)(!!i,"Trying to remove an injector which has not been registered"),r.injections=(0,f.without)(i)(r.injections),this.runInjections({registration:r}),this.clearRegistrationIfEmpty({registration:r})}},{key:"render",value:function(){return s.Children.only(this.props.children)}}]),t}(s.Component);p.childContextTypes={registerInjector:s.PropTypes.func.isRequired,removeInjector:s.PropTypes.func.isRequired,updateInjector:s.PropTypes.func.isRequired,registerInjectable:s.PropTypes.func.isRequired,removeInjectable:s.PropTypes.func.isRequired},p.propTypes={children:s.PropTypes.element},t.default=p},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function c(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),a=n(1),s=r(a),f=n(3),l=r(f),j="Invalid Injectable target. Please provide a Component that has been wrapped Injectable wrapped Component.",p="Invalid injection value provided into Injector. You must supply a Component or stateless Component.",d=0,v=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.into;(0,l.default)(t&&"function"==typeof t&&t.injectionId&&t.contextTypes&&t.contextTypes.registerInjectable&&t.contextTypes.removeInjectable,j),d++;var n="injector_"+d;return function(e){(0,l.default)(e&&"function"==typeof e,p);var r=function(r){function a(){return i(this,a),o(this,Object.getPrototypeOf(a).apply(this,arguments))}return c(a,r),u(a,[{key:"componentWillMount",value:function(){var r=this;this.context.registerInjector({injectionId:t.injectionId,injectorId:n,injectorInstance:this,inject:function(){return s.default.createElement(e,r.props)}})}},{key:"componentWillUpdate",value:function(r){this.context.updateInjector({injectionId:t.injectionId,injectorId:n,injectorInstance:this,inject:function(){return s.default.createElement(e,r)}})}},{key:"componentWillUnmount",value:function(){this.context.removeInjector({injectionId:t.injectionId,injectorId:n,injectorInstance:this})}},{key:"render",value:function(){return null}}]),a}(a.Component);return r.contextTypes={registerInjector:a.PropTypes.func.isRequired,updateInjector:a.PropTypes.func.isRequired,removeInjector:a.PropTypes.func.isRequired},r}};t.default=v}])}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-injectables", 3 | "version": "1.0.0", 4 | "description": "Explicitly inject Components into any part of your React render tree.", 5 | "main": "lib/react-injectables.js", 6 | "scripts": { 7 | "prebuild": "rm -rf lib && mkdir lib", 8 | "build": "NODE_ENV=production webpack", 9 | "commit": "git-cz", 10 | "test": "mocha --compilers js:babel-register --recursive --require ./test/setup.js \"test/**/*.test.js\"", 11 | "test:coverage": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha -- -R spec --require ./test/setup.js", 12 | "report-coverage": "cat ./coverage/lcov.info | $(npm bin)/codecov", 13 | "lint": "eslint src", 14 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/ctrlplusb/react-injectables.git" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "reactjs", 23 | "injectables", 24 | "portals", 25 | "elements" 26 | ], 27 | "author": "Sean Matheson (http://ctrlplusb.com)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/ctrlplusb/react-injectables/issues" 31 | }, 32 | "homepage": "https://github.com/ctrlplusb/react-injectables#readme", 33 | "dependencies": { 34 | "invariant": "2.2.1" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "6.7.5", 38 | "babel-core": "6.7.6", 39 | "babel-eslint": "6.0.2", 40 | "babel-loader": "6.2.4", 41 | "babel-preset-es2015": "6.6.0", 42 | "babel-preset-react": "6.5.0", 43 | "babel-preset-stage-1": "6.5.0", 44 | "babel-register": "6.7.2", 45 | "chai": "3.5.0", 46 | "codecov.io": "0.1.6", 47 | "commitizen": "2.7.6", 48 | "cz-conventional-changelog": "1.1.5", 49 | "enzyme": "2.2.0", 50 | "eslint": "2.7.0", 51 | "eslint-config-airbnb": "6.2.0", 52 | "eslint-loader": "1.3.0", 53 | "eslint-plugin-mocha": "2.1.0", 54 | "eslint-plugin-react": "4.3.0", 55 | "ghooks": "1.2.0", 56 | "isparta": "4.0.0", 57 | "jsdom": "8.3.1", 58 | "mocha": "2.4.5", 59 | "path": "0.12.7", 60 | "react": "15.0.1", 61 | "react-addons-test-utils": "15.0.1", 62 | "react-dom": "15.0.1", 63 | "semantic-release": "^6.2.1", 64 | "sinon": "1.17.3", 65 | "sinon-chai": "2.8.0", 66 | "stats-webpack-plugin": "0.3.1", 67 | "webpack": "1.12.15" 68 | }, 69 | "peerDependencies": { 70 | "react": "^0.14.0 || ^15.0.0" 71 | }, 72 | "czConfig": { 73 | "path": "node_modules/cz-conventional-changelog" 74 | }, 75 | "config": { 76 | "ghooks": { 77 | "pre-commit": "npm run test" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Injectable.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { containsUniq, keyedElements } from './utils'; 3 | 4 | let injectionIdIndex = 0; 5 | 6 | const Injectable = (VeinComponent) => { 7 | injectionIdIndex++; 8 | const injectionId = `injectionId_${injectionIdIndex}`; 9 | 10 | class InjectableComponent extends Component { 11 | static injectionId = injectionId; 12 | 13 | static contextTypes = { 14 | registerInjectable: PropTypes.func.isRequired, 15 | removeInjectable: PropTypes.func.isRequired 16 | }; 17 | 18 | state = { 19 | injections: [] 20 | } 21 | 22 | componentWillMount() { 23 | this.context.registerInjectable({ 24 | injectionId, 25 | injectableInstance: this, 26 | receive: (elements) => this.consume(elements) 27 | }); 28 | } 29 | 30 | componentWillUnmount() { 31 | this.context.removeInjectable({ 32 | injectionId, 33 | injectableInstance: this 34 | }); 35 | } 36 | 37 | consume = (elements) => { 38 | if (elements.length !== this.state.injections.length || 39 | containsUniq(this.state.injections, elements)) { 40 | this.setState({ injections: elements }); 41 | } 42 | } 43 | 44 | render() { 45 | const keyed = keyedElements(`injections`, this.state.injections); 46 | 47 | return ( 48 | 52 | ); 53 | } 54 | } 55 | 56 | return InjectableComponent; 57 | }; 58 | 59 | export default Injectable; 60 | -------------------------------------------------------------------------------- /src/InjectablesProvider.js: -------------------------------------------------------------------------------- 1 | import { Children, Component, PropTypes } from 'react'; 2 | import { compose, find, filter, map, uniqBy, without, withoutAll } from './utils'; 3 | import invariant from 'invariant'; 4 | 5 | class InjectablesProvider extends Component { 6 | static childContextTypes = { 7 | registerInjector: PropTypes.func.isRequired, 8 | removeInjector: PropTypes.func.isRequired, 9 | updateInjector: PropTypes.func.isRequired, 10 | registerInjectable: PropTypes.func.isRequired, 11 | removeInjectable: PropTypes.func.isRequired, 12 | }; 13 | 14 | static propTypes = { 15 | children: PropTypes.element 16 | }; 17 | 18 | constructor(props, context) { 19 | super(props, context); 20 | this.registrations = []; 21 | } 22 | 23 | getChildContext() { 24 | return { 25 | registerInjector: (args) => this.registerInjector(args), 26 | 27 | removeInjector: (args) => this.removeInjector(args), 28 | 29 | updateInjector: (args) => this.updateInjector(args), 30 | 31 | registerInjectable: (args) => this.registerInjectable(args), 32 | 33 | removeInjectable: (args) => this.removeInjectable(args) 34 | }; 35 | } 36 | 37 | getRegistration(args) { 38 | const { injectionId } = args; 39 | 40 | let registration = find( 41 | x => x.injectionId === injectionId 42 | )(this.registrations); 43 | 44 | if (!registration) { 45 | registration = { 46 | injectionId, 47 | injectables: [], 48 | injections: [] 49 | }; 50 | 51 | this.registrations.push(registration); 52 | } 53 | 54 | return registration; 55 | } 56 | 57 | runInjections(args) { 58 | const { registration } = args; 59 | const { injectables, injections } = registration; 60 | 61 | const elements = compose( 62 | filter(x => x !== null && x !== undefined), 63 | map(x => x.inject()), 64 | uniqBy(`injectorId`) 65 | )(injections); 66 | 67 | injectables.forEach(injectable => { 68 | injectable.receive(elements); 69 | }); 70 | } 71 | 72 | removeRegistration(args) { 73 | const { registration } = args; 74 | this.registrations = without(registration)(this.registrations); 75 | } 76 | 77 | findInjectable({ registration, injectableInstance }) { 78 | const isInjectableInstance = x => Object.is(x.instance, injectableInstance); 79 | return find(isInjectableInstance)(registration.injectables); 80 | } 81 | 82 | clearRegistrationIfEmpty({ registration }) { 83 | if (registration.injectables.length === 0 && registration.injections.length === 0) { 84 | this.removeRegistration({ registration }); 85 | } 86 | } 87 | 88 | registerInjectable(args) { 89 | const { injectionId, injectableInstance, receive } = args; 90 | const registration = this.getRegistration({ injectionId }); 91 | const injectable = this.findInjectable( 92 | { registration, injectableInstance }); 93 | 94 | if (withoutAll(registration.injectables)([injectable]).length > 0) { 95 | const newInjectable = { 96 | instance: injectableInstance, 97 | receive 98 | }; 99 | registration.injectables = [...registration.injectables, newInjectable]; 100 | this.runInjections({ registration }); // First time consumption. 101 | } 102 | } 103 | 104 | removeInjectable(args) { 105 | const { injectionId, injectableInstance } = args; 106 | const registration = this.getRegistration({ injectionId }); 107 | const injectable = this.findInjectable( 108 | { registration, injectableInstance }); 109 | 110 | registration.injectables = without(injectable)(registration.injectables); 111 | 112 | this.clearRegistrationIfEmpty({ registration }); 113 | } 114 | 115 | findInjection({ registration, injectorInstance }) { 116 | const isInjectorInstance = x => Object.is(x.instance, injectorInstance); 117 | return find(isInjectorInstance)(registration.injections); 118 | } 119 | 120 | findInjector({ registration, injectorId }) { 121 | const isInjectorId = x => x.injectorId === injectorId; 122 | return find(isInjectorId)(registration.injections); 123 | } 124 | 125 | registerInjector(args) { 126 | const { injectionId, injectorId, injectorInstance, inject } = args; 127 | const registration = this.getRegistration({ injectionId }); 128 | const existingInjection = this.findInjection({ registration, injectorInstance }); 129 | 130 | invariant( 131 | !existingInjection, 132 | `An Injector instance is being registered multiple times.`); 133 | 134 | if (process.env.NODE_ENV !== `production`) { 135 | const existingInjector = this.findInjector({ registration, injectorId }); 136 | 137 | if (existingInjector && console && console.warn) { // eslint-disable-line no-console 138 | console.warn( // eslint-disable-line no-console 139 | `Multiple instances of an Injector has been found. This may not be ` + 140 | `your intended behaviour`); 141 | } 142 | } 143 | 144 | const newInjection = { 145 | injectorId, 146 | instance: injectorInstance, 147 | inject 148 | }; 149 | 150 | registration.injections = [ 151 | ...registration.injections, 152 | newInjection 153 | ]; 154 | 155 | this.runInjections({ registration }); 156 | } 157 | 158 | updateInjector(args) { 159 | const { injectionId, injectorInstance, inject } = args; 160 | const registration = this.getRegistration({ injectionId }); 161 | const existingInjection = this.findInjection({ registration, injectorInstance }); 162 | 163 | invariant( 164 | existingInjection, 165 | `Trying to update an Injector that is not registered`); 166 | 167 | existingInjection.inject = inject; 168 | 169 | this.runInjections({ registration }); 170 | } 171 | 172 | removeInjector(args) { 173 | const { injectionId, injectorInstance } = args; 174 | const registration = this.getRegistration({ injectionId }); 175 | const injection = this.findInjection({ registration, injectorInstance }); 176 | 177 | invariant( 178 | !!injection, 179 | `Trying to remove an injector which has not been registered`); 180 | 181 | registration.injections = without(injection)(registration.injections); 182 | this.runInjections({ registration }); 183 | 184 | this.clearRegistrationIfEmpty({ registration }); 185 | } 186 | 187 | render() { 188 | return ( 189 | Children.only(this.props.children) 190 | ); 191 | } 192 | } 193 | 194 | export default InjectablesProvider; 195 | -------------------------------------------------------------------------------- /src/Injector.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import invariant from 'invariant'; 3 | 4 | const invalidTargetMsg = 5 | `Invalid Injectable target. Please provide a Component that has been wrapped ` + 6 | `Injectable wrapped Component.`; 7 | const invalidInjectMsg = 8 | `Invalid injection value provided into Injector. You must supply a Component ` + 9 | `or stateless Component.`; 10 | 11 | let injectorIndex = 0; 12 | 13 | const Injector = ({ into } = {}) => { 14 | invariant( 15 | into && 16 | typeof into === `function` && 17 | into.injectionId && 18 | into.contextTypes && 19 | into.contextTypes.registerInjectable && 20 | into.contextTypes.removeInjectable, 21 | // Error message 22 | invalidTargetMsg); 23 | 24 | return function WrapComponent(InjectionComponent) { 25 | invariant( 26 | InjectionComponent && 27 | typeof InjectionComponent === `function`, 28 | invalidInjectMsg 29 | ); 30 | 31 | injectorIndex++; 32 | const injectorId = `injector_${injectorIndex}`; 33 | 34 | class InjectorComponent extends Component { 35 | static contextTypes = { 36 | registerInjector: PropTypes.func.isRequired, 37 | updateInjector: PropTypes.func.isRequired, 38 | removeInjector: PropTypes.func.isRequired 39 | }; 40 | 41 | componentWillMount() { 42 | this.context.registerInjector({ 43 | injectionId: into.injectionId, 44 | injectorId, 45 | injectorInstance: this, 46 | inject: () => 47 | }); 48 | } 49 | 50 | componentWillUpdate(nextProps) { 51 | this.context.updateInjector({ 52 | injectionId: into.injectionId, 53 | injectorId, 54 | injectorInstance: this, 55 | inject: () => 56 | }); 57 | } 58 | 59 | componentWillUnmount() { 60 | this.context.removeInjector({ 61 | injectionId: into.injectionId, 62 | injectorId, 63 | injectorInstance: this 64 | }); 65 | } 66 | 67 | render() { 68 | return null; 69 | } 70 | } 71 | 72 | return InjectorComponent; 73 | }; 74 | }; 75 | 76 | export default Injector; 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export InjectablesProvider from './InjectablesProvider'; 2 | export Injectable from './Injectable'; 3 | export Injector from './Injector'; 4 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | 3 | /** 4 | * Composes single-argument functions from right to left. The rightmost 5 | * function can take multiple arguments as it provides the signature for 6 | * the resulting composite function. 7 | * 8 | * @param {...Function} funcs The functions to compose. 9 | * @returns {Function} A function obtained by composing the argument functions 10 | * from right to left. For example, compose(f, g, h) is identical to doing 11 | * (...args) => f(g(h(...args))). 12 | * 13 | * Thank you Dan Abramov for this code! 14 | */ 15 | export function compose(...funcs) { 16 | return (...args) => { 17 | /* istanbul ignore next */ 18 | if (funcs.length === 0) { 19 | return args[0]; 20 | } 21 | 22 | const last = funcs[funcs.length - 1]; 23 | const rest = funcs.slice(0, -1); 24 | 25 | return rest.reduceRight((composed, f) => f(composed), last(...args)); 26 | }; 27 | } 28 | 29 | // :: (a -> boolean) => [a] => [a] 30 | export const filter = f => x => x.filter(f); 31 | 32 | // :: (a -> boolean) => [a] => [a] 33 | export const all = f => x => { 34 | for (let i = 0; i < x.length; i++) { 35 | if (!f(x[i])) return false; 36 | } 37 | 38 | return true; 39 | }; 40 | 41 | // :: a -> [a] -> [a] 42 | export const without = (toRemove) => (point) => 43 | filter((x) => !Object.is(x, toRemove))(point); 44 | 45 | // :: [a] -> [a] -> [a] 46 | export const withoutAll = (toRemove) => (point) => 47 | filter( 48 | (x) => all(y => !Object.is(x, y))(toRemove) 49 | )(point); 50 | 51 | // :: a -> [b] 52 | export const uniqBy = x => y => { 53 | const checked = new Set(); 54 | const result = []; 55 | 56 | y.forEach(a => { 57 | const prop = a[x]; 58 | if (!checked.has(prop)) { 59 | checked.add(prop); 60 | result.push(a); 61 | } 62 | }); 63 | 64 | return result; 65 | }; 66 | 67 | /** 68 | * :: [a] -> [a] -> boolean 69 | * 70 | * Determines if an array, `point`, has any items that is not contained within 71 | * the `toCompare` array. 72 | * 73 | * @param toCompare 74 | * The array to compare against. 75 | * @param point 76 | * The array to check with. 77 | * 78 | * @return 79 | * `true` if and only if `point` has at least one item that isn't 80 | * contained within `toCompare`. 81 | */ 82 | export const containsUniq = (toCompare) => (point) => 83 | withoutAll(toCompare)(point).length > 0; 84 | 85 | // :: [[a]] -> [a] 86 | export const concatAll = x => x.reduce((acc, cur) => [...acc, ...cur], []); 87 | 88 | // :: (a => b) => [a] => [b] 89 | export const map = f => x => x.map(f); 90 | 91 | // :: (a => boolean) => [a] => a|undefined 92 | export const find = f => x => x.find(f); 93 | 94 | function KeyedComponent({ children }) { 95 | return Children.only(children); 96 | } 97 | 98 | // 99 | /** 100 | * :: [Element] -> [Element] 101 | * 102 | * Ensures the given react elements have 'key' properties on them. 103 | * 104 | * @param prefix 105 | * The prefix for the keys. 106 | * @param items 107 | * The react elements. 108 | * 109 | * @return The keyed react elements. 110 | */ 111 | export function keyedElements(prefix : string, items : Array) { 112 | let index = 0; 113 | return items.map(x => { 114 | index++; 115 | return {x}; 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /test/Injectable.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | 3 | import React, { Component } from 'react'; 4 | import { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import { mount } from 'enzyme'; 7 | 8 | describe(`Given the Injector interface`, () => { 9 | const Injectable = require(`../src/Injectable`).default; 10 | 11 | describe(`When creating an Injectable`, () => { 12 | function assertIsValidInjectable(injectableInstance) { 13 | const actual = typeof injectableInstance === `function` && 14 | !!injectableInstance.contextTypes && 15 | !!injectableInstance.contextTypes.registerInjectable && 16 | !!injectableInstance.contextTypes.removeInjectable; 17 | 18 | const expected = true; 19 | 20 | expect(actual).to.equal(expected, `Invalid Injectable created.`); 21 | } 22 | 23 | it(`It should allow use of a stateless component`, () => { 24 | const StatelessComponent = () =>
bar
; 25 | const InjectableBob = Injectable(StatelessComponent); 26 | assertIsValidInjectable(InjectableBob); 27 | }); 28 | 29 | it(`It should allow use of an ES6 class based component`, () => { 30 | class ClassComponent extends Component { 31 | state = { bob: `baz` } 32 | render() { 33 | return
foo
; 34 | } 35 | } 36 | const InjectableBob = Injectable(ClassComponent); 37 | assertIsValidInjectable(InjectableBob); 38 | }); 39 | 40 | it(`It should allow use of a React.createClass based component`, () => { 41 | const CreateClassComponent = 42 | React.createClass({ // eslint-disable-line react/prefer-es6-class 43 | state: { foo: `bar` }, 44 | render() { 45 | return
foo
; 46 | } 47 | }); 48 | const InjectableBob = Injectable(CreateClassComponent); 49 | assertIsValidInjectable(InjectableBob); 50 | }); 51 | }); 52 | 53 | describe(`When using an Injectable Component`, () => { 54 | let InjectableComponentBob; 55 | let context; 56 | 57 | beforeEach(() => { 58 | InjectableComponentBob = Injectable(({ injections }) =>
{injections}
); 59 | 60 | context = { 61 | registerInjectable: sinon.spy(), 62 | removeInjectable: sinon.spy() 63 | }; 64 | }); 65 | 66 | it(`It should have an "injectionId" static set`, () => { 67 | expect(InjectableComponentBob.injectionId) 68 | .to.match(/^injectionId_[\d]+$/); 69 | }); 70 | 71 | it(`It should not render anything when initially mounted`, () => { 72 | const mounted = mount(, { context }); 73 | 74 | expect(mounted.html()).to.equal(`
`); 75 | }); 76 | 77 | it(`It should call the correct context items on mount`, () => { 78 | const mounted = mount(, { context }); 79 | 80 | expect(context.registerInjectable.callCount).to.equal(1); 81 | expect(context.removeInjectable.callCount).to.equal(0); 82 | 83 | const { 84 | injectionId: actualInjectionId, 85 | injectableInstance: actualInjectableInstance, 86 | receive: actualReceive 87 | } = context.registerInjectable.args[0][0]; 88 | 89 | expect(actualInjectionId).to.equal(InjectableComponentBob.injectionId); 90 | expect(actualInjectableInstance).to.equal(mounted.instance()); 91 | expect(typeof actualReceive).to.equal(`function`); 92 | }); 93 | 94 | it(`It should call the correct context items on unmount`, () => { 95 | const mounted = mount(, { context }); 96 | const actualInstance = mounted.instance(); 97 | mounted.unmount(); 98 | 99 | expect(context.registerInjectable.callCount).to.equal(1); 100 | expect(context.removeInjectable.callCount).to.equal(1); 101 | 102 | const { 103 | injectionId: actualInjectionId, 104 | injectableInstance: actualInjectableInstance 105 | } = context.registerInjectable.args[0][0]; 106 | 107 | expect(actualInjectionId).to.equal(InjectableComponentBob.injectionId); 108 | expect(actualInjectableInstance).to.equal(actualInstance); 109 | }); 110 | 111 | it(`It should render consumed injections`, () => { 112 | const mounted = mount(, { context }); 113 | 114 | const injectionOne =
injection 1
; 115 | const injectionTwo =
injection 2
; 116 | 117 | mounted.instance().consume([ 118 | injectionOne, 119 | injectionTwo 120 | ]); 121 | 122 | expect(mounted.state(`injections`)).to.eql([injectionOne, injectionTwo]); 123 | expect(mounted.html()) 124 | .to.equal(`
injection 1
injection 2
`); 125 | }); 126 | 127 | it(`It should not render duplicate elements`, () => { 128 | const mounted = mount(, { context }); 129 | 130 | const injectionOne =
injection 1
; 131 | 132 | mounted.instance().consume([injectionOne]); 133 | mounted.instance().consume([injectionOne]); 134 | 135 | expect(mounted.html()) 136 | .to.equal(`
injection 1
`); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/InjectablesProvider.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | 4 | describe(`Given the Injectables Provider`, () => { 5 | let instance; 6 | 7 | beforeEach(() => { 8 | const Provider = require(`../src/InjectablesProvider`).default; 9 | instance = new Provider(); 10 | }); 11 | 12 | it(`Then a newly added injectable should not receive any elements`, () => { 13 | let receivedInjections = []; 14 | 15 | instance.registerInjectable({ 16 | injectionId: `foo`, 17 | injectableInstance: () =>
foo
, 18 | receive: (elements) => { receivedInjections = elements; } 19 | }); 20 | 21 | expect(receivedInjections).to.eql([]); 22 | }); 23 | 24 | it(`Then injector registration, update, and removal should affect injectables`, () => { 25 | // Register injectable 26 | let receivedInjections = []; 27 | instance.registerInjectable({ 28 | injectionId: `foo`, 29 | injectableInstance: () =>
foo
, 30 | receive: (elements) => { receivedInjections = elements; } 31 | }); 32 | 33 | // Register injector. 34 | const injection =
injection
; 35 | const injectorInstance = () => null; 36 | instance.registerInjector({ 37 | injectionId: `foo`, 38 | injectorId: `injector1`, 39 | injectorInstance, 40 | inject: () => injection 41 | }); 42 | expect(receivedInjections).to.eql([injection]); 43 | 44 | // Update injector 45 | const newInjection =
new injection
; 46 | instance.updateInjector({ 47 | injectionId: `foo`, 48 | injectorId: `injector1`, 49 | injectorInstance, 50 | inject: () => newInjection 51 | }); 52 | expect(receivedInjections).to.eql([newInjection]); 53 | 54 | // Remove injector 55 | instance.removeInjector({ 56 | injectionId: `foo`, 57 | injectorId: `injector1`, 58 | injectorInstance 59 | }); 60 | expect(receivedInjections).to.eql([]); 61 | }); 62 | 63 | it(`Then a removed injectable should not receive any injections`, () => { 64 | // Register injectable 65 | const injectableInstance = () => null; 66 | let receivedInjections = []; 67 | instance.registerInjectable({ 68 | injectionId: `foo`, 69 | injectableInstance, 70 | receive: (elements) => { receivedInjections = elements; } 71 | }); 72 | expect(receivedInjections).to.eql([]); 73 | 74 | // Remove injectable 75 | instance.removeInjectable({ 76 | injectionId: `foo`, 77 | injectableInstance 78 | }); 79 | 80 | // Register injector. 81 | const injection =
injection
; 82 | const injectorInstance = () => null; 83 | instance.registerInjector({ 84 | injectionId: `foo`, 85 | injectorId: `injector1`, 86 | injectorInstance, 87 | inject: () => injection 88 | }); 89 | 90 | expect(receivedInjections).to.eql([]); 91 | }); 92 | 93 | it(`Then multiple injectables should recieve existing injections`, () => { 94 | // Register injector. 95 | const injection =
injection
; 96 | const injectorInstance = () => null; 97 | instance.registerInjector({ 98 | injectionId: `foo`, 99 | injectorId: `injector1`, 100 | injectorInstance, 101 | inject: () => injection 102 | }); 103 | 104 | // Register injectable 105 | const injectableInstance = () => null; 106 | let receivedInjections = []; 107 | instance.registerInjectable({ 108 | injectionId: `foo`, 109 | injectableInstance, 110 | receive: (elements) => { receivedInjections = elements; } 111 | }); 112 | expect(receivedInjections).to.eql([injection]); 113 | 114 | // Register injectable 115 | const injectableInstanceTwo = () => null; 116 | let receivedInjectionsTwo = []; 117 | instance.registerInjectable({ 118 | injectionId: `foo`, 119 | injectableInstance: injectableInstanceTwo, 120 | receive: (elements) => { receivedInjectionsTwo = elements; } 121 | }); 122 | expect(receivedInjectionsTwo).to.eql([injection]); 123 | }); 124 | 125 | it(`Then a duplicate injector instance registration should result in an error`, () => { 126 | // Register injector. 127 | const injection =
injection
; 128 | const injectorInstance = () => null; 129 | instance.registerInjector({ 130 | injectionId: `foo`, 131 | injectorId: `injector1`, 132 | injectorInstance, 133 | inject: () => injection 134 | }); 135 | 136 | // Re-Register injector. 137 | const duplicateRegister = () => 138 | instance.registerInjector({ 139 | injectionId: `foo`, 140 | injectorId: `injector1`, 141 | injectorInstance, 142 | inject: () => injection 143 | }); 144 | 145 | expect(duplicateRegister).to.throw(/An Injector instance is being registered multiple times/); 146 | }); 147 | 148 | it(`Then a duplicate injector id registration should result in an warning`, () => { 149 | const prevWarn = console.warn; // eslint-disable-line no-console 150 | const warnings = []; 151 | console.warn = (msg) => { warnings.push(msg); }; // eslint-disable-line no-console 152 | 153 | // Register injector. 154 | instance.registerInjector({ 155 | injectionId: `foo`, 156 | injectorId: `injector1`, 157 | injectorInstance: () => null, 158 | inject: () =>
injection
159 | }); 160 | 161 | // Register injector. 162 | instance.registerInjector({ 163 | injectionId: `foo`, 164 | injectorId: `injector1`, 165 | injectorInstance: () => null, 166 | inject: () =>
injection
167 | }); 168 | 169 | expect(warnings.length).to.eql(1); 170 | expect(warnings[0]).to.match(/Multiple instances of an Injector has been found/); 171 | 172 | console.warn = prevWarn; // eslint-disable-line no-console 173 | }); 174 | 175 | it(`Then multiple unique injectors should result in multiple injection changes`, () => { 176 | // Register injectable 177 | const injectableInstance = () => null; 178 | let receivedInjections = []; 179 | instance.registerInjectable({ 180 | injectionId: `foo`, 181 | injectableInstance, 182 | receive: (elements) => { receivedInjections = elements; } 183 | }); 184 | 185 | // Register injector one. 186 | const injection =
injection
; 187 | const injectorInstance = () => null; 188 | instance.registerInjector({ 189 | injectionId: `foo`, 190 | injectorId: `injector1`, 191 | injectorInstance, 192 | inject: () => injection 193 | }); 194 | expect(receivedInjections).to.eql([injection]); 195 | 196 | // Register injector two. 197 | const injectionTwo =
injection 2
; 198 | const injectorInstanceTwo = () => null; 199 | instance.registerInjector({ 200 | injectionId: `foo`, 201 | injectorId: `injector2`, 202 | injectorInstanceTwo, 203 | inject: () => injectionTwo 204 | }); 205 | expect(receivedInjections).to.eql([injection, injectionTwo]); 206 | 207 | // Remove injector one. 208 | instance.removeInjector({ 209 | injectionId: `foo`, 210 | injectorId: `injector1`, 211 | injectorInstance 212 | }); 213 | expect(receivedInjections).to.eql([injectionTwo]); 214 | 215 | // Remove injector two. 216 | instance.removeInjector({ 217 | injectionId: `foo`, 218 | injectorId: `injector2`, 219 | injectorInstanceTwo 220 | }); 221 | expect(receivedInjections).to.eql([]); 222 | }); 223 | 224 | it(`Then removing all Injectors and Injectables should clear registrations`, () => { 225 | // Register two injectables. 226 | const injectableInstance = () => null; 227 | instance.registerInjectable({ 228 | injectionId: `foo`, 229 | injectableInstance, 230 | receive: () => undefined 231 | }); 232 | const injectableInstanceTwo = () => null; 233 | instance.registerInjectable({ 234 | injectionId: `foo`, 235 | injectableInstanceTwo, 236 | receive: () => undefined 237 | }); 238 | // Register two injector. 239 | const injectorInstance = () => null; 240 | instance.registerInjector({ 241 | injectionId: `foo`, 242 | injectorId: `injector1`, 243 | injectorInstance, 244 | inject: () =>
injection
245 | }); 246 | const injectorInstanceTwo = () => null; 247 | instance.registerInjector({ 248 | injectionId: `foo`, 249 | injectorId: `injector2`, 250 | injectorInstanceTwo, 251 | inject: () =>
injection
252 | }); 253 | 254 | expect(instance.registrations.length).to.equal(1); 255 | 256 | // Remove injectables. 257 | instance.removeInjectable({ 258 | injectionId: `foo`, 259 | injectableInstance 260 | }); 261 | instance.removeInjectable({ 262 | injectionId: `foo`, 263 | injectableInstanceTwo 264 | }); 265 | // Remove injectors. 266 | instance.removeInjector({ 267 | injectionId: `foo`, 268 | injectorId: `injector2`, 269 | injectorInstance 270 | }); 271 | instance.removeInjector({ 272 | injectionId: `foo`, 273 | injectorId: `injector2`, 274 | injectorInstanceTwo 275 | }); 276 | 277 | expect(instance.registrations.length).to.equal(0); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /test/Injector.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | 3 | import React, { Component } from 'react'; 4 | import { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import { mount } from 'enzyme'; 7 | 8 | describe(`Given the Injector interface`, () => { 9 | const Injector = require(`../src/Injector`).default; 10 | const Injectable = require(`../src/Injectable`).default; 11 | 12 | describe(`When creating an Injector`, () => { 13 | let ValidInjectable; 14 | 15 | function assertIsValidInjector(injectorInstance) { 16 | const actual = typeof injectorInstance === `function` && 17 | !!injectorInstance.contextTypes && 18 | !!injectorInstance.contextTypes.registerInjector && 19 | !!injectorInstance.contextTypes.updateInjector && 20 | !!injectorInstance.contextTypes.removeInjector; 21 | 22 | const expected = true; 23 | 24 | expect(actual).to.equal(expected, `Invalid Injector created.`); 25 | } 26 | 27 | beforeEach(() => { 28 | ValidInjectable = Injectable(() =>
foo
); 29 | }); 30 | 31 | it(`It should allow a stateless component for the injection`, () => { 32 | const StatelessComponentInjection = () =>
bar
; 33 | const InjectorBob = Injector({ 34 | into: ValidInjectable 35 | })(StatelessComponentInjection); 36 | assertIsValidInjector(InjectorBob); 37 | }); 38 | 39 | it(`It should allow an ES6 class based component for the injection`, () => { 40 | class ClassComponentInjection extends Component { 41 | state = { bob: `baz` } 42 | render() { 43 | return
foo
; 44 | } 45 | } 46 | const InjectorBob = Injector({ 47 | into: ValidInjectable 48 | })(ClassComponentInjection); 49 | assertIsValidInjector(InjectorBob); 50 | }); 51 | 52 | it(`It should allow an React.createClass based component for the injection`, () => { 53 | const CreateClassComponentInjection = 54 | React.createClass({ // eslint-disable-line react/prefer-es6-class 55 | state: { foo: `bar` }, 56 | render() { 57 | return
foo
; 58 | } 59 | }); 60 | const InjectorBob = Injector({ 61 | into: ValidInjectable 62 | })(CreateClassComponentInjection); 63 | assertIsValidInjector(InjectorBob); 64 | }); 65 | }); 66 | 67 | describe(`When using an Injector Component`, () => { 68 | let InjectorComponentBob; 69 | let InjectionComponent; 70 | let context; 71 | let injectionId; 72 | 73 | beforeEach(() => { 74 | const InjectableComponent = Injectable(() =>
foo
); 75 | injectionId = InjectableComponent.injectionId; 76 | 77 | InjectionComponent = () => 78 |
injection
; 79 | 80 | InjectorComponentBob = Injector({ 81 | into: InjectableComponent 82 | })(InjectionComponent); 83 | 84 | context = { 85 | registerInjector: sinon.spy(), 86 | removeInjector: sinon.spy(), 87 | updateInjector: sinon.spy() 88 | }; 89 | }); 90 | 91 | it(`It should not render anything when mounted`, () => { 92 | const mounted = mount(, { context }); 93 | 94 | expect(mounted.html()).to.equal(null); 95 | }); 96 | 97 | it(`It should call the correct context items on mount`, () => { 98 | const mounted = mount(, { context }); 99 | 100 | expect(context.registerInjector.callCount).to.equal(1); 101 | expect(context.updateInjector.callCount).to.equal(0); 102 | expect(context.removeInjector.callCount).to.equal(0); 103 | 104 | const { 105 | injectionId: actualInjectionId, 106 | injectorId: actualInjectorId, 107 | injectorInstance: actualInjectorInstance, 108 | inject: actualInject 109 | } = context.registerInjector.args[0][0]; 110 | 111 | expect(actualInjectionId).to.equal(injectionId); 112 | expect(actualInjectorId).to.match(/^injector_[\d]+$/); 113 | expect(actualInjectorInstance).to.equal(mounted.instance()); 114 | expect(typeof actualInject).to.equal(`function`); 115 | expect( 116 | mount(
{actualInject()}
) 117 | .find(InjectionComponent) 118 | .length 119 | ).to.equal(1); 120 | }); 121 | 122 | it(`It should call the correct context items on updates`, () => { 123 | const mounted = mount(, { context }); 124 | mounted.update(); 125 | 126 | expect(context.registerInjector.callCount).to.equal(1); 127 | expect(context.updateInjector.callCount).to.equal(1); 128 | expect(context.removeInjector.callCount).to.equal(0); 129 | 130 | const { 131 | injectionId: actualInjectionId, 132 | injectorId: actualInjectorId, 133 | injectorInstance: actualInjectorInstance, 134 | inject: actualInject 135 | } = context.updateInjector.args[0][0]; 136 | 137 | expect(actualInjectionId).to.equal(injectionId); 138 | expect(actualInjectorId).to.match(/^injector_[\d]+$/); 139 | expect(actualInjectorInstance).to.equal(mounted.instance()); 140 | expect(typeof actualInject).to.equal(`function`); 141 | expect( 142 | mount(
{actualInject()}
) 143 | .find(InjectionComponent) 144 | .length 145 | ).to.equal(1); 146 | }); 147 | 148 | it(`It should call the correct context items on unmount`, () => { 149 | const mounted = mount(, { context }); 150 | const actualInstance = mounted.instance(); 151 | mounted.unmount(); 152 | 153 | expect(context.registerInjector.callCount).to.equal(1); 154 | expect(context.updateInjector.callCount).to.equal(0); 155 | expect(context.removeInjector.callCount).to.equal(1); 156 | 157 | const { 158 | injectionId: actualInjectionId, 159 | injectorId: actualInjectorId, 160 | injectorInstance: actualInjectorInstance 161 | } = context.removeInjector.args[0][0]; 162 | 163 | expect(actualInjectionId).to.equal(injectionId); 164 | expect(actualInjectorId).to.match(/^injector_[\d]+$/); 165 | expect(actualInjectorInstance).to.equal(actualInstance); 166 | }); 167 | }); 168 | 169 | describe(`When trying to create an Injector with an invalid "injection"`, () => { 170 | it(`Then an error should be thrown`, () => { 171 | const invalidInjections = [ 172 | 1, 173 | `2`, 174 | true, 175 |
foo
, 176 | new Date(), 177 | {}, 178 | [] 179 | ]; 180 | 181 | const validToArg = Injectable(() =>
foo
); 182 | 183 | invalidInjections.forEach(invalidInjection => { 184 | const actual = () => Injector({ 185 | into: validToArg 186 | })(invalidInjection); 187 | 188 | const expected = /Invalid injection value/; 189 | 190 | expect(actual).to.throw(expected); 191 | }); 192 | }); 193 | }); 194 | 195 | describe(`When trying to create an Injector with an invalid "to" argument`, () => { 196 | it(`Then an error should be thrown`, () => { 197 | // Invalid type 198 | const InvalidInjectable1 = {}; 199 | 200 | // Invalid interface 201 | const InvalidInjectable2 = () => undefined; 202 | 203 | // Invalid interface 204 | class InvalidInjectable3 extends Component { 205 | static injectionId = `foo`; 206 | 207 | render() { 208 | return
Foo
; 209 | } 210 | } 211 | 212 | // Invalid interface 213 | class InvalidInjectable4 extends Component { 214 | static injectionId = `foo`; 215 | 216 | static contextTypes = { } 217 | 218 | render() { 219 | return
Foo
; 220 | } 221 | } 222 | 223 | // Invalid interface 224 | class InvalidInjectable5 extends Component { 225 | static injectionId = `foo`; 226 | 227 | static contextTypes = { 228 | registerInjectable: () => undefined 229 | } 230 | 231 | render() { 232 | return
Foo
; 233 | } 234 | } 235 | 236 | // Invalid interface 237 | class InvalidInjectable6 extends Component { 238 | static injectionId = `foo`; 239 | 240 | static contextTypes = { 241 | removeInjectable: () => undefined 242 | } 243 | 244 | render() { 245 | return
Foo
; 246 | } 247 | } 248 | 249 | const invalidCases = []; 250 | invalidCases.push(InvalidInjectable1); 251 | invalidCases.push(InvalidInjectable2); 252 | invalidCases.push(InvalidInjectable3); 253 | invalidCases.push(InvalidInjectable4); 254 | invalidCases.push(InvalidInjectable5); 255 | invalidCases.push(InvalidInjectable6); 256 | 257 | invalidCases.forEach(invalidInjectable => { 258 | const actual = () => Injector({ 259 | into: invalidInjectable 260 | })(() =>
bar
); 261 | 262 | const expected = /Invalid Injectable target/; 263 | 264 | expect(actual).to.throw(expected); 265 | }); 266 | }); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { describeWithDOM } from './jsdom'; 3 | import { expect } from 'chai'; 4 | import { mount } from 'enzyme'; 5 | 6 | describeWithDOM(`Given an Injectables configuration`, () => { 7 | const { InjectablesProvider, Injectable, Injector } = require(`../src/index.js`); 8 | 9 | let InjectableHeader; 10 | let Layout; 11 | let HeaderInjectingSectionOne; 12 | let HeaderInjectingSectionTwo; 13 | let HeaderInjection; 14 | let render; 15 | 16 | before(() => { 17 | render = elements => mount( 18 | 19 | {elements} 20 | 21 | ); 22 | 23 | const Header = ({ injections }) => ( 24 | 27 | ); 28 | Header.propTypes = { 29 | injections: PropTypes.arrayOf(PropTypes.element).isRequired 30 | }; 31 | 32 | InjectableHeader = Injectable(Header); 33 | 34 | Layout = ({ children }) => ( // eslint-disable-line react/prop-types 35 |
36 | 37 |
38 | {children} 39 |
40 |
41 | ); 42 | Layout.propTypes = { 43 | children: PropTypes.any 44 | }; 45 | 46 | HeaderInjection = props => ( 47 |
{props.message || `injection`}.
// eslint-disable-line 48 | ); 49 | 50 | HeaderInjectingSectionOne = Injector({ 51 | into: InjectableHeader 52 | })(HeaderInjection); 53 | 54 | HeaderInjectingSectionTwo = Injector({ 55 | into: InjectableHeader 56 | })(HeaderInjection); 57 | }); 58 | 59 | describe(`When the injector is rendered as a child of the injectable`, () => { 60 | let rendered; 61 | 62 | before(() => { 63 | rendered = render( 64 | 65 | 66 | 67 | ); 68 | }); 69 | 70 | it(`Then the injected content should have been rendered in the header`, () => { 71 | const expected = 1; 72 | 73 | const actual = rendered 74 | .find(InjectableHeader) 75 | .find(HeaderInjection) 76 | .length; 77 | 78 | expect(actual).to.equal(expected, `The injected content was not found.`); 79 | }); 80 | }); 81 | 82 | describe(`When the injector is rendered before the injectable`, () => { 83 | let rendered; 84 | 85 | before(() => { 86 | rendered = render( 87 |
88 | 89 | 90 |
91 | ); 92 | }); 93 | 94 | it(`Then the injected content should have been rendered in the header`, () => { 95 | const expected = 1; 96 | const actual = rendered 97 | .find(InjectableHeader) 98 | .find(HeaderInjection) 99 | .length; 100 | 101 | expect(actual).to.equal(expected, `The injected content was not found.`); 102 | }); 103 | }); 104 | 105 | describe(`When rendering multiple of the same injector instance`, () => { 106 | let rendered; 107 | 108 | before(() => { 109 | rendered = render( 110 |
111 | 112 | 113 | 114 | 115 |
116 | ); 117 | }); 118 | 119 | it(`Then the injected content should have been rendered in the header`, () => { 120 | const expected = 1; 121 | const actual = rendered 122 | .find(InjectableHeader) 123 | .find(HeaderInjection) 124 | .length; 125 | 126 | expect(actual).to.equal(expected, `The injected content was not found.`); 127 | }); 128 | }); 129 | 130 | describe(`When rendering different injectors targetting the same injectable`, () => { 131 | let rendered; 132 | 133 | before(() => { 134 | rendered = render( 135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 |
143 | ); 144 | }); 145 | 146 | it(`Then the injected content should have been rendered in the header`, () => { 147 | const expected = 2; 148 | const actual = rendered 149 | .find(InjectableHeader) 150 | .find(HeaderInjection) 151 | .length; 152 | 153 | expect(actual).to.equal(expected, `The injected content was not found.`); 154 | }); 155 | }); 156 | 157 | describe(`When rendering a null/undefined from an Injector`, () => { 158 | it(`Then nothing should be rendered`, () => { 159 | const NullInjector = Injector({ 160 | into: InjectableHeader 161 | })(() => null); 162 | 163 | const rendered = render( 164 | 165 | 166 | 167 | ); 168 | 169 | const actual = rendered 170 | .find(InjectableHeader) 171 | .html(); 172 | 173 | expect(actual) 174 | .to.match(/^