Skip to content

Commit

Permalink
feat(react-core): implement dependencies support for PluginContainer (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
gsobolev authored Aug 2, 2017
1 parent 9dd4273 commit 6ef3be6
Show file tree
Hide file tree
Showing 16 changed files with 454 additions and 183 deletions.
40 changes: 34 additions & 6 deletions packages/dx-core/src/plugin-host.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
import { sortPlugins } from './utils';
import { insertPlugin } from './utils';

const getDependencyError = (pluginName, dependencyName) =>
new Error(`The '${pluginName}' plugin requires '${dependencyName}' to be defined before it.`);

export class PluginHost {
constructor() {
this.plugins = [];
this.subscriptions = [];
this.gettersCache = {};
}
ensureDependencies() {
const defined = new Set();
const unresolved = this.plugins
.slice()
.reverse()
.reduce((acc, plugin) => {
if (!plugin.pluginName) return acc;

defined.add(plugin.pluginName);
return [
...acc.filter(item => item.dependencyName !== plugin.pluginName),
...plugin.dependencies
.filter(dependency => !dependency.optional || defined.has(dependency.pluginName))
.map(dependency => ({
pluginName: plugin.pluginName,
dependencyName: dependency.pluginName,
})),
];
}, [])[0];

if (unresolved) {
throw (getDependencyError(unresolved.pluginName, unresolved.dependencyName));
}
}
registerPlugin(plugin) {
this.plugins.push(plugin);
this.plugins = insertPlugin(this.plugins, plugin);
this.cleanPluginsCache();
}
unregisterPlugin(plugin) {
this.plugins.splice(this.plugins.indexOf(plugin), 1);
this.cleanPluginsCache();
}
cleanPluginsCache() {
this.unordered = true;
this.validationRequired = true;
this.gettersCache = {};
}
collect(key, upTo) {
if (this.unordered) {
this.plugins = sortPlugins(this.plugins);
this.unordered = false;
if (this.validationRequired) {
this.ensureDependencies();
this.validationRequired = false;
}

if (!this.gettersCache[key]) {
this.gettersCache[key] = this.plugins.map(plugin => plugin[key]).filter(plugin => !!plugin);
}
Expand Down
75 changes: 75 additions & 0 deletions packages/dx-core/src/plugin-host.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,81 @@ describe('PluginHost', () => {
host.registerPlugin(plugin3);
expect(host.collect('something')).toEqual([1, 2, 3]);
});

it('should validate dependencies after a pluginContainer was registered', () => {
const plugin1 = {
position: () => [0],
pluginName: 'Plugin1',
dependencies: [],
container: true,
};
const plugin2 = {
position: () => [2],
pluginName: 'Plugin2',
dependencies: [
{ pluginName: 'Plugin1' },
{ pluginName: 'Plugin3' },
{ pluginName: 'Plugin4', optional: true },
],
container: true,
};

expect(() => {
host.registerPlugin(plugin1);
host.collect();
}).not.toThrow();
expect(() => {
host.registerPlugin(plugin2);
host.collect();
}).toThrow(/Plugin2.*Plugin3/);
});

it('should validate optional dependencies after a pluginContainer was registered', () => {
const plugin1 = {
position: () => [1],
pluginName: 'Plugin1',
dependencies: [
{ pluginName: 'Plugin2', optional: true },
],
container: true,
};
const plugin2 = {
position: () => [2],
pluginName: 'Plugin2',
dependencies: [],
container: true,
};

host.registerPlugin(plugin1);

expect(() => {
host.registerPlugin(plugin2);
host.collect();
}).toThrow(/Plugin1.*Plugin2/);
});

it('should validate dependencies after a pluginContainer was unregistered', () => {
const plugin1 = {
position: () => [0],
pluginName: 'Plugin1',
dependencies: [],
container: true,
};
const plugin2 = {
position: () => [1],
pluginName: 'Plugin2',
dependencies: [{ pluginName: 'Plugin1' }],
container: true,
};

host.registerPlugin(plugin1);
host.registerPlugin(plugin2);

expect(() => {
host.unregisterPlugin(plugin1);
host.collect();
}).toThrow(/Plugin2.*Plugin1/);
});
});

describe('#registerSubscription', () => {
Expand Down
25 changes: 14 additions & 11 deletions packages/dx-core/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
export const sortPlugins = (plugins) => {
const result = plugins.slice();
result.sort((a, b) => {
const aPosition = a.position();
const bPosition = b.position();
for (let i = 0; i < Math.min(aPosition.length, bPosition.length); i += 1) {
if (aPosition[i] < bPosition[i]) return -1;
if (aPosition[i] > bPosition[i]) return 1;
}
return aPosition.length - bPosition.length;
});
const compare = (a, b) => {
const aPosition = a.position();
const bPosition = b.position();
for (let i = 0; i < Math.min(aPosition.length, bPosition.length); i += 1) {
if (aPosition[i] < bPosition[i]) return -1;
if (aPosition[i] > bPosition[i]) return 1;
}
return aPosition.length - bPosition.length;
};

export const insertPlugin = (array, newItem) => {
const result = array.slice();
const targetIndex = array.findIndex(item => compare(newItem, item) < 0);
result.splice(targetIndex < 0 ? array.length : targetIndex, 0, newItem);
return result;
};
39 changes: 19 additions & 20 deletions packages/dx-core/src/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { sortPlugins } from './utils';
import { insertPlugin } from './utils';

describe('utils', () => {
describe('sortPlugins', () => {
it('should work', () => {
const plugins = [{
position: () => [1, 0],
}, {
position: () => [10],
}, {
position: () => [3],
}, {
position: () => [1, 1],
}, {
position: () => [2, 0, 12],
}, {
position: () => [2, 0, 0],
}, {
position: () => [0],
}];
describe('#insertPlugin', () => {
const mapPlugins = plugins => plugins.map(p => p.position().join());

expect(sortPlugins(plugins).map(plugin => plugin.position().join()))
.toEqual(['0', '1,0', '1,1', '2,0,0', '2,0,12', '3', '10']);
it('should work correctly', () => {
const plugins = [
{ position: () => [1] },
{ position: () => [5, 3] },
];

expect(mapPlugins(insertPlugin(plugins, { position: () => [0] })))
.toEqual(['0', '1', '5,3']);
expect(mapPlugins(insertPlugin(plugins, { position: () => [3, 2, 0] })))
.toEqual(['1', '3,2,0', '5,3']);
expect(mapPlugins(insertPlugin(plugins, { position: () => [5, 2] })))
.toEqual(['1', '5,2', '5,3']);
expect(mapPlugins(insertPlugin(plugins, { position: () => [5, 3, 1] })))
.toEqual(['1', '5,3', '5,3,1']);
expect(mapPlugins(insertPlugin(plugins, { position: () => [7] })))
.toEqual(['1', '5,3', '7']);
});
});
});
1 change: 1 addition & 0 deletions packages/dx-react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"output": "../../shippable/testresults/dx-react-core.xml"
},
"devDependencies": {
"@devexpress/dx-testing": "1.0.0-alpha.5",
"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-jest": "^20.0.3",
Expand Down
5 changes: 4 additions & 1 deletion packages/dx-react-core/src/plugged/action.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ export class Action extends React.PureComponent {
return null;
}
}

Action.propTypes = {
position: PropTypes.func,
name: PropTypes.string.isRequired,
action: PropTypes.func.isRequired,
};

Action.defaultProps = {
position: () => NaN,
position: null,
};

Action.contextTypes = {
pluginHost: PropTypes.object.isRequired,
};
2 changes: 1 addition & 1 deletion packages/dx-react-core/src/plugged/action.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('Action', () => {
const onAction = jest.fn();
const tree = mount(
<Test onAction={onAction} />,
);
);

tree.find('button').simulate('click');
expect(onAction.mock.calls).toHaveLength(1);
Expand Down
99 changes: 38 additions & 61 deletions packages/dx-react-core/src/plugged/container.jsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Getter } from './getter';
import { Action } from './action';
import { Template } from './template';

const CONTAINER_CONTEXT = 'pluginContainerContext';

export const PluginContainer = (
{ children },
{ [CONTAINER_CONTEXT]: containerContext },
) => (
<div style={{ display: 'none' }}>
{
React.Children.map(children, (child, index) => {
if (!child || !child.type) return child;

const childPosition = () => {
const calculatedPosition =
(containerContext && containerContext()) || [];
return [...calculatedPosition, index];
};

if (child.type === Getter ||
child.type === Action ||
child.type === Template) {
return React.cloneElement(child, { position: childPosition });
}

return (
<PluginContainerContext position={childPosition}>
{child}
</PluginContainerContext>
);
})
}
</div>
);

PluginContainer.defaultProps = {
children: null,
};

PluginContainer.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};

PluginContainer.contextTypes = {
[CONTAINER_CONTEXT]: PropTypes.func,
};

class PluginContainerContext extends React.Component {
getChildContext() {
return {
[CONTAINER_CONTEXT]: this.props.position,
import { PluginIndexer } from './indexer';

export class PluginContainer extends React.PureComponent {
componentWillMount() {
const { pluginHost, positionContext: position } = this.context;
const { pluginName, dependencies } = this.props;
this.plugin = {
position,
pluginName,
dependencies,
container: true,
};
pluginHost.registerPlugin(this.plugin);
}
componentWillUnmount() {
const { pluginHost } = this.context;
pluginHost.unregisterPlugin(this.plugin);
}
render() {
return this.props.children;
const { children } = this.props;
return (
<PluginIndexer>
{children}
</PluginIndexer>
);
}
}

PluginContainerContext.propTypes = {
position: PropTypes.func.isRequired,
PluginContainer.propTypes = {
children: PropTypes.node.isRequired,
pluginName: PropTypes.string,
dependencies: PropTypes.arrayOf(
PropTypes.shape({
pluginName: PropTypes.string,
optional: PropTypes.bool,
}),
),
};

PluginContainerContext.childContextTypes = {
[CONTAINER_CONTEXT]: PropTypes.func,
PluginContainer.defaultProps = {
pluginName: '',
dependencies: [],
};

PluginContainer.contextTypes = {
pluginHost: PropTypes.object.isRequired,
positionContext: PropTypes.func.isRequired,
};
Loading

0 comments on commit 6ef3be6

Please sign in to comment.