This repository has been archived by the owner on Feb 20, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 58
/
action-reducer.js
202 lines (179 loc) · 5.89 KB
/
action-reducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
'use strict';
import { Map } from 'immutable';
import invariant from 'invariant'
// For updating multiple UI variables at once. Each variable might be part of
// a different context; this means that we need to either call updateUI on each
// key of the object to update or do transformations within one action in the
// reducer. The latter only triggers one store change event and is more
// performant.
export const MASS_UPDATE_UI_STATE = '@@redux-ui/MASS_UPDATE_UI_STATE';
export const UPDATE_UI_STATE = '@@redux-ui/UPDATE_UI_STATE';
export const SET_DEFAULT_UI_STATE = '@@redux-ui/SET_DEFAULT_UI_STATE';
// These are private consts used in actions only given to the UI decorator.
const MOUNT_UI_STATE = '@@redux-ui/MOUNT_UI_STATE';
const UNMOUNT_UI_STATE = '@@redux-ui/UNMOUNT_UI_STATE';
export const defaultState = new Map({
__reducers: new Map({
// This contains a map of component paths (joined by '.') to an object
// containing the fully qualified path and the reducer function:
// 'parent.child': {
// path: ['parent', 'child'],
// func: (state, action) => { ... }
// }
})
});
export default function reducer(state = defaultState, action) {
let key = action.payload && (action.payload.key || []);
if (!Array.isArray(key)) {
key = [key];
}
switch (action.type) {
case UPDATE_UI_STATE:
const { name, value } = action.payload;
if (typeof value === 'function') {
state = state.updateIn(key.concat(name), value);
} else {
state = state.setIn(key.concat(name), value);
}
break;
case MASS_UPDATE_UI_STATE:
const { uiVars, transforms } = action.payload;
state = state.withMutations( s => {
Object.keys(transforms).forEach(k => {
const path = uiVars[k];
invariant(
path,
`Couldn't find variable ${k} within your component's UI state ` +
`context. Define ${k} before using it in the @ui decorator`
);
s.setIn(path.concat(k), transforms[k]);
});
});
break;
case SET_DEFAULT_UI_STATE:
// Replace all UI under a key with the given values
state = state.setIn(key, new Map(action.payload.value));
break;
case MOUNT_UI_STATE:
const { defaults, customReducer } = action.payload;
state = state.withMutations( s => {
// Set the defaults for the component
s.setIn(key, new Map(defaults));
// If this component has a custom reducer add it to the list.
// We store the reducer func and UI path for the current component
// inside the __reducers map.
if (customReducer) {
let path = key.join('.');
s.setIn(['__reducers', path], {
path: key,
func: customReducer
});
}
return s;
});
break;
case UNMOUNT_UI_STATE:
// We have to use deleteIn as react unmounts root components first;
// this means that using setIn in child contexts will fail as the root
// context will be stored as undefined in our state
state= state.withMutations(s => {
s.deleteIn(key);
// Remove any custom reducers
s.deleteIn(['__reducers', key.join('.')]);
});
break;
}
const customReducers = state.get('__reducers');
if (customReducers.size > 0) {
state = state.withMutations(mut => {
customReducers.forEach(r => {
// This calls each custom reducer with the UI state for each custom
// reducer with the component's UI state tree passed into it.
//
// NOTE: Each component's reducer gets its own UI state: not the entire
// UI reducer's state. Whatever is returned from this reducer is set
// within the **components** UI scope.
//
// This is because it's the only way to update UI state for components
// without keys - you need to know the path in advance to update state
// from a reducer. If you have list of components with no UI keys in
// the component heirarchy, any children will not be able to use custom
// reducers as the path is random.
//
// TODO: Potentially add the possibility for a global UI state reducer?
// Though why wouldn't you just add a custom reducer to the
// top-level component?
const { path, func } = r;
const newState = func(mut.getIn(path), action);
if (newState === undefined) {
throw new Error(`Your custom UI reducer at path ${path.join('.')} must return some state`);
}
mut.setIn(path, newState);
});
return mut;
});
}
return state;
}
export const reducerEnhancer = (customReducer) => (state, action) => {
state = reducer(state, action);
if (typeof customReducer === 'function') {
state = customReducer(state, action);
}
return state;
}
export function updateUI(key, name, value) {
return {
type: UPDATE_UI_STATE,
payload: {
key,
name,
value
}
};
};
export function massUpdateUI(uiVars, transforms) {
return {
type: MASS_UPDATE_UI_STATE,
payload: {
uiVars,
transforms
}
};
}
// Exposed to components, allowing them to reset their and all child scopes to
// the default variables set up
export function setDefaultUI(key, value) {
return {
type: SET_DEFAULT_UI_STATE,
payload: {
key,
value
}
};
};
/** Private, decorator only actions **/
// This is not exposed to your components; it's only used in the decorator.
export function unmountUI(key) {
return {
type: UNMOUNT_UI_STATE,
payload: {
key
}
};
};
/**
* Given the key/path, set of defaults and custom reducer for a UI component
* during construction prepare the state of the UI reducer
*
*/
export function mountUI(key, defaults, customReducer) {
return {
type: MOUNT_UI_STATE,
payload: {
key,
defaults,
customReducer
}
}
}