, or explicitly pass the "store" to the Connector.'
76 | );
77 | }
78 |
79 | componentWillMount() {
80 | this.dispatchProps = builder.dispatchMapper ? builder.dispatchMapper(this.store.dispatch) : {};
81 |
82 | if (!builder.moduleNames.length) {
83 | return;
84 | }
85 |
86 | const hdrt = builder.hydrator ? builder.hydrator(this.props) : undefined;
87 |
88 | const state$ = builder.moduleNames.length === 1
89 | ? getSingeState$(this.store, builder, hdrt)
90 | : getCombinedState$(this.store, builder, hdrt);
91 |
92 | this.subscription = state$.subscribe(state => this.setState(state));
93 |
94 | if (hdrt) {
95 | builder.moduleNames.forEach(moduleName => {
96 | if (hdrt[moduleName]) {
97 | this.store.hydrate(moduleName, hdrt[moduleName]);
98 | }
99 | });
100 | }
101 | }
102 |
103 | componentWillUnmount() {
104 | if (!builder.moduleNames.length) {
105 | return;
106 | }
107 |
108 | if (builder.clearOnUnmount) {
109 | const defined = typeof builder.clearOnUnmount === 'object';
110 | builder.moduleNames.forEach(moduleName => {
111 | if (defined && !builder.clearOnUnmount[moduleName]) {
112 | return;
113 | }
114 | this.store.clearState(moduleName);
115 | });
116 | }
117 | this.subscription.unsubscribe();
118 | }
119 |
120 | render() {
121 | const { Component } = builder;
122 | return ;
123 | }
124 | }
125 | Connected.displayName = connectorDisplayName;
126 | Connected.contextTypes = {
127 | store: storeShape
128 | };
129 | return Connected;
130 | };
131 |
--------------------------------------------------------------------------------
/src/Provider.js:
--------------------------------------------------------------------------------
1 | import { Component, PropTypes, Children } from 'react';
2 | import { storeShape } from './storeShape';
3 |
4 | export class Provider extends Component {
5 | getChildContext() {
6 | return { store: this.store };
7 | }
8 |
9 | constructor(props, context) {
10 | super(props, context);
11 | this.store = props.store;
12 | }
13 |
14 | render() {
15 | return Children.only(this.props.children);
16 | }
17 | }
18 |
19 | Provider.propTypes = {
20 | store: storeShape.isRequired,
21 | children: PropTypes.element.isRequired
22 | };
23 |
24 | Provider.childContextTypes = {
25 | store: storeShape.isRequired
26 | };
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { Connector } from './Connector';
2 | export { Provider } from './Provider';
3 |
--------------------------------------------------------------------------------
/src/storeShape.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 |
3 | export const storeShape = PropTypes.shape({
4 | getState$: PropTypes.func.isRequired,
5 | dispatch: PropTypes.func.isRequired,
6 | hydrate: PropTypes.func.isRequired,
7 | clearState: PropTypes.func.isRequired,
8 | });
9 |
--------------------------------------------------------------------------------
/test/Connector-spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe it */
2 | import React from 'react';
3 | import { expect } from 'chai';
4 | import { createStore } from 'udeo';
5 | import { Connector, Provider } from '../';
6 | import { mount } from 'enzyme';
7 |
8 | const FOO = '@test/FOO';
9 | const BAR = '@test/BAR';
10 |
11 | describe('Connector', () => {
12 | describe('with state from a single module', () => {
13 | it('receives state updates', () => {
14 | const initialState = { foos: ['Tommy'] };
15 | const fooModule = {
16 | flow(dispatch$) {
17 | return [
18 | dispatch$.filterAction(FOO)
19 | ];
20 | },
21 | reducer(state = initialState, action) {
22 | switch (action.type) {
23 | case FOO:
24 | return {
25 | ...state,
26 | foos: state.foos.concat(action.payload)
27 | };
28 | default:
29 | return state;
30 | }
31 | }
32 | };
33 |
34 | const store = createStore({ fooModule });
35 |
36 | const Component = ({ foos }) => (
37 |
38 | {foos.map((f, i) =>
-{f}-
)}
39 |
40 | );
41 |
42 | const Connected = new Connector(Component)
43 | .withStateFrom('fooModule')
44 | .build();
45 |
46 | const mounted = mount(
47 |
48 |
49 |
50 | );
51 |
52 | expect(mounted.text()).to.eq('-Tommy-');
53 |
54 | store.dispatch({ type: FOO, payload: 'Shelby' });
55 | expect(mounted.text()).to.eq('-Tommy--Shelby-');
56 |
57 | store.dispatch({ type: BAR, payload: 'Arthur' });
58 | expect(mounted.text()).to.eq('-Tommy--Shelby-');
59 | });
60 |
61 | it('hydrates with computed props', () => {
62 | const initialState = { value: 0 };
63 | const fooModule = {
64 | flow(dispatch$) {
65 | return [
66 | dispatch$.filterAction(FOO)
67 | ];
68 | },
69 | reducer(state = initialState, action) {
70 | return state;
71 | }
72 | };
73 |
74 | const store = createStore({ fooModule });
75 |
76 | const Component = ({ value }) => (
77 |
78 | The answer is {value}
79 |
80 | );
81 |
82 | const Connected = new Connector(Component)
83 | .withStateFrom('fooModule')
84 | .hydrateWith(
85 | props => ({
86 | fooModule: { value: props.initialValue + 10 }
87 | })
88 | )
89 | .build();
90 |
91 | const mounted = mount(
92 |
93 |
94 |
95 | );
96 |
97 | expect(mounted.text()).to.eq('The answer is 42');
98 | });
99 |
100 | it('maps state', () => {
101 | const initialState = { valueA: 21, valueB: 42 };
102 | const fooModule = {
103 | flow(dispatch$) {
104 | return [
105 | dispatch$.filterAction(FOO)
106 | ];
107 | },
108 | reducer(state = initialState, action) {
109 | return state;
110 | }
111 | };
112 |
113 | const store = createStore({ fooModule });
114 |
115 | const Component = ({ valueA }) => (
116 |
117 | Value A is {valueA}
118 |
119 | );
120 |
121 | const Connected = new Connector(Component)
122 | .withStateFrom('fooModule')
123 | .mapStateTo(
124 | fooState => ({
125 | valueA: fooState.valueA,
126 | })
127 | )
128 | .build();
129 |
130 | const mounted = mount(
131 |
132 |
133 |
134 | );
135 |
136 | expect(mounted.find(Component).props()).to.have.all.keys(['valueA']);
137 | expect(mounted.find(Component).text()).to.eq('Value A is 21');
138 | });
139 |
140 | it('maps dispatch', () => {
141 | const initialState = { valueA: 20, valueB: 42 };
142 | const fooModule = {
143 | flow(dispatch$) {
144 | return [
145 | dispatch$.filterAction(FOO)
146 | ];
147 | },
148 | reducer(state = initialState, action) {
149 | switch (action.type) {
150 | case FOO:
151 | return {
152 | ...state,
153 | valueA: state.valueA * 2,
154 | valueB: state.valueB * 2,
155 | };
156 | default:
157 | return state;
158 | }
159 | }
160 | };
161 |
162 | const store = createStore({ fooModule });
163 |
164 | const Component = ({ valueA, valueB, onDubble }) => (
165 |
166 | A: {valueA} B: {valueB}
167 |
168 |
169 | );
170 |
171 | const Connected = new Connector(Component)
172 | .withStateFrom('fooModule')
173 | .mapDispatchTo(
174 | dispatch => ({
175 | onDubble() {
176 | dispatch({ type: FOO });
177 | }
178 | })
179 | )
180 | .build();
181 |
182 | const mounted = mount(
183 |
184 |
185 |
186 | );
187 |
188 | expect(mounted.find(Component).props()).to.have.all.keys(['valueA', 'valueB', 'onDubble']);
189 | expect(mounted.find('.values').text()).to.eq('A: 20 B: 42');
190 |
191 | mounted.find('#dubble').simulate('click');
192 | expect(mounted.find('.values').text()).to.eq('A: 40 B: 84');
193 |
194 | mounted.find('#dubble').simulate('click');
195 | expect(mounted.find('.values').text()).to.eq('A: 80 B: 168');
196 | });
197 |
198 | it('clears state on unmount', () => {
199 | const initialState = { valueA: 20, valueB: 42 };
200 | const fooModule = {
201 | flow(dispatch$) {
202 | return [
203 | dispatch$.filterAction(FOO)
204 | ];
205 | },
206 | reducer(state = initialState, action) {
207 | switch (action.type) {
208 | case FOO:
209 | return {
210 | ...state,
211 | valueA: state.valueA * 2,
212 | valueB: state.valueB * 2,
213 | };
214 | default:
215 | return state;
216 | }
217 | }
218 | };
219 |
220 | const store = createStore({ fooModule });
221 |
222 | const Component = ({ onDubble }) => (
223 |
224 | );
225 |
226 | const Connected = new Connector(Component)
227 | .withStateFrom('fooModule')
228 | .mapDispatchTo(
229 | dispatch => ({
230 | onDubble() {
231 | dispatch({ type: FOO });
232 | }
233 | })
234 | )
235 | .build();
236 |
237 | const mounted = mount(
238 |
239 |
240 |
241 | );
242 |
243 | let fooState;
244 | store.getState$('fooModule').subscribe(state => {
245 | fooState = state;
246 | });
247 |
248 | mounted.find('#dubble').simulate('click');
249 | mounted.find('#dubble').simulate('click');
250 |
251 | expect(fooState).to.deep.eq({ valueA: 80, valueB: 168 });
252 |
253 | mounted.unmount();
254 | expect(fooState).to.deep.eq({ valueA: 20, valueB: 42 });
255 | });
256 | });
257 | });
258 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | /* globals global */
2 | import { jsdom } from 'jsdom';
3 |
4 | global.document = jsdom('');
5 | global.window = document.defaultView;
6 | global.navigator = global.window.navigator;
7 |
--------------------------------------------------------------------------------