Skip to content

Commit

Permalink
perf(api): Injector API updated to inject via an 'inject' prop.
Browse files Browse the repository at this point in the history
The Injector no longer supports the 'elements' prop, it now has an 'inject' prop which allows you to
pass through either a stateless component (that will recieve the same props as the component being
wrapped) or an element.  Using the stateless component method is preferable as it will only be
executed when needed, whereas passing an element requires you to actually create the element up
front.

closes #1 BREAKING CHANGE: The Injector no longer supports the 'elements' prop, it now has an
'inject' prop which allows you to pass through either a stateless component (that will recieve the
same props as the component being wrapped) or an element. Using the stateless component method is
preferable as it will only be executed when needed, whereas passing an element requires you to
acutally create the element up front.
  • Loading branch information
ctrlplusb committed Apr 6, 2016
1 parent 49316f8 commit c7e282c
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 89 deletions.
1 change: 0 additions & 1 deletion examples/router/src/InjectableHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const Header = ({ injected }) => (
<div style={{ backgroundColor: `red`, color: `white` }}>
<h1>INJECTABLE HEADER</h1>
<div>
<strong>INJECTED ITEMS:</strong>
{injected.length > 0 ? injected : <div>Nothing has been injected</div>}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/router/src/PageOne.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ const PageOne = () => (
);
export default Injector({
to: InjectableHeader,
elements: [<div>Injection from Page One</div>]
inject: () => <div>Injection from Page One</div>
})(PageOne);
10 changes: 9 additions & 1 deletion examples/router/src/PageTwo.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ const PageTwo = () => (
</div>
);

const Inject = (props) => (
<div>
Injection from Page Two.<br />
I also recieved these props:<br />
{Object.keys(props).join(`, `)}
</div>
);

export default Injector({
to: InjectableHeader,
elements: [<div>Injection from Page Two</div>]
inject: Inject
})(PageTwo);
2 changes: 1 addition & 1 deletion examples/router/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ReactDOM.render((
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="pageOne" component={PageOne} />
<Route path="pageTwo" component={PageTwo} />
<Route path="pageTwo" component={() => <PageTwo foo bar baz />} />
</Route>
</Router>
</InjectablesProvider>
Expand Down
2 changes: 1 addition & 1 deletion lib/react-injectables.js

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions src/Injectable.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import React, { PropTypes, Component } from 'react';
import { containsUniq, keyedElements } from './utils';

let injectionIndex = 0;
let injectionIdIndex = 0;

const Injectable = (WrappedComponent) => {
injectionIndex++;
const injectionId = `injection_${injectionIndex}`;
injectionIdIndex++;
const injectionId = `injectionId_${injectionIdIndex}`;

class InjectableComponent extends Component {
static injectionId = injectionId;

static contextTypes = {
consumeElements: PropTypes.func.isRequired,
stopConsumingElements: PropTypes.func.isRequired
registerInjectable: PropTypes.func.isRequired,
removeInjectable: PropTypes.func.isRequired
};

state = {
injected: []
}

componentWillMount() {
this.context.consumeElements({
injectionId: InjectableComponent.injectionId,
this.context.registerInjectable({
injectionId,
injectable: this
});
}

componentWillUnmount() {
this.context.stopConsumingElements({ listener: this });
this.context.removeInjectable({
injectionId,
injectable: this
});
}

consume = (elements) => {
Expand Down
33 changes: 24 additions & 9 deletions src/Injector.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import React, { PropTypes, Component } from 'react';

const Injector = (args: { to: Object, elements : Array<Object> }) => {
const { to, elements } = args;
type Inject = Object | (props: Object) => Object;

let injectorIndex = 0;

const Injector = (args: { to: Object, inject: Inject }) => {
const { to, inject } = args;

injectorIndex++;
const injectorId = `injector_${injectorIndex}`;

return function WrapComponent(WrappedComponent) {
class InjectorComponent extends Component {
static contextTypes = {
produceElements: PropTypes.func.isRequired,
removeProducer: PropTypes.func.isRequired
registerInjector: PropTypes.func.isRequired,
removeInjector: PropTypes.func.isRequired
};

componentWillMount() {
this.context.produceElements({
this.context.registerInjector({
injectionId: to.injectionId,
injector: this,
elements
injectorId,
injector: this
});
}

componentWillUnmount() {
this.context.removeProducer({
injectionId: to.injectionId, injector: this
this.context.removeInjector({
injectionId: to.injectionId,
injector: this
});
}

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

render() {
return (<WrappedComponent {...this.props} />);
}
Expand Down
80 changes: 44 additions & 36 deletions src/Provider.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Children, Component, PropTypes } from 'react';
import { compose, concatAll, find, map, uniq, without, withoutAll } from './utils';
import { compose, find, map, uniqBy, without, withoutAll } from './utils';

class InjectablesProvider extends Component {
static childContextTypes = {
produceElements: PropTypes.func.isRequired,
removeProducer: PropTypes.func.isRequired,
consumeElements: PropTypes.func.isRequired,
stopConsumingElements: PropTypes.func.isRequired,
registerInjector: PropTypes.func.isRequired,
removeInjector: PropTypes.func.isRequired,
registerInjectable: PropTypes.func.isRequired,
removeInjectable: PropTypes.func.isRequired,
};

static propTypes = {
Expand All @@ -20,13 +20,13 @@ class InjectablesProvider extends Component {

getChildContext() {
return {
produceElements: (args) => this.produceElements(args),
registerInjector: (args) => this.registerInjector(args),

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

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

stopConsumingElements: (args) => this.stopConsumingElements(args)
removeInjectable: (args) => this.removeInjectable(args)
};
}

Expand All @@ -38,11 +38,10 @@ class InjectablesProvider extends Component {
)(this.registrations);

if (!registration) {
// Need to create the registration.
registration = {
injectionId,
injectables: [],
injectors: []
injections: []
};

this.registrations.push(registration);
Expand All @@ -51,15 +50,14 @@ class InjectablesProvider extends Component {
return registration;
}

notifyConsumers(args: { registration: Object }) {
runInjections(args: { registration: Object }) {
const { registration } = args;
const { injectables, injectors } = registration;
const { injectables, injections } = registration;

const elements = compose(
uniq,
concatAll,
map(x => x.elements)
)(injectors);
map(x => x.injector.getInjectElement()),
uniqBy(`injectorId`)
)(injections);

injectables.forEach(injectable => {
injectable.consume(elements);
Expand All @@ -71,56 +69,66 @@ class InjectablesProvider extends Component {
this.registrations = without(registration)(this.registrations);
}

consumeElements(args: { injectionId: string, injectable: Object}) {
registerInjectable(args: { injectionId: string, injectable: Object}) {
const { injectionId, injectable } = args;
const registration = this.getRegistration({ injectionId });

if (withoutAll(registration.injectables)([injectable]).length > 0) {
registration.injectables = [...registration.injectables, injectable];
this.notifyConsumers({ registration }); // First time consumption.
this.runInjections({ registration }); // First time consumption.
}
}

stopConsumingElements(args: { injectionId: string, injectable: Object }) {
removeInjectable(args: { injectionId: string, injectable: Object }) {
const { injectionId, injectable } = args;
const registration = this.getRegistration({ injectionId });

const injectables = without(injectable)(registration.injectables);

if (injectables.length === 0 && registration.injectors.length === 0) {
if (injectables.length === 0 && registration.injections.length === 0) {
this.removeRegistration({ registration });
} else {
registration.injectables = injectables;
}
}

findProducer({ registration, injector }) {
return find(x => Object.is(x.injector, injector))(registration.injectors);
findInjection({ registration, injector }) {
return find(x => Object.is(x.injector, injector))(registration.injections);
}

produceElements(args: { injectionId: string, injector: Object, elements: Array<Object> }) {
const { injectionId, injector, elements } = args;
registerInjector(args: { injectionId: string, injectorId: string, injector: Object }) {
const { injectionId, injectorId, injector } = args;
const registration = this.getRegistration({ injectionId });
const existingProducer = this.findProducer({ registration, injector });
const existingInjection = this.findInjection({ registration, injector });

if (existingProducer) {
if (existingInjection) {
return;
}

const newInjector = { injector, elements };
registration.injectors = [
...registration.injectors,
newInjector
const newInjection = { injector, injectorId };
registration.injections = [
...registration.injections,
newInjection
];
this.notifyConsumers({ registration });

this.runInjections({ registration });
}

removeProducer(args: { injectionId: string, injector: Object }) {
removeInjector(args: { injectionId: string, injector: Object }) {
const { injectionId, injector } = args;
const registration = this.getRegistration({ injectionId });
const existingInjector = this.findProducer({ registration, injector });
registration.injectors = without(existingInjector)(registration.injectors);
this.notifyConsumers({ registration });
const injection = this.findInjection({ registration, injector });

if (injection) {
registration.injections = without(injection)(registration.injections);
this.runInjections({ registration });
} else {
/* istanbul ignore next */
if (process.env.NODE_ENV === `development`) {
throw new Error(
`Trying to remove an injector which has not been registered`);
}
}
}

render() {
Expand Down
18 changes: 16 additions & 2 deletions src/utils/index.js → src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, { Children } from 'react';
*/
export function compose(...funcs) {
return (...args) => {
/* istanbul ignore next */
if (funcs.length === 0) {
return args[0];
}
Expand Down Expand Up @@ -47,8 +48,21 @@ export const withoutAll = (toRemove) => (point) =>
(x) => all(y => !Object.is(x, y))(toRemove)
)(point);

// :: [a] -> [a]
export const uniq = y => Array.from(new Set(y));
// :: a -> [b]
export const uniqBy = x => y => {
const checked = new Set();
const result = [];

y.forEach(a => {
const prop = a[x];
if (!checked.has(prop)) {
checked.add(prop);
result.push(a);
}
});

return result;
};

/**
* :: [a] -> [a] -> boolean
Expand Down
Loading

0 comments on commit c7e282c

Please sign in to comment.