react_webpack_rails 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.babelrc +3 -0
- data/.gitignore +3 -0
- data/.npmignore +1 -0
- data/.travis.yml +37 -3
- data/CHANGELOG.md +56 -0
- data/README.md +6 -0
- data/Rakefile +42 -6
- data/bin/setup +2 -0
- data/docs/README.md +10 -9
- data/docs/api.md +103 -99
- data/js/.eslintrc +12 -0
- data/js/src/env.js +1 -0
- data/js/src/index.js +30 -0
- data/js/src/integrations-manager.js +22 -0
- data/js/src/integrations/react-router.js +48 -0
- data/js/src/integrations/react.js +49 -0
- data/js/src/nodes.js +51 -0
- data/js/src/version.js +1 -0
- data/js/test/env.spec.js +10 -0
- data/js/test/integrations-manager.spec.js +38 -0
- data/js/test/integrations/react-router.spec.js +133 -0
- data/js/test/integrations/react.spec.js +96 -0
- data/js/test/nodes.spec.js +121 -0
- data/lib/assets/javascripts/react_integration.js.erb +74 -155
- data/lib/generators/react_webpack_rails/templates/.babelrc +1 -1
- data/lib/generators/react_webpack_rails/templates/karma.conf.js +1 -1
- data/lib/generators/react_webpack_rails/templates/package.json.erb +9 -3
- data/lib/generators/react_webpack_rails/templates/react/index.js.erb +4 -6
- data/lib/generators/react_webpack_rails/templates/webpack.config.js +1 -1
- data/lib/react_webpack_rails/railtie.rb +5 -0
- data/lib/react_webpack_rails/version.rb +1 -1
- data/lib/react_webpack_rails/view_helpers.rb +24 -12
- data/package.json +49 -0
- metadata +19 -4
data/js/.eslintrc
ADDED
data/js/src/env.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export default (typeof __RWR_ENV__ !== 'undefined') ? __RWR_ENV__ : {};
|
data/js/src/index.js
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
import env from './env';
|
2
|
+
import nodes from './nodes';
|
3
|
+
import integrationsManager from './integrations-manager';
|
4
|
+
import react from './integrations/react';
|
5
|
+
import reactRouter from './integrations/react-router';
|
6
|
+
import version from './version';
|
7
|
+
|
8
|
+
export { react as react };
|
9
|
+
export { nodes as nodes };
|
10
|
+
export { integrationsManager as integrationsManager };
|
11
|
+
export { env as env };
|
12
|
+
export { reactRouter as reactRouter };
|
13
|
+
|
14
|
+
export default {
|
15
|
+
version: version,
|
16
|
+
|
17
|
+
registerComponent: react.registerComponent,
|
18
|
+
getComponent: react.getComponent,
|
19
|
+
createComponent: react.createComponent,
|
20
|
+
renderComponent: react.renderComponent,
|
21
|
+
unmountComponent: react.unmountComponent,
|
22
|
+
|
23
|
+
renderRouter: reactRouter.renderRouter,
|
24
|
+
registerRouter: reactRouter.registerRouter,
|
25
|
+
unmountRouter: reactRouter.unmountRouter,
|
26
|
+
getRouter: reactRouter.getRouter,
|
27
|
+
|
28
|
+
mountNodes: nodes.mountNodes,
|
29
|
+
unmountNodes: nodes.unmountNodes,
|
30
|
+
};
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import reactIntegration from './integrations/react';
|
2
|
+
import reactRouterIntegration from './integrations/react-router';
|
3
|
+
|
4
|
+
class IntegrationsManager {
|
5
|
+
constructor() {
|
6
|
+
this.integrations = {
|
7
|
+
'react-component': reactIntegration.integrationWrapper,
|
8
|
+
'react-router': reactRouterIntegration.integrationWrapper,
|
9
|
+
};
|
10
|
+
}
|
11
|
+
|
12
|
+
get(name) {
|
13
|
+
// handle missing one here;
|
14
|
+
return this.integrations[name];
|
15
|
+
}
|
16
|
+
|
17
|
+
register(name, integration) {
|
18
|
+
this.integrations[name] = integration;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
export default new IntegrationsManager;
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import ReactDOM from 'react-dom';
|
2
|
+
|
3
|
+
class ReactRouterIntegration {
|
4
|
+
constructor() {
|
5
|
+
this.routers = {};
|
6
|
+
this.enabled = false;
|
7
|
+
this.registerRouter = this.registerRouter.bind(this);
|
8
|
+
this.getRouter = this.getRouter.bind(this);
|
9
|
+
this.renderRouter = this.renderRouter.bind(this);
|
10
|
+
}
|
11
|
+
|
12
|
+
registerRouter(name, route) {
|
13
|
+
this.routers[name] = route;
|
14
|
+
}
|
15
|
+
|
16
|
+
getRouter(name) {
|
17
|
+
return this.routers[name];
|
18
|
+
}
|
19
|
+
|
20
|
+
renderRouter(name, node) {
|
21
|
+
if (this.enabled === true) {
|
22
|
+
throw new Error(
|
23
|
+
`Error when rendering ${name}\n\trenderRouter: can't render more than one router.`
|
24
|
+
);
|
25
|
+
}
|
26
|
+
this.enabled = true;
|
27
|
+
ReactDOM.render(this.getRouter(name), node);
|
28
|
+
}
|
29
|
+
|
30
|
+
unmountRouter(node) {
|
31
|
+
ReactDOM.unmountComponentAtNode(node);
|
32
|
+
this.enabled = false;
|
33
|
+
}
|
34
|
+
|
35
|
+
get integrationWrapper() {
|
36
|
+
return {
|
37
|
+
mount: function _mount(node, payload) {
|
38
|
+
this.renderRouter(payload.name, node);
|
39
|
+
}.bind(this),
|
40
|
+
|
41
|
+
unmount: function _unmount(node) {
|
42
|
+
this.unmountRouter(node);
|
43
|
+
}.bind(this),
|
44
|
+
};
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
export default new ReactRouterIntegration;
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import ReactDOM from 'react-dom';
|
3
|
+
|
4
|
+
class ReactIntegration {
|
5
|
+
constructor() {
|
6
|
+
this.components = {};
|
7
|
+
this.registerComponent = this.registerComponent.bind(this);
|
8
|
+
this.getComponent = this.getComponent.bind(this);
|
9
|
+
this.createComponent = this.createComponent.bind(this);
|
10
|
+
this.renderComponent = this.renderComponent.bind(this);
|
11
|
+
this.unmountComponent = this.unmountComponent.bind(this);
|
12
|
+
}
|
13
|
+
|
14
|
+
registerComponent(name, component) {
|
15
|
+
this.components[name] = component;
|
16
|
+
}
|
17
|
+
|
18
|
+
getComponent(name) {
|
19
|
+
return this.components[name];
|
20
|
+
}
|
21
|
+
|
22
|
+
createComponent(name, props) {
|
23
|
+
const constructor = this.getComponent(name);
|
24
|
+
return React.createElement(constructor, props);
|
25
|
+
}
|
26
|
+
|
27
|
+
renderComponent(name, props, node) {
|
28
|
+
const component = this.createComponent(name, props);
|
29
|
+
ReactDOM.render(component, node);
|
30
|
+
}
|
31
|
+
|
32
|
+
unmountComponent(node) {
|
33
|
+
ReactDOM.unmountComponentAtNode(node);
|
34
|
+
}
|
35
|
+
|
36
|
+
get integrationWrapper() {
|
37
|
+
return {
|
38
|
+
mount: function _mount(node, payload) {
|
39
|
+
this.renderComponent(payload.name, payload.props, node);
|
40
|
+
}.bind(this),
|
41
|
+
|
42
|
+
unmount: function _unmount(node) {
|
43
|
+
this.unmountComponent(node);
|
44
|
+
}.bind(this),
|
45
|
+
};
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
export default new ReactIntegration;
|
data/js/src/nodes.js
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
import IntegrationsManager from './integrations-manager';
|
2
|
+
|
3
|
+
const ELEMENT_ATTR = 'data-react-element';
|
4
|
+
const PAYLOAD_ATTR = 'data-payload';
|
5
|
+
const INTEGRATION_NAME_ATTR = 'data-integration-name';
|
6
|
+
const OPTIONS_ATTR = 'data-options';
|
7
|
+
|
8
|
+
|
9
|
+
function _findDOMNodes(searchSelector) {
|
10
|
+
const selector = searchSelector || '[' + ELEMENT_ATTR + ']';
|
11
|
+
return document.querySelectorAll(selector);
|
12
|
+
}
|
13
|
+
|
14
|
+
function _nodeData(node) {
|
15
|
+
const rawPayload = node.getAttribute(PAYLOAD_ATTR);
|
16
|
+
const rawOptions = node.getAttribute(OPTIONS_ATTR);
|
17
|
+
|
18
|
+
return {
|
19
|
+
payload: rawPayload && JSON.parse(rawPayload),
|
20
|
+
options: rawOptions && JSON.parse(rawOptions),
|
21
|
+
integrationName: node.getAttribute(INTEGRATION_NAME_ATTR),
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
25
|
+
function _mountNode(node) {
|
26
|
+
const data = _nodeData(node);
|
27
|
+
const mount = IntegrationsManager.get(data.integrationName).mount;
|
28
|
+
if (typeof(mount) === 'function') { mount(node, data.payload); }
|
29
|
+
}
|
30
|
+
|
31
|
+
function _unmountNode(node) {
|
32
|
+
const data = _nodeData(node);
|
33
|
+
const unmount = IntegrationsManager.get(data.integrationName).unmount;
|
34
|
+
if (typeof(unmount) === 'function') { unmount(node, data.payload); }
|
35
|
+
}
|
36
|
+
|
37
|
+
export default {
|
38
|
+
mountNodes: function _mountNodes(searchSelector) {
|
39
|
+
const nodes = _findDOMNodes(searchSelector);
|
40
|
+
for (let i = 0; i < nodes.length; ++i) {
|
41
|
+
_mountNode(nodes[i]);
|
42
|
+
}
|
43
|
+
},
|
44
|
+
|
45
|
+
unmountNodes: function _unmountNodes(searchSelector) {
|
46
|
+
const nodes = _findDOMNodes(searchSelector);
|
47
|
+
for (let i = 0; i < nodes.length; ++i) {
|
48
|
+
_unmountNode(nodes[i]);
|
49
|
+
}
|
50
|
+
},
|
51
|
+
};
|
data/js/src/version.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export default '0.1.0';
|
data/js/test/env.spec.js
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import expect from 'expect';
|
2
|
+
import reactIntegration from '../src/integrations/react';
|
3
|
+
import reactRouterIntegration from '../src/integrations/react-router';
|
4
|
+
import subject from '../src/integrations-manager';
|
5
|
+
|
6
|
+
describe('IntegrationsManager', function () {
|
7
|
+
describe('.constructor', function () {
|
8
|
+
it('by default sets react and react-router integrations', function () {
|
9
|
+
expect(subject.integrations['react-component']).toEqual(
|
10
|
+
reactIntegration.integrationWrapper
|
11
|
+
);
|
12
|
+
expect(subject.integrations['react-router']).toEqual(
|
13
|
+
reactRouterIntegration.integrationWrapper
|
14
|
+
);
|
15
|
+
expect(Object.keys(subject.integrations).length).toEqual(2);
|
16
|
+
});
|
17
|
+
});
|
18
|
+
|
19
|
+
describe('#get', function () {
|
20
|
+
it('returns integration by name', function () {
|
21
|
+
expect(subject.get('react-component')).toEqual(
|
22
|
+
reactIntegration.integrationWrapper
|
23
|
+
);
|
24
|
+
});
|
25
|
+
|
26
|
+
it('returns undefined if name is invalid', function () {
|
27
|
+
expect(subject.get('invalidName')).toBe(undefined);
|
28
|
+
});
|
29
|
+
});
|
30
|
+
|
31
|
+
describe('#register', function () {
|
32
|
+
it('registers integration', function () {
|
33
|
+
subject.register('newIntegration', {});
|
34
|
+
|
35
|
+
expect(subject.integrations.newIntegration).toEqual({});
|
36
|
+
});
|
37
|
+
});
|
38
|
+
});
|
@@ -0,0 +1,133 @@
|
|
1
|
+
import expect, { spyOn } from 'expect';
|
2
|
+
import ReactDOM from 'react-dom';
|
3
|
+
import subject from '../../src/integrations/react-router';
|
4
|
+
|
5
|
+
const HelloRouter = {};
|
6
|
+
|
7
|
+
describe('ReactIntegration', function () {
|
8
|
+
afterEach(function () {
|
9
|
+
subject.routers = {};
|
10
|
+
subject.enabled = false;
|
11
|
+
});
|
12
|
+
|
13
|
+
describe('.constructor', function () {
|
14
|
+
it('intializes empty components dictionary', function () {
|
15
|
+
expect(subject.routers).toEqual({});
|
16
|
+
});
|
17
|
+
|
18
|
+
it('by default sets router presence flag to false', function () {
|
19
|
+
expect(subject.enabled).toBe(false);
|
20
|
+
});
|
21
|
+
});
|
22
|
+
|
23
|
+
describe('#registerRouter', function () {
|
24
|
+
it('adds router to the routers storage', function () {
|
25
|
+
subject.registerRouter('HelloRouter', HelloRouter);
|
26
|
+
expect(subject.routers.HelloRouter).toBe(HelloRouter);
|
27
|
+
});
|
28
|
+
});
|
29
|
+
|
30
|
+
describe('#getRouter', function () {
|
31
|
+
it('returns route by name', function () {
|
32
|
+
subject.registerRouter('HelloRouter', HelloRouter);
|
33
|
+
expect(subject.getRouter('HelloRouter')).toBe(HelloRouter);
|
34
|
+
});
|
35
|
+
|
36
|
+
it('returns undefined if route is not found', function () {
|
37
|
+
expect(subject.getRouter('HelloRouter')).toBe(undefined);
|
38
|
+
});
|
39
|
+
});
|
40
|
+
|
41
|
+
describe('#renderRouter', function () {
|
42
|
+
beforeEach(function () {
|
43
|
+
subject.registerRouter('HelloRouter', HelloRouter);
|
44
|
+
});
|
45
|
+
|
46
|
+
context('when router is present', function () {
|
47
|
+
it('throws an error', function () {
|
48
|
+
expect(function () {
|
49
|
+
subject.renderRouter('HelloRouter', { node: 'someNode' });
|
50
|
+
})
|
51
|
+
.withContext(subject.enabled = true)
|
52
|
+
.toThrow(/Error when rendering HelloRouter/);
|
53
|
+
});
|
54
|
+
});
|
55
|
+
|
56
|
+
context('when router does not exist', function () {
|
57
|
+
let ReactDOMSpy;
|
58
|
+
const node = { nodeType: 1, nodeName: 'DIV' };
|
59
|
+
beforeEach(function () {
|
60
|
+
ReactDOMSpy = spyOn(ReactDOM, 'render');
|
61
|
+
});
|
62
|
+
|
63
|
+
afterEach(function () {
|
64
|
+
expect.restoreSpies();
|
65
|
+
});
|
66
|
+
|
67
|
+
it('sets router presence flag to true', function () {
|
68
|
+
subject.renderRouter('HelloRouter', node);
|
69
|
+
|
70
|
+
expect(subject.enabled).toBe(true);
|
71
|
+
});
|
72
|
+
|
73
|
+
it('calls ReactDOM renderer once', function () {
|
74
|
+
subject.renderRouter('HelloRouter', { nodeType: 1, nodeName: 'DIV' });
|
75
|
+
|
76
|
+
expect(ReactDOMSpy.calls.length).toEqual(1);
|
77
|
+
expect(ReactDOMSpy).toHaveBeenCalledWith(HelloRouter, { nodeType: 1, nodeName: 'DIV' });
|
78
|
+
});
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('#unmountRouter', function () {
|
83
|
+
let ReactDOMSpy;
|
84
|
+
beforeEach(function () {
|
85
|
+
ReactDOMSpy = spyOn(ReactDOM, 'unmountComponentAtNode');
|
86
|
+
});
|
87
|
+
|
88
|
+
afterEach(function () {
|
89
|
+
expect.restoreSpies();
|
90
|
+
});
|
91
|
+
|
92
|
+
it('unmounts router component at specified node', function () {
|
93
|
+
subject.unmountRouter({ node: 'someNode' });
|
94
|
+
|
95
|
+
expect(ReactDOMSpy.calls.length).toEqual(1);
|
96
|
+
expect(ReactDOMSpy).toHaveBeenCalledWith({ node: 'someNode' });
|
97
|
+
});
|
98
|
+
|
99
|
+
it('allows to add new router', function () {
|
100
|
+
subject.unmountRouter({ node: 'someNode' });
|
101
|
+
expect(subject.enabled).toBe(false);
|
102
|
+
});
|
103
|
+
});
|
104
|
+
|
105
|
+
describe('#integrationWrapper', function () {
|
106
|
+
const node = { nodeType: 1, nodeName: 'DIV' };
|
107
|
+
|
108
|
+
describe('function mount', function () {
|
109
|
+
it('calls renderComponent', function () {
|
110
|
+
const mountSpy = spyOn(subject, 'renderRouter');
|
111
|
+
|
112
|
+
const payload = { name: 'routerName' };
|
113
|
+
subject.integrationWrapper.mount(node, payload);
|
114
|
+
|
115
|
+
expect(mountSpy.calls.length).toEqual(1);
|
116
|
+
expect(mountSpy).toHaveBeenCalledWith(
|
117
|
+
'routerName',
|
118
|
+
{ nodeType: 1, nodeName: 'DIV' }
|
119
|
+
);
|
120
|
+
});
|
121
|
+
});
|
122
|
+
|
123
|
+
describe('function unmount', function () {
|
124
|
+
it('calls unmountComponent', function () {
|
125
|
+
const unmountSpy = spyOn(subject, 'unmountRouter');
|
126
|
+
subject.integrationWrapper.unmount(node);
|
127
|
+
|
128
|
+
expect(unmountSpy.calls.length).toEqual(1);
|
129
|
+
expect(unmountSpy).toHaveBeenCalledWith({ nodeType: 1, nodeName: 'DIV' });
|
130
|
+
});
|
131
|
+
});
|
132
|
+
});
|
133
|
+
});
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import expect, { spyOn } from 'expect';
|
2
|
+
import React, { PropTypes } from 'react';
|
3
|
+
import ReactDOM from 'react-dom';
|
4
|
+
import subject from '../../src/integrations/react';
|
5
|
+
|
6
|
+
class HelloComponent extends React.Component {
|
7
|
+
static propTypes() {
|
8
|
+
return {
|
9
|
+
username: PropTypes.string.isRequired,
|
10
|
+
};
|
11
|
+
}
|
12
|
+
|
13
|
+
render() {
|
14
|
+
return (<div>Hello World! {this.props.username}</div>);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
describe('ReactIntegration', function () {
|
19
|
+
afterEach(function () {
|
20
|
+
subject.components = {};
|
21
|
+
});
|
22
|
+
|
23
|
+
describe('.constructor', function () {
|
24
|
+
it('intializes empty components dictionary', function () {
|
25
|
+
expect(subject.components).toEqual({});
|
26
|
+
});
|
27
|
+
});
|
28
|
+
|
29
|
+
describe('#registerComponent', function () {
|
30
|
+
it('adds component to the components storage', function () {
|
31
|
+
subject.registerComponent('HelloWorld', HelloComponent);
|
32
|
+
expect(subject.components.HelloWorld).toBe(HelloComponent);
|
33
|
+
});
|
34
|
+
});
|
35
|
+
|
36
|
+
describe('#getComponent', function () {
|
37
|
+
it('returns component by name', function () {
|
38
|
+
subject.registerComponent('HelloWorld', HelloComponent);
|
39
|
+
expect(subject.getComponent('HelloWorld')).toBe(HelloComponent);
|
40
|
+
});
|
41
|
+
|
42
|
+
it('returns undefined if component is not found', function () {
|
43
|
+
expect(subject.getComponent('HelloWorld')).toBe(undefined);
|
44
|
+
});
|
45
|
+
});
|
46
|
+
|
47
|
+
describe('#createComponent', function () {
|
48
|
+
it('creates component with given props', function () {
|
49
|
+
subject.registerComponent('HelloWorld', HelloComponent);
|
50
|
+
const component = subject.createComponent('HelloWorld', { username: 'testUser' });
|
51
|
+
|
52
|
+
expect(component.props).toEqual({ username: 'testUser' });
|
53
|
+
expect(component.type).toBe(HelloComponent);
|
54
|
+
});
|
55
|
+
});
|
56
|
+
|
57
|
+
describe('#unmountComponent', function () {
|
58
|
+
it('unmount component at specified node', function () {
|
59
|
+
const node = { nodeType: 1, nodeName: 'DIV' };
|
60
|
+
const unmountSpy = spyOn(ReactDOM, 'unmountComponentAtNode');
|
61
|
+
subject.unmountComponent(node);
|
62
|
+
|
63
|
+
expect(unmountSpy.calls.length).toEqual(1);
|
64
|
+
expect(unmountSpy).toHaveBeenCalledWith({ nodeType: 1, nodeName: 'DIV' });
|
65
|
+
});
|
66
|
+
});
|
67
|
+
|
68
|
+
describe('#integrationWrapper', function () {
|
69
|
+
const node = { nodeType: 1, nodeName: 'DIV' };
|
70
|
+
|
71
|
+
describe('function mount', function () {
|
72
|
+
it('calls renderComponent', function () {
|
73
|
+
const payload = { name: 'componentName', props: { username: 'testUser' } };
|
74
|
+
const mountSpy = spyOn(subject, 'renderComponent');
|
75
|
+
subject.integrationWrapper.mount(node, payload);
|
76
|
+
|
77
|
+
expect(mountSpy.calls.length).toEqual(1);
|
78
|
+
expect(mountSpy).toHaveBeenCalledWith(
|
79
|
+
'componentName',
|
80
|
+
{ username: 'testUser' },
|
81
|
+
{ nodeType: 1, nodeName: 'DIV' }
|
82
|
+
);
|
83
|
+
});
|
84
|
+
});
|
85
|
+
|
86
|
+
describe('function unmount', function () {
|
87
|
+
it('calls unmountComponent', function () {
|
88
|
+
const unmountSpy = spyOn(subject, 'unmountComponent');
|
89
|
+
subject.integrationWrapper.unmount(node);
|
90
|
+
|
91
|
+
expect(unmountSpy.calls.length).toEqual(1);
|
92
|
+
expect(unmountSpy).toHaveBeenCalledWith({ nodeType: 1, nodeName: 'DIV' });
|
93
|
+
});
|
94
|
+
});
|
95
|
+
});
|
96
|
+
});
|