├── .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 | [](https://travis-ci.org/ctrlplusb/react-injectables)
15 | [](http://npm.im/react-injectables)
16 | [](http://opensource.org/licenses/MIT)
17 | [](https://codecov.io/github/ctrlplusb/react-injectables)
18 | []()
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 |
45 |
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 |
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 |