Skip to content

Commit

Permalink
perf(api): Massive simplification to API, along with guards and 100% …
Browse files Browse the repository at this point in the history
…test cov.

The API has been refocused to allow for the injection of Components of any type
(i.e. class/stateless/createClass).  The Provider has been renamed to make it
more explicit, and the Injector API has been greatly simplified.  A lot more
invariant based checks have been added to the project and we have achieved
full test coverage.

closes #2
closes #3
BREAKING CHANGE: Provider renamed to InjectableProvider.
BREAKING CHANGE: The Injector helper has been greatly simplified.  Instead of
  wrapping a component that will host the injection, it rather produces an
  injection component which you can render into any component that you would
  like to initiate an injection.  This produces a much cleaner API and allows
  you to easily pass down specific props to the component that will be injected.
  Please look at the updated readme for example usage.
  • Loading branch information
ctrlplusb committed Apr 11, 2016
1 parent 59ee4b0 commit adf2d57
Show file tree
Hide file tree
Showing 16 changed files with 964 additions and 422 deletions.
123 changes: 61 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align='center'>
<img src='https://raw.githubusercontent.com/ctrlplusb/react-injectables/master/assets/logo.png' width='350'/>
<p align='center'>Explicitly inject Elements to any part of your React render tree</p>
<p align='center'>Explicitly inject Components to any part of your React render tree</p>
</p>

[![Travis](https://img.shields.io/travis/ctrlplusb/react-injectables.svg?style=flat-square)](https://travis-ci.org/ctrlplusb/react-injectables)
Expand All @@ -9,15 +9,11 @@
[![Codecov](https://img.shields.io/codecov/c/github/ctrlplusb/react-injectables.svg?style=flat-square)](https://codecov.io/github/ctrlplusb/react-injectables)
[![Maintenance](https://img.shields.io/maintenance/yes/2016.svg?style=flat-square)]()

Tiny, and the only dependency is a peer dependency of React.

### Warning

I am actively developing this project whilst applying it to a production use-case. This is exposing the limitations of the API pretty fast requiring that I do quick updates. Expect the versions to bump excessively over the next couple of weeks, but I will stabilise it as quickly as I can. I'm not a fan of 0.x.x beta releases and don't mind big numbers. I like that each version I release clearly indicates breaking changes. This message will disappear as soon as I believe the API will remain stable for an extended period of time.
This is a teeny tiny React library - it's almost unnoticable when gzipped.

## What is this for?

Placeholders, Modals, etc etc.
Placeholders, Modals, etc.

## Overview

Expand All @@ -44,26 +40,34 @@ One option would be to use react-routers native capability to pass down multiple

Enter Injectables.

Injectables aims to provide you with a mechanism to explicity define an Injectable target and Injector source component(s).
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`.

With Injectables you can easily inject into `Sidebar` from your `ProductPage` doing something similar to the following:
With Injectables you can easily inject a Component into the `Sidebar` when your `ProductPage` Component gets mounted. Here is a partial example of this:

```javascript
import { Injector } from 'react-injectables';
const MyBasketSidebarInjection = Injector({
into: InjectableSidebar
})(MyBasket)

class ProductsPage extends Component {
...

render() {
return (
<div>
<MyBasketSidebarInjection />

<h1>Product Page</h1>
...
</div>
);
}
}

export default Injector({
to: Sidebar,
inject: function () { return (<MyBasketSummary />); }
})(ProductsPage);
```

Now every time `ProductPage` is rendered onto the page the `MyBasketSummary` will be injected into `Sidebar`. Ok, there is a tiny bit of additional setup. But it's quick sticks to get going.
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, but the above is a basic overview of how easy it is to define your injections after the initial configuration.

Fairy dust is involved, but we attempt to keep things as un-magical as possible, keeping things explict, whilst also easy to use.
Yep, some fairy dust is involved, but we attempt to keep things as un-magical as possible, pushing for explictness whilst keeping it easy to use.


## Usage
Expand All @@ -76,48 +80,45 @@ First install the library.

To get going there are only 3 steps:

1. Wrap your application with our `Provider`.
2. Wrap a component you would like to _receive_ content with our `Injectable`. e.g. `Injectable(Sidebar)`
4. Wrap a component you would like to _produce_ content with our `Injector`. e.g.: `Injector({ to: InjectableSidebar, inject: () => <MyBasketsView>)(ProductPage)`
1. Wrap your application with our `InjectablesProvider`.
2. Wrap a Component you would like to _receive_ injected content with our `Injectable` helper. e.g. `Injectable(MainScreen)`
4. Wrap a Component you would like to _inject_ with our `Injector` helper. e.g.: `Injector({ into: MainScreen })(MyModal)`

### Full Tutorial

Ok, here's a more detailed run through with example code.

__Step 1__

Somewhere very low down in your application wrap your component tree with our `Provider`. This is the engine that will do all the heavy lifting for you. For example:
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:

```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-injectables';
import { InjectablesProvider } from 'react-injectables';

ReactDOM.render((
<Provider>
<InjectablesProvider>
<Router>
...
</Router>
</Provider>
</InjectablesProvider>
), document.getElementById('app'));
```

_Note:_ If you already have another component with the name of `Provider` in your app (think redux), then you can rename the import, like so:
`import { Provider as InjectablesProvider } from 'react-injectables';`

__Step 2__

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:
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:

```javascript
import React, { PropTypes } from 'react';
import { Injectable } from 'react-injectables';

// Note the 'injected' prop. This will contain any injected elements.
function Sidebar({ injected }) {
// Note the 'injections' prop. This will contain any injected elements.
function Sidebar({ injections }) {
return (
<div className="header">
{injected}
{injections}
</div>
);
}
Expand All @@ -132,45 +133,43 @@ __Step 3__

Ok, so you have an `InjectableSidebar` now, but you need to declare the components that will inject content in to it.

You need to make use of our `Injector`, wrapping your component, whilst also providing the target injectable component and the elements that you wish to inject.
You need to make use of our `Injector`, wrapping the Component you would like to inject, whilst also providing the target `Injectable` Component you created.

```javascript
import React from 'react';
import { Injector } from 'react-injectables';
import InjectableSidebar from './InjectableSidebar';
import BasketViewPage from './BasketViewPage';
import MyBasketView from './MyBasketView';

// This sets up our injection.
const MyBasketViewSidebarInjection = Injector({
into: InjectableSidebar // The target Injectable Component. The actual Component - explicit. :)
})(MyBasketView); // The Component you would like to be injected.

class ProductsPage extends Component {
....

render() {
return (
<div>
{/* The injection! Nothing actually gets rendered here, it gets sent to
our target Injectable. In this case it means that MyBasketView will
be injected into the Sidebar.
Notice how you can also pass down props into your injected component too. */}
<MyBasketViewSidebarInjection focusOnProductId={this.props.productId} />

<h1>Products Page</h1>
</div>
);
}
}

// We are using this stateless component as our inject resolver.
// It will receive any props that will be passed into ProductsPage, including updated ones.
function Inject(props) {
return (<BasketViewPage focusOnProductId={props.productId} />);
}

export default Injector({
to: InjectableSidebar, // You have to specify the actual Injectable Component. Explicit.
inject: Inject // The inject function or a React Element.
})(ProductsPage);
export default ProductsPage;
```

The `inject` property argument for the `Injector` accepts either of two things:

* A function with a `props` argument that when executed will return a React Element. i.e. a stateless functional component. It will recieve the same props that will be passed into the component being wrapped by the `Injector`. If you wrap your component with multiple higher order functions/components then please make sure that `Injector` is the first of the functions used to wrap your component. For e.g.
```javascript
export default compose( // executes last to first.
Connect(stateM, propsM),
Injector(args)
)(ProductsPage);
```

* A React element. This works, however the element is created up front before it is even needed. Also you lose the capability to interogate any props that may have been passed into your component.

---

And that's it. Any time the `ProductsPage` is rendered it will inject it's content into the `Sidebar`. When it unmounts, it's injected elements will be removed from the `Sidebar`.
And that's it. Any time the `ProductsPage` is mounted it will inject the `MyBasketView` Component into the `Sidebar`. When it unmounts, the respective Component will be removed from the `Sidebar`.

As you can see it's all explicit, so you can follow the import references to find out any relationships.

Expand All @@ -180,17 +179,17 @@ You will need to repeat steps 2 and 3 for any set of Injectable targets and Inje

Here are a few basic properties you should be aware of:

* All injection happens within the initial render cycle by react-dom. Injection does not cause a double render to occur. This is a result of us trying to intentionally keep injection as "uni-directional" / "input -> output" as possible.
* 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.

* You can have multiple instances of an Injectable rendered in your app. They will all recieve the same injected content from their respective Injectors.
* 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.

* You can create multiple Injectors components targetting the same Injectable component. The rendered Injectors shall have their injected elements collected and passed through to the target Injectable. For example, you may want to pass in action buttons from different components into an InjectableActions component.
* 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.

* If an Injector is removed from the tree then it's injected elements will automatically be removed from the Injectable target.
* If an Component that is hosting `Injector` is unmounted then the injected Components will automatically be removed from the `Injectable` target.

* Any new Injectors that are rendered into the tree will automatically have their injected content rendered within any rendered Injectable target.
* 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.

* Injectors are allowed to be rendered before any Injectables. Once their target Injectable Component is rendered then any content from existing the Injectors will automatically be rendered within the newly rendered Injectable.
* `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`.

## Examples

Expand Down
11 changes: 5 additions & 6 deletions examples/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@
"author": "",
"license": "MIT",
"dependencies": {
"babel-cli": "6.6.5",
"babel-core": "6.7.4",
"babel-cli": "6.7.5",
"babel-core": "6.7.6",
"babel-loader": "6.2.4",
"babel-preset-es2015": "6.6.0",
"babel-preset-react": "6.5.0",
"babel-preset-stage-1": "6.5.0",
"babel-register": "6.7.2",
"express": "4.13.4",
"react": "0.14.8",
"react-dom": "0.14.8",
"react-injectables": "1.0.0",
"react": "15.0.1",
"react-dom": "15.0.1",
"react-router": "2.0.1",
"webpack": "1.12.14",
"webpack": "1.12.15",
"webpack-dev-middleware": "1.6.1",
"webpack-hot-middleware": "2.10.0"
}
Expand Down
8 changes: 4 additions & 4 deletions examples/router/src/InjectableHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import React, { PropTypes } from 'react';
import { Injectable } from '../../../src/index.js';

// Prep a header component which we intend to make injectable.
// Note the prop named 'injected'. This will contain any injected elements.
const Header = ({ injected }) => (
// Note the prop named 'injections'. This will contain any injected elements.
const Header = ({ injections }) => (
<div style={{ backgroundColor: `red`, color: `white` }}>
<h1>INJECTABLE HEADER</h1>
<div>
{injected.length > 0 ? injected : <div>Nothing has been injected</div>}
{injections.length > 0 ? injections : <div>Nothing has been injected</div>}
</div>
</div>
);
Header.propTypes = {
injected: PropTypes.arrayOf(PropTypes.element)
injections: PropTypes.arrayOf(PropTypes.element)
};

// Convert our header into an injectable!
Expand Down
34 changes: 15 additions & 19 deletions examples/router/src/PageOne.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,24 @@ import React, { Component, PropTypes } from 'react';
import { Injector } from '../../../src/index.js';
import InjectableHeader from './InjectableHeader';

const Content = ({ active }) => (
<div>
<p>I am page one.</p>
<p>My State is {active ? `active` : `inactive`}</p>
</div>
);
Content.propTypes = {
active: PropTypes.bool.isRequired
};

const Inject = ({ active }) => (
// Our component that we will inject.
const Injection = ({ active }) => (
<div>
<p>Injection from Page One</p>
<p>The active prop value is: {active ? `active` : `inactive`}</p>
</div>
);
Inject.propTypes = {
Injection.propTypes = {
active: PropTypes.bool.isRequired
};

const InjectingContent = Injector({
to: InjectableHeader,
inject: Inject
})(Content);
// Our Injector instance configured to inject into the InjectableHeader.
const HeaderInjection = Injector({
into: InjectableHeader
})(Injection);

/**
* We wrap our injecting content with a class based component so we can track
* state and pass down props, thereby adding behaviour.
* This is the component that when rendered will cause the injection to occur.
*/
class PageOne extends Component {
state = {
Expand All @@ -45,7 +35,13 @@ class PageOne extends Component {

return (
<div>
<InjectingContent active={active} />
{/* The injection! Nothing gets rendered here. */}
<HeaderInjection active={active} />

<div>
<p>I am page one.</p>
<p>My State is {active ? `active` : `inactive`}</p>
</div>

<button onClick={this.onClick}>Change my state</button>
</div>
Expand Down
30 changes: 20 additions & 10 deletions examples/router/src/PageTwo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import React from 'react';
import { Injector } from '../../../src/index.js';
import InjectableHeader from './InjectableHeader';

const PageTwo = () => (
<div>
I am page two.
</div>
);

// Our component that we will inject.
const Inject = (props) => (
<div>
Injection from Page Two.<br />
Expand All @@ -16,7 +11,22 @@ const Inject = (props) => (
</div>
);

export default Injector({
to: InjectableHeader,
inject: Inject
})(PageTwo);
// Our Injector instance configured to inject into the InjectableHeader.
const HeaderInjection = Injector({
into: InjectableHeader
})(Inject);

/**
* This is the component that when rendered will cause the injection to occur.
*/
const PageTwo = () => (
<div>
{/* The injection! Nothing actually gets rendered here, it gets sent to
our target Injectable. */}
<HeaderInjection />

I am page two.
</div>
);

export default PageTwo;
2 changes: 1 addition & 1 deletion examples/router/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider as InjectablesProvider } from '../../../src/index.js';
import { InjectablesProvider } from '../../../src/index.js';
import { browserHistory, Router, Route, IndexRoute } from 'react-router';

import App from './App';
Expand Down
Loading

0 comments on commit adf2d57

Please sign in to comment.