From 59ee4b06b3f535ab3a982e70998bf78816526e3b Mon Sep 17 00:00:00 2001 From: Sean Matheson Date: Wed, 6 Apr 2016 23:15:25 +0100 Subject: [PATCH] fix(Injector): Injectors were not updating for prop updates. Updated props being passed through to injectors were not resolving in updates occurring on their inject functions. --- README.md | 47 ++++++++++++---------- examples/router/devServer/devServer.js | 4 +- examples/router/src/PageOne.js | 55 +++++++++++++++++++++++--- src/Injector.js | 29 ++++++++++---- src/Provider.js | 27 +++++++++++-- test/Provider.test.js | 36 +++++++++-------- 6 files changed, 141 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 3d7a564..ea95764 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 }) => ( -
- {injected.length > 0 - ? injected - :
Nothing has been injected
} -
-); -Sidebar.propTypes = { - injected: PropTypes.arrayOf(PropTypes.element) -}; +// Note the 'injected' prop. This will contain any injected elements. +function Sidebar({ injected }) { + return ( +
+ {injected} +
+ ); +} // We wrap our Sidebar component with Injectable. This does all the wiring up for us. export default Injectable(Sidebar); @@ -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 (); +} export default Injector({ - to: InjectableSidebar, - inject: function (props) { return (); } + to: InjectableSidebar, // You have to specify the actual Injectable Component. Explicit. + inject: Inject // The inject function or a React Element. })(ProductsPage); ``` @@ -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 diff --git a/examples/router/devServer/devServer.js b/examples/router/devServer/devServer.js index 9f132f4..eaa0386 100644 --- a/examples/router/devServer/devServer.js +++ b/examples/router/devServer/devServer.js @@ -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 }); diff --git a/examples/router/src/PageOne.js b/examples/router/src/PageOne.js index f0d1342..215c230 100644 --- a/examples/router/src/PageOne.js +++ b/examples/router/src/PageOne.js @@ -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 }) => (
- I am page one. +

I am page one.

+

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

); -export default Injector({ +Content.propTypes = { + active: PropTypes.bool.isRequired +}; + +const Inject = ({ active }) => ( +
+

Injection from Page One

+

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

+
+); +Inject.propTypes = { + active: PropTypes.bool.isRequired +}; + +const InjectingContent = Injector({ to: InjectableHeader, - inject: () =>
Injection from Page One
-})(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 ( +
+ + + +
+ ); + } +} + +export default PageOne; diff --git a/src/Injector.js b/src/Injector.js index e5b885b..5f16921 100644 --- a/src/Injector.js +++ b/src/Injector.js @@ -14,6 +14,7 @@ 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 }; @@ -21,7 +22,26 @@ const Injector = (args: { to: Object, inject: Inject }) => { 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; + } }); } @@ -32,13 +52,6 @@ const Injector = (args: { to: Object, inject: Inject }) => { }); } - getInjectElement = () => { - if (typeof inject === `function`) { - return inject(this.props); - } - return inject; - } - render() { return (); } diff --git a/src/Provider.js b/src/Provider.js index 20ce1a5..c071d59 100644 --- a/src/Provider.js +++ b/src/Provider.js @@ -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, }; @@ -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) @@ -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); @@ -97,8 +100,9 @@ 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 }); @@ -106,7 +110,7 @@ class InjectablesProvider extends Component { return; } - const newInjection = { injector, injectorId }; + const newInjection = { injectorId, injector, inject }; registration.injections = [ ...registration.injections, newInjection @@ -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 }); diff --git a/test/Provider.test.js b/test/Provider.test.js index 2e81600..ee7e530 100644 --- a/test/Provider.test.js +++ b/test/Provider.test.js @@ -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; @@ -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`, () => { @@ -42,7 +45,8 @@ describe(`Given the Injectables Provider`, () => { instance.registerInjector({ injectionId: `foo`, injectorId: `injector1`, - injector: producer1 + injector: producer1, + inject: inject1 }); const expected = [element1]; @@ -69,7 +73,8 @@ describe(`Given the Injectables Provider`, () => { instance.registerInjector({ injectionId: `foo`, injectorId: `injector1`, - injector: producer1 + injector: producer1, + inject: inject1 }); const expected = []; @@ -99,7 +104,8 @@ describe(`Given the Injectables Provider`, () => { instance.registerInjector({ injectionId: `foo`, injectorId: `injector1`, - injector: producer1 + injector: producer1, + inject: inject1 }); const expected = [element1]; @@ -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]; @@ -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]; @@ -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); @@ -159,10 +167,6 @@ describe(`Given the Injectables Provider`, () => { injectionId: `foo`, injector: producer1 }); - instance.removeInjector({ - injectionId: `foo`, - injector: producer3 - }); const expected = []; const actual = consumed1;