rwr-redux 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ import { createElement } from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { render, unmountComponentAtNode } from 'react-dom';
4
+ import { renderToString } from 'react-dom/server';
5
+
6
+ import ReduxStore from './redux-store';
7
+
8
+ class ReduxContainer {
9
+ constructor() {
10
+ this.containers = {};
11
+
12
+ this.registerContainer = this.registerContainer.bind(this);
13
+ }
14
+
15
+ registerContainer(name, container) {
16
+ this.containers[name] = container;
17
+ }
18
+
19
+ getContainer(name) {
20
+ return this.containers[name];
21
+ }
22
+
23
+ createContainer(name) {
24
+ const constructor = this.getContainer(name);
25
+ return createElement(constructor);
26
+ }
27
+
28
+ createRootComponent(name, storeName) {
29
+ const container = this.createContainer(name);
30
+ const store = ReduxStore.getStore(storeName);
31
+
32
+ return createElement(Provider, { store }, container);
33
+ }
34
+
35
+ renderContainer(name, node, storeName) {
36
+ const rootComponent = this.createRootComponent(name, storeName);
37
+ render(rootComponent, node);
38
+ }
39
+
40
+ unmountContainer(node) {
41
+ unmountComponentAtNode(node);
42
+ }
43
+
44
+ renderContainerToString(name, storeName) {
45
+ const rootComponent = this.createRootComponent(name, storeName);
46
+ const result = renderToString(rootComponent);
47
+
48
+ return JSON.stringify({ body: result });
49
+ }
50
+
51
+ get integrationWrapper() {
52
+ return {
53
+ mount: function _mount(node, payload) {
54
+ this.renderContainer(payload.name, node, payload.storeName);
55
+ }.bind(this),
56
+
57
+ unmount: function _unmount(node) {
58
+ this.unmountContainer(node);
59
+ }.bind(this),
60
+
61
+ nodeRun: function _prerender(payload) {
62
+ const { name, storeName } = payload;
63
+ return this.renderContainerToString(name, storeName);
64
+ }.bind(this),
65
+ };
66
+ }
67
+ }
68
+
69
+ export default new ReduxContainer;
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { createElement } from 'react';
3
+ import { render, unmountComponentAtNode } from 'react-dom';
4
+ import { renderToString } from 'react-dom/server';
5
+ import { Provider } from 'react-redux';
6
+ import { Router, match, RouterContext, createMemoryHistory, browserHistory } from 'react-router';
7
+ import { syncHistoryWithStore } from 'react-router-redux';
8
+
9
+ import ReduxStore from './redux-store';
10
+
11
+ class ReduxRouter {
12
+ constructor() {
13
+ this.routes = {};
14
+
15
+ this.registerRoutes = this.registerRoutes.bind(this);
16
+ }
17
+
18
+ registerRoutes(name, routes) {
19
+ this.routes[name] = routes;
20
+ }
21
+
22
+ getRoutes(name) {
23
+ return this.routes[name];
24
+ }
25
+
26
+ createRootRouter(name, storeName) {
27
+ const routes = this.getRoutes(name);
28
+ const store = ReduxStore.getStore(storeName);
29
+ const history = syncHistoryWithStore(browserHistory, store);
30
+
31
+ return createElement(Provider, { store },
32
+ createElement(Router, { history }, routes)
33
+ );
34
+ }
35
+
36
+ unmountRouter(node) {
37
+ unmountComponentAtNode(node);
38
+ }
39
+
40
+ renderRouter(node, name, storeName) {
41
+ const rootRouter = this.createRootRouter(name, storeName);
42
+ render(rootRouter, node);
43
+ }
44
+
45
+ renderRouterToString(name, storeName, path) {
46
+ const routes = this.getRoutes(name);
47
+ const memoryHistory = createMemoryHistory(path);
48
+ const store = ReduxStore.getStore(storeName);
49
+ const history = syncHistoryWithStore(memoryHistory, store);
50
+
51
+ const result = {
52
+ body: '',
53
+ code: 0,
54
+ };
55
+
56
+ match({ history, routes, location: path }, (error, redirectLocation, renderProps) => {
57
+ if (error) {
58
+ throw error;
59
+ } else if (redirectLocation) {
60
+ result.code = 302;
61
+ result.redirectUri = `${redirectLocation.pathname}${redirectLocation.search}`;
62
+ } else if (renderProps) {
63
+ result.body = renderToString(
64
+ <Provider store={store}>
65
+ <RouterContext {...renderProps}/>
66
+ </Provider>
67
+ );
68
+ result.code = 200;
69
+ } else {
70
+ result.code = 404;
71
+ }
72
+ });
73
+
74
+ return JSON.stringify(result);
75
+ }
76
+
77
+ get integrationWrapper() {
78
+ return {
79
+ mount: function _mount(node, payload) {
80
+ const { name, storeName } = payload;
81
+ this.renderRouter(node, name, storeName);
82
+ }.bind(this),
83
+
84
+ unmount: function _unmount(node) {
85
+ this.unmountRouter(node);
86
+ }.bind(this),
87
+
88
+ nodeRun: function _nodeRun(payload) {
89
+ const { name, storeName, path } = payload;
90
+ return this.renderRouterToString(name, storeName, path);
91
+ }.bind(this),
92
+ };
93
+ }
94
+ }
95
+
96
+ export default new ReduxRouter;
@@ -0,0 +1,50 @@
1
+ import { isFunction, isReduxStore } from '../utils/validators';
2
+
3
+ class ReduxStore {
4
+ constructor() {
5
+ this.registeredStores = {};
6
+ this.mountedStores = {};
7
+ this.defaultStore = null;
8
+
9
+ this.registerStore = this.registerStore.bind(this);
10
+ this.getStore = this.getStore.bind(this);
11
+ this.mountStore = this.mountStore.bind(this);
12
+ }
13
+
14
+ registerStore(name, store) {
15
+ isFunction(store, `Error when registering '${name}' store: must be a function.`);
16
+ this.registeredStores[name] = store;
17
+ }
18
+
19
+ mountStore(name, props) {
20
+ const store = this.registeredStores[name];
21
+ isFunction(store, `Error when mounting '${name}' store: must be a function.`);
22
+
23
+ const storeObject = store(props);
24
+ isReduxStore(storeObject, `Error when mounting '${name}' store: must be a valid Redux store.`);
25
+ this.mountedStores[name] = storeObject;
26
+ this.defaultStore = storeObject;
27
+ }
28
+
29
+ getStore(name) {
30
+ if (name) {
31
+ return this.mountedStores[name];
32
+ }
33
+
34
+ return this.defaultStore;
35
+ }
36
+
37
+ get integrationWrapper() {
38
+ return {
39
+ mount: function _mount(_, payload) {
40
+ this.mountStore(payload.name, payload.props);
41
+ }.bind(this),
42
+
43
+ nodeRun: function _mount(payload) {
44
+ this.mountStore(payload.name, payload.props);
45
+ }.bind(this),
46
+ };
47
+ }
48
+ }
49
+
50
+ export default new ReduxStore;
@@ -1 +1 @@
1
- export default '0.1.0-alpha1';
1
+ export default '0.2.0';
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { createStore, combineReducers } from 'redux';
3
+ import { Route, Redirect } from 'react-router';
4
+ import { routerReducer } from 'react-router-redux';
5
+
6
+ import ReduxStore from '../../src/integrations/redux-store';
7
+
8
+ class AppComponent extends React.Component {
9
+ render() {
10
+ return <div>App Component</div>;
11
+ }
12
+ }
13
+
14
+ const routes = (
15
+ <Route path="/" component={AppComponent}>
16
+ <Redirect from="home" to="/" />
17
+ </Route>
18
+ );
19
+
20
+ const fakeReducer = combineReducers({ routing: routerReducer });
21
+ const store = function (initialState) {
22
+ return createStore(fakeReducer, initialState);
23
+ };
24
+
25
+ const registerRoutesAndMountStore = (subject) => {
26
+ subject.registerRoutes('RoutesName', routes);
27
+ ReduxStore.registerStore('StoreName', store);
28
+ ReduxStore.mountStore('StoreName', {});
29
+ };
30
+
31
+ export {
32
+ routes,
33
+ registerRoutesAndMountStore,
34
+ };
@@ -0,0 +1,19 @@
1
+ import expect from 'expect';
2
+ import subject from '../src/index';
3
+
4
+ describe('RWRRedux', function () {
5
+ it('has correct properites', function () {
6
+ expect(subject.version).toBeA('string');
7
+
8
+ expect(subject.registerStore).toBeA('function');
9
+ expect(subject.mountStore).toBeA('function');
10
+ expect(subject.getStore).toBeA('function');
11
+ expect(subject.storeIntegrationWrapper).toBeA('object');
12
+
13
+ expect(subject.registerContainer).toBeA('function');
14
+ expect(subject.containerIntegrationWrapper).toBeA('object');
15
+
16
+ expect(subject.registerRoutes).toBeA('function');
17
+ expect(subject.routerIntegrationWrapper).toBeA('object');
18
+ });
19
+ });
@@ -0,0 +1,163 @@
1
+ import expect, { spyOn } from 'expect';
2
+ import { createStore } from 'redux';
3
+ import React from 'react';
4
+ import ReactDOM from 'react-dom';
5
+ import ReactDOMServer from 'react-dom/server';
6
+
7
+ import subject from '../../src/integrations/redux-container';
8
+ import ReduxStore from '../../src/integrations/redux-store';
9
+
10
+ class AppContainer extends React.Component {
11
+ render() {
12
+ return <div>AppContainer</div>;
13
+ }
14
+ }
15
+
16
+ const fakeReducer = function () {};
17
+ const store = function (initialState) {
18
+ return createStore(fakeReducer, initialState);
19
+ };
20
+
21
+ function resetConstructor() {
22
+ subject.containers = {};
23
+ expect.restoreSpies();
24
+ }
25
+
26
+ describe('ReduxContainer', function () {
27
+ before(function () {
28
+ resetConstructor();
29
+ });
30
+
31
+ afterEach(function () {
32
+ resetConstructor();
33
+ });
34
+
35
+ describe('.constructor', function () {
36
+ it('initialize empty containers object', function () {
37
+ expect(subject.containers).toEqual({});
38
+ });
39
+ });
40
+
41
+ describe('#registerContainer', function () {
42
+ it('adds container to the storage', function () {
43
+ subject.registerContainer('AppContainer', AppContainer);
44
+
45
+ expect(subject.containers.AppContainer).toEqual(AppContainer);
46
+ });
47
+ });
48
+
49
+ describe('#getContainer', function () {
50
+ it('returns container by name', function () {
51
+ subject.registerContainer('AppContainer', AppContainer);
52
+
53
+ expect(subject.getContainer('AppContainer')).toEqual(AppContainer);
54
+ });
55
+ });
56
+
57
+ describe('#createContainer', function () {
58
+ it('creates redux container', function () {
59
+ subject.registerContainer('AppContainer', AppContainer);
60
+ const container = subject.createContainer('AppContainer');
61
+
62
+ expect(React.isValidElement(container)).toBe(true);
63
+ expect(container.type).toBe(AppContainer);
64
+ });
65
+ });
66
+
67
+ describe('#createRootComponent', function () {
68
+ it('creates redux root component', function () {
69
+ const initialState = { fake: 'state' };
70
+ ReduxStore.registerStore('StoreName', store);
71
+ ReduxStore.mountStore('StoreName', initialState);
72
+
73
+ subject.registerContainer('AppContainer', AppContainer);
74
+ const rootComponent = subject.createRootComponent('AppContainer', 'ValidStore');
75
+
76
+ expect(React.isValidElement(rootComponent)).toBe(true);
77
+ });
78
+ });
79
+
80
+ describe('#renderContainer', function () {
81
+ it('calls #createRootComponent and ReactDOM.render functions', function () {
82
+ const subjectSpy = spyOn(subject, 'createRootComponent');
83
+ const reactSpy = spyOn(ReactDOM, 'render');
84
+
85
+ subject.renderContainer('ContainerName', 'node', 'StoreName');
86
+
87
+ expect(subjectSpy.calls.length).toEqual(1);
88
+ expect(subjectSpy).toHaveBeenCalledWith('ContainerName', 'StoreName');
89
+ expect(reactSpy.calls.length).toEqual(1);
90
+ });
91
+ });
92
+
93
+ describe('#unmountContainer', function () {
94
+ it('calls #unmountComponentAtNode', function () {
95
+ const node = { nodeType: 1, nodeName: 'DIV' };
96
+ const unmountSpy = spyOn(ReactDOM, 'unmountComponentAtNode');
97
+
98
+ subject.unmountContainer(node);
99
+
100
+ expect(unmountSpy.calls.length).toEqual(1);
101
+ expect(unmountSpy).toHaveBeenCalledWith(node);
102
+ });
103
+ });
104
+
105
+ describe('#renderContainerToString', function () {
106
+ it('calls #createRootComponent and ReactDOM.renderToString', function () {
107
+ const subjectSpy = spyOn(subject, 'createRootComponent');
108
+ const reactSpy = spyOn(ReactDOMServer, 'renderToString');
109
+
110
+ subject.renderContainerToString('ContainerName', 'StoreName');
111
+
112
+ expect(subjectSpy.calls.length).toEqual(1);
113
+ expect(subjectSpy).toHaveBeenCalledWith('ContainerName', 'StoreName');
114
+ expect(reactSpy.calls.length).toEqual(1);
115
+ });
116
+
117
+ it('returns JSON.stringify result', function () {
118
+ const initialState = { fake: 'state' };
119
+ ReduxStore.registerStore('StoreName', store);
120
+ ReduxStore.mountStore('StoreName', initialState);
121
+ subject.registerContainer('AppContainer', AppContainer);
122
+
123
+ const result = subject.renderContainerToString('AppContainer', 'StoreName');
124
+
125
+ expect(JSON.parse(result).body).toNotEqual(null);
126
+ });
127
+ });
128
+
129
+ describe('#integrationWrapper', function () {
130
+ const node = { nodeType: 1, nodeName: 'DIV' };
131
+ const payload = { name: 'ContainerName', storeName: 'StoreName' };
132
+
133
+ describe('mount', function () {
134
+ it('calls #renderContainer', function () {
135
+ const mountSpy = spyOn(subject, 'renderContainer');
136
+ subject.integrationWrapper.mount(node, payload);
137
+
138
+ expect(mountSpy.calls.length).toEqual(1);
139
+ expect(mountSpy).toHaveBeenCalledWith(payload.name, node, payload.storeName);
140
+ });
141
+ });
142
+
143
+ describe('unmount', function () {
144
+ it('calls #unmountContainer', function () {
145
+ const unmountSpy = spyOn(subject, 'unmountContainer');
146
+ subject.integrationWrapper.unmount(node);
147
+
148
+ expect(unmountSpy.calls.length).toEqual(1);
149
+ expect(unmountSpy).toHaveBeenCalledWith(node);
150
+ });
151
+ });
152
+
153
+ describe('nodeRun', function () {
154
+ it('calls #renderContainerToString', function () {
155
+ const nodeRunSpy = spyOn(subject, 'renderContainerToString');
156
+ subject.integrationWrapper.nodeRun(payload);
157
+
158
+ expect(nodeRunSpy.calls.length).toEqual(1);
159
+ expect(nodeRunSpy).toHaveBeenCalledWith(payload.name, payload.storeName);
160
+ });
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,147 @@
1
+ import expect, { spyOn } from 'expect';
2
+ import ReactDOM from 'react-dom';
3
+
4
+ import subject from '../../src/integrations/redux-router';
5
+ import { routes, registerRoutesAndMountStore } from '../helpers/redux-router';
6
+
7
+ function resetConstructor() {
8
+ subject.routes = {};
9
+ expect.restoreSpies();
10
+ }
11
+
12
+ describe('ReduxRouter', function () {
13
+ before(function () {
14
+ resetConstructor();
15
+ });
16
+
17
+ afterEach(function () {
18
+ resetConstructor();
19
+ });
20
+
21
+ describe('.constructor', function () {
22
+ it('initialize empty routes object', function () {
23
+ expect(subject.routes).toEqual({});
24
+ });
25
+ });
26
+
27
+ describe('#registerRoutes', function () {
28
+ it('adds routes to the storage', function () {
29
+ subject.registerRoutes('RoutesName', routes);
30
+
31
+ expect(subject.routes.RoutesName).toEqual(routes);
32
+ });
33
+ });
34
+
35
+ describe('#getRoutes', function () {
36
+ it('returns routes by name', function () {
37
+ subject.registerRoutes('RoutesName', routes);
38
+
39
+ expect(subject.getRoutes('RoutesName')).toEqual(routes);
40
+ });
41
+ });
42
+
43
+ describe('#unmountRouter', function () {
44
+ it('calls #unmountComponentAtNode', function () {
45
+ const node = { nodeType: 1, nodeName: 'DIV' };
46
+ const unmountSpy = spyOn(ReactDOM, 'unmountComponentAtNode');
47
+
48
+ subject.unmountRouter(node);
49
+
50
+ expect(unmountSpy.calls.length).toEqual(1);
51
+ expect(unmountSpy).toHaveBeenCalledWith(node);
52
+ });
53
+ });
54
+
55
+ describe('#renderRouter', function () {
56
+ it('calls createRootRouter and render functions', function () {
57
+ const subjectSpy = spyOn(subject, 'createRootRouter').andReturn('router');
58
+ const reactSpy = spyOn(ReactDOM, 'render');
59
+
60
+ subject.renderRouter('node', 'RouterName', 'StoreName');
61
+
62
+ expect(subjectSpy.calls.length).toEqual(1);
63
+ expect(subjectSpy).toHaveBeenCalledWith('RouterName', 'StoreName');
64
+ expect(reactSpy.calls.length).toEqual(1);
65
+ expect(reactSpy).toHaveBeenCalledWith('router', 'node');
66
+ });
67
+ });
68
+
69
+ describe('#renderRouterToString', function () {
70
+ const renderRouterToString = (path) => {
71
+ const result = subject.renderRouterToString('RoutesName', 'StoreName', path);
72
+ return JSON.parse(result);
73
+ };
74
+
75
+ beforeEach(function () {
76
+ registerRoutesAndMountStore(subject);
77
+ });
78
+
79
+ context('when router can match location', function () {
80
+ it('returns string component and 200 status code', function () {
81
+ const validPath = '/';
82
+ const result = renderRouterToString(validPath);
83
+
84
+ expect(result.code).toEqual(200);
85
+ expect(result.body).toInclude('App Component');
86
+ });
87
+ });
88
+
89
+ context('when router returns redirectLocation', function () {
90
+ it('returns 302 status code and redirectUri', function () {
91
+ const redirectPath = '/home';
92
+ const result = renderRouterToString(redirectPath);
93
+
94
+ expect(result.code).toEqual(302);
95
+ expect(result.body).toEqual('');
96
+ expect(result.redirectUri).toEqual('/');
97
+ });
98
+ });
99
+
100
+ context('when router can not match location', function () {
101
+ it('returns 404 status code', function () {
102
+ const invalidPath = '/path';
103
+ const result = renderRouterToString(invalidPath);
104
+
105
+ expect(result.code).toEqual(404);
106
+ expect(result.body).toEqual('');
107
+ });
108
+ });
109
+ });
110
+
111
+ describe('#integrationWrapper', function () {
112
+ const node = { nodeType: 1, nodeName: 'DIV' };
113
+ const payload = { name: 'RouterName', storeName: 'StoreName', path: '/path' };
114
+
115
+ describe('#mount', function () {
116
+ it('calls #renderRouter', function () {
117
+ const { name, storeName } = payload;
118
+ const mountSpy = spyOn(subject, 'renderRouter');
119
+ subject.integrationWrapper.mount(node, payload);
120
+
121
+ expect(mountSpy.calls.length).toEqual(1);
122
+ expect(mountSpy).toHaveBeenCalledWith(node, name, storeName);
123
+ });
124
+ });
125
+
126
+ describe('#unmount', function () {
127
+ it('calls #unmountRouter', function () {
128
+ const unmountSpy = spyOn(subject, 'unmountRouter');
129
+ subject.integrationWrapper.unmount(node);
130
+
131
+ expect(unmountSpy.calls.length).toEqual(1);
132
+ expect(unmountSpy).toHaveBeenCalledWith(node);
133
+ });
134
+ });
135
+
136
+ describe('#nodeRun', function () {
137
+ it('calls #renderRouterToString', function () {
138
+ const { name, storeName, path } = payload;
139
+ const nodeRunSpy = spyOn(subject, 'renderRouterToString');
140
+ subject.integrationWrapper.nodeRun(payload);
141
+
142
+ expect(nodeRunSpy.calls.length).toEqual(1);
143
+ expect(nodeRunSpy).toHaveBeenCalledWith(name, storeName, path);
144
+ });
145
+ });
146
+ });
147
+ });