react_webpack_rails 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
});
|