Skip to content

Commit

Permalink
fix(Injector): Injectors were not updating for prop updates.
Browse files Browse the repository at this point in the history
Updated props being passed through to injectors were not resolving in updates occurring on their
inject functions.
  • Loading branch information
ctrlplusb committed Apr 6, 2016
1 parent 7085201 commit 59ee4b0
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 57 deletions.
47 changes: 26 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@

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.

## What is this for?

Placeholders, Modals, etc etc.

## Overview

Envision you have the following component tree:

```html
Expand Down Expand Up @@ -102,22 +110,17 @@ __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:

```javascript
// ./src/components/InjectableHeader.js

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

// Note the prop named 'injected'. This will contain any injected elements.
const Sidebar = ({ injected }) => (
<div className="header">
{injected.length > 0
? injected
: <div>Nothing has been injected</div>}
</div>
);
Sidebar.propTypes = {
injected: PropTypes.arrayOf(PropTypes.element)
};
// Note the 'injected' prop. This will contain any injected elements.
function Sidebar({ injected }) {
return (
<div className="header">
{injected}
</div>
);
}

// We wrap our Sidebar component with Injectable. This does all the wiring up for us.
export default Injectable(Sidebar);
Expand All @@ -140,10 +143,16 @@ import BasketViewPage from './BasketViewPage';
class ProductsPage extends Component {
....
}

// 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,
inject: function (props) { return (<BasketViewPage focusOnProductId={props.productId} />); }
to: InjectableSidebar, // You have to specify the actual Injectable Component. Explicit.
inject: Inject // The inject function or a React Element.
})(ProductsPage);
```

Expand Down Expand Up @@ -176,16 +185,12 @@ Here are a few basic properties you should be aware of:
* 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 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.

* Multiple renders of the same Injector instance will not result in duplicate content being rendered within the Injectable target. Only unique instances are passed through to the Injectable target.

* If an Injector is removed from the tree then it's injected elements will automatically be removed from the Injectable target.
* If an Injector is removed from the tree then it's injected elements 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.

* Injectors are allowed to be rendered before any Injectables. Once a related Injectable instance is rendered then any content from existing Injectors will automatically be rendered by the newly rendered Injectable.

* You should create a new Injectable/Injector HOC pair (via the prepInjection function) for every Injectable target you would like.
* 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.

## Examples

Expand Down
4 changes: 2 additions & 2 deletions examples/router/devServer/devServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ server.get(`*`, (req, res) => {
res.sendFile(path.resolve(__dirname, `../public/index.html`));
});

server.listen(3001, `localhost`, (err) => {
server.listen(3002, `localhost`, (err) => {
if (err) {
console.log(err); // eslint-disable-line no-console
return;
}

console.log(`Listening at http://localhost:3001`); // eslint-disable-line no-console
console.log(`Listening at http://localhost:3002`); // eslint-disable-line no-console
});
55 changes: 49 additions & 6 deletions examples/router/src/PageOne.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,56 @@
import React from 'react';
import React, { Component, PropTypes } from 'react';
import { Injector } from '../../../src/index.js';
import InjectableHeader from './InjectableHeader';

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

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

const InjectingContent = Injector({
to: InjectableHeader,
inject: () => <div>Injection from Page One</div>
})(PageOne);
inject: Inject
})(Content);

/**
* We wrap our injecting content with a class based component so we can track
* state and pass down props, thereby adding behaviour.
*/
class PageOne extends Component {
state = {
active: false
}

onClick = () => {
this.setState({ active: !this.state.active });
}

render() {
const { active } = this.state;

return (
<div>
<InjectingContent active={active} />

<button onClick={this.onClick}>Change my state</button>
</div>
);
}
}

export default PageOne;
29 changes: 21 additions & 8 deletions src/Injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,34 @@ const Injector = (args: { to: Object, inject: Inject }) => {
class InjectorComponent extends Component {
static contextTypes = {
registerInjector: PropTypes.func.isRequired,
updateInjector: PropTypes.func.isRequired,
removeInjector: PropTypes.func.isRequired
};

componentWillMount() {
this.context.registerInjector({
injectionId: to.injectionId,
injectorId,
injector: this
injector: this,
inject: () => {
if (typeof inject === `function`) {
return inject(this.props);
}
return inject;
}
});
}

componentWillUpdate(nextProps) {
this.context.updateInjector({
injectionId: to.injectionId,
injector: this,
inject: () => {
if (typeof inject === `function`) {
return inject(nextProps);
}
return inject;
}
});
}

Expand All @@ -32,13 +52,6 @@ const Injector = (args: { to: Object, inject: Inject }) => {
});
}

getInjectElement = () => {
if (typeof inject === `function`) {
return inject(this.props);
}
return inject;
}

render() {
return (<WrappedComponent {...this.props} />);
}
Expand Down
27 changes: 23 additions & 4 deletions src/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class InjectablesProvider extends Component {
static childContextTypes = {
registerInjector: PropTypes.func.isRequired,
removeInjector: PropTypes.func.isRequired,
updateInjector: PropTypes.func.isRequired,
registerInjectable: PropTypes.func.isRequired,
removeInjectable: PropTypes.func.isRequired,
};
Expand All @@ -24,6 +25,8 @@ class InjectablesProvider extends Component {

removeInjector: (args) => this.removeInjector(args),

updateInjector: (args) => this.updateInjector(args),

registerInjectable: (args) => this.registerInjectable(args),

removeInjectable: (args) => this.removeInjectable(args)
Expand Down Expand Up @@ -56,7 +59,7 @@ class InjectablesProvider extends Component {

const elements = compose(
filter(x => x !== null && x !== undefined),
map(x => x.injector.getInjectElement()),
map(x => x.inject()),
uniqBy(`injectorId`)
)(injections);

Expand Down Expand Up @@ -97,16 +100,17 @@ class InjectablesProvider extends Component {
return find(x => Object.is(x.injector, injector))(registration.injections);
}

registerInjector(args: { injectionId: string, injectorId: string, injector: Object }) {
const { injectionId, injectorId, injector } = args;
registerInjector(args
: { injectionId: string, injectorId: string, injector: Object, inject: Function }) {
const { injectionId, injectorId, injector, inject } = args;
const registration = this.getRegistration({ injectionId });
const existingInjection = this.findInjection({ registration, injector });

if (existingInjection) {
return;
}

const newInjection = { injector, injectorId };
const newInjection = { injectorId, injector, inject };
registration.injections = [
...registration.injections,
newInjection
Expand All @@ -115,6 +119,21 @@ class InjectablesProvider extends Component {
this.runInjections({ registration });
}

updateInjector(args
: { injectionId: string, injector: Object, inject: Function }) {
const { injectionId, injector, inject } = args;
const registration = this.getRegistration({ injectionId });
const existingInjection = this.findInjection({ registration, injector });

if (!existingInjection) {
throw new Error(`Trying to update an Injector that is not registered`);
}

existingInjection.inject = inject;

this.runInjections({ registration });
}

removeInjector(args: { injectionId: string, injector: Object }) {
const { injectionId, injector } = args;
const registration = this.getRegistration({ injectionId });
Expand Down
36 changes: 20 additions & 16 deletions test/Provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ describe(`Given the Injectables Provider`, () => {
let producer1;
let producer2;
let producer3;
let inject1;
let inject2;

before(() => {
const Provider = require(`../src/Provider`).default;
Expand All @@ -22,9 +24,10 @@ describe(`Given the Injectables Provider`, () => {
consumed2 = [];
consumer1 = { consume: (elements) => { consumed1 = elements; } };
consumer2 = { consume: (elements) => { consumed2 = elements; } };
producer1 = { getInjectElement: () => element1 };
producer2 = { getInjectElement: () => element1 };
producer3 = { getInjectElement: () => element2 };
producer1 = {};
producer2 = {};
inject1 = () => element1;
inject2 = () => element2;
});

it(`Then a newly added consumer should initially receive now elements`, () => {
Expand All @@ -42,7 +45,8 @@ describe(`Given the Injectables Provider`, () => {
instance.registerInjector({
injectionId: `foo`,
injectorId: `injector1`,
injector: producer1
injector: producer1,
inject: inject1
});

const expected = [element1];
Expand All @@ -69,7 +73,8 @@ describe(`Given the Injectables Provider`, () => {
instance.registerInjector({
injectionId: `foo`,
injectorId: `injector1`,
injector: producer1
injector: producer1,
inject: inject1
});

const expected = [];
Expand Down Expand Up @@ -99,7 +104,8 @@ describe(`Given the Injectables Provider`, () => {
instance.registerInjector({
injectionId: `foo`,
injectorId: `injector1`,
injector: producer1
injector: producer1,
inject: inject1
});

const expected = [element1];
Expand All @@ -113,8 +119,9 @@ describe(`Given the Injectables Provider`, () => {
it(`Then a duplicate producer registration should result in no consumption change`, () => {
instance.registerInjector({
injectionId: `foo`,
injectorId: `injector2`,
injector: producer1
injectorId: `injector1`,
injector: producer1,
inject: inject1
});

const expected = [element1];
Expand All @@ -128,8 +135,9 @@ describe(`Given the Injectables Provider`, () => {
it(`Then a new producer with an new element should result in 2 element consumption`, () => {
instance.registerInjector({
injectionId: `foo`,
injectorId: `injector3`,
injector: producer3
injectorId: `injector2`,
injector: producer2,
inject: inject2
});

const expected = [element1, element2];
Expand All @@ -140,13 +148,13 @@ describe(`Given the Injectables Provider`, () => {
expect(actual2).to.eql(expected);
});

it(`Then removing producer 2 should result in no consumption changes`, () => {
it(`Then removing producer 2 should result in a consumption change`, () => {
instance.removeInjector({
injectionId: `foo`,
injector: producer2
});

const expected = [element1, element2];
const expected = [element1];
const actual = consumed1;
expect(actual).to.eql(expected);

Expand All @@ -159,10 +167,6 @@ describe(`Given the Injectables Provider`, () => {
injectionId: `foo`,
injector: producer1
});
instance.removeInjector({
injectionId: `foo`,
injector: producer3
});

const expected = [];
const actual = consumed1;
Expand Down

0 comments on commit 59ee4b0

Please sign in to comment.