Skip to content

Commit d467c8f

Browse files
committed
feat: 🎸 added TreeStateModifiers
added TreeStateModifiers, a set of functions that help modify state
1 parent d578c06 commit d467c8f

File tree

6 files changed

+544
-0
lines changed

6 files changed

+544
-0
lines changed

index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,8 @@ export interface TreeState {
147147
getNumberOfVisibleDescendants: (state: State, index: number) => number;
148148
}
149149

150+
export interface TreeStateModifiers {
151+
editNodeAt: (state: State, index: number, setNode: (oldNode: Node) => Node) => State;
152+
}
153+
150154
export const selectors: Selectors;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"babel-preset-es2015": "^6.24.1",
4343
"babel-preset-react": "^6.24.1",
4444
"babel-preset-react-app": "^3.1.0",
45+
"deep-diff": "^1.0.2",
4546
"deep-freeze": "^0.0.1",
4647
"enzyme": "^3.3.0",
4748
"enzyme-adapter-react-16": "^1.1.1",

src/state/TreeStateModifiers.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {getFlattenedTreePaths, doesChangeAffectFlattenedTree, isNodeExpanded} from '../selectors/getFlattenedTree';
2+
import TreeState, {validateState, State} from './TreeState';
3+
import {replaceNodeFromTree} from '../selectors/nodes';
4+
5+
/**
6+
* @callback setNode
7+
* @param {Node} node - current node value
8+
* @return {Node} The updated node
9+
*/
10+
11+
/**
12+
* Set of Tree State Modifiers
13+
*/
14+
export default class TreeStateModifiers {
15+
/**
16+
* Given a state, finds a node at a certain row index.
17+
* @param {State} state - The current state
18+
* @param {number} index - The visible row index
19+
* @param {setNode} setNode - A function to update the node
20+
* @return {State} An internal state representation
21+
*/
22+
static editNodeAt = (state, index, setNode) => {
23+
validateState(state);
24+
25+
const node = TreeState.getNodeAt(state, index);
26+
const updatedNode = setNode(node);
27+
const flattenedTree = [...state.flattenedTree];
28+
const flattenedNodeMap = flattenedTree[index];
29+
const parents = flattenedNodeMap.slice(0, flattenedNodeMap.length - 1);
30+
31+
if (doesChangeAffectFlattenedTree(node, updatedNode)) {
32+
const numberOfVisibleDescendants = TreeState.getNumberOfVisibleDescendants(state, index);
33+
34+
if (isNodeExpanded(updatedNode)) {
35+
const updatedNodeSubTree = getFlattenedTreePaths([updatedNode], parents);
36+
37+
flattenedTree.splice(index + 1, 0, ...updatedNodeSubTree.slice(1));
38+
} else {
39+
flattenedTree.splice(index + 1, numberOfVisibleDescendants);
40+
}
41+
}
42+
43+
const tree = replaceNodeFromTree(state.tree, {...updatedNode, parents});
44+
45+
return new State(tree, flattenedTree);
46+
};
47+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import deepFreeze from 'deep-freeze';
2+
import {diff} from 'deep-diff';
3+
4+
import TreeStateModifiers from '../TreeStateModifiers';
5+
import {Nodes} from '../../../testData/sampleTree';
6+
import TreeState from '../TreeState';
7+
8+
describe('TreeStateModifiers', () => {
9+
const noop = () => {};
10+
11+
describe('editNodeAt', () => {
12+
test('should fail when invalid state is supplied', () => {
13+
expect(() => TreeStateModifiers.editNodeAt('state', 0, noop)).toThrowError(
14+
'Expected a State instance but got string',
15+
);
16+
expect(() => TreeStateModifiers.editNodeAt(1225, 0, noop)).toThrowError(
17+
'Expected a State instance but got number',
18+
);
19+
expect(() => TreeStateModifiers.editNodeAt([], 0, noop)).toThrowError('Expected a State instance but got object');
20+
expect(() => TreeStateModifiers.editNodeAt({}, 0, noop)).toThrowError('Expected a State instance but got object');
21+
expect(() => TreeStateModifiers.editNodeAt(true, 0, noop)).toThrowError(
22+
'Expected a State instance but got boolean',
23+
);
24+
expect(() => TreeStateModifiers.editNodeAt(() => {}, 0, noop)).toThrowError(
25+
'Expected a State instance but got function',
26+
);
27+
});
28+
29+
test('should fail with descriptive error when node at index does not exist', () => {
30+
expect(() =>
31+
TreeStateModifiers.editNodeAt(TreeState.createFromTree(Nodes), 20, noop),
32+
).toThrowErrorMatchingSnapshot();
33+
});
34+
35+
describe('flattened tree', () => {
36+
test('should collapse a node in a root node', () => {
37+
const state = TreeState.createFromTree(Nodes);
38+
39+
deepFreeze(state);
40+
41+
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 0, n => ({
42+
...n,
43+
state: {...n.state, expanded: false},
44+
}));
45+
46+
expect(flattenedTree).toMatchSnapshot();
47+
});
48+
49+
test('should collapse a node in a children node', () => {
50+
const state = TreeState.createFromTree(Nodes);
51+
52+
deepFreeze(state);
53+
54+
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 1, n => ({
55+
...n,
56+
state: {...n.state, expanded: false},
57+
}));
58+
59+
expect(flattenedTree).toMatchSnapshot();
60+
});
61+
62+
test('should expand a node in a root node', () => {
63+
const state = TreeState.createFromTree(Nodes);
64+
65+
deepFreeze(state);
66+
67+
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 5, n => ({
68+
...n,
69+
state: {...n.state, expanded: true},
70+
}));
71+
72+
expect(flattenedTree).toMatchSnapshot();
73+
});
74+
75+
test('should expand a node in a children node', () => {
76+
const state = TreeState.createFromTree(Nodes);
77+
78+
deepFreeze(state);
79+
80+
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
81+
...n,
82+
state: {...n.state, expanded: true},
83+
}));
84+
85+
expect(flattenedTree).toMatchSnapshot();
86+
});
87+
88+
test('should not change for updates that do not change state', () => {
89+
const state = TreeState.createFromTree(Nodes);
90+
91+
deepFreeze(state);
92+
93+
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
94+
...n,
95+
name: 'node',
96+
}));
97+
98+
expect(flattenedTree).toEqual(state.flattenedTree);
99+
});
100+
101+
test('should not change for updates that change state but not expansion', () => {
102+
const state = TreeState.createFromTree(Nodes);
103+
104+
deepFreeze(state);
105+
106+
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
107+
...n,
108+
state: {...n.state, favorite: true},
109+
}));
110+
111+
expect(flattenedTree).toEqual(state.flattenedTree);
112+
113+
const {flattenedTree: flattenedTree2} = TreeStateModifiers.editNodeAt(state, 0, n => ({
114+
...n,
115+
state: {...n.state, deletable: true},
116+
}));
117+
118+
expect(flattenedTree2).toEqual(state.flattenedTree);
119+
120+
const {flattenedTree: flattenedTree3} = TreeStateModifiers.editNodeAt(state, 0, n => ({
121+
...n,
122+
state: {...n.state, randomKey: true},
123+
}));
124+
125+
expect(flattenedTree3).toEqual(state.flattenedTree);
126+
});
127+
});
128+
129+
describe('tree', () => {
130+
test('should update a node in the root and keep the rest intact', () => {
131+
const state = TreeState.createFromTree(Nodes);
132+
133+
deepFreeze(state);
134+
135+
const updatedName = 'Edit node 1';
136+
137+
// Change 'Leaf 1'
138+
const {tree} = TreeStateModifiers.editNodeAt(state, 0, n => ({
139+
...n,
140+
name: updatedName,
141+
}));
142+
143+
const changes = diff(state.tree, tree);
144+
145+
expect(changes.length).toBe(1);
146+
expect(changes[0]).toMatchSnapshot();
147+
});
148+
149+
test('should update a child node and keep the rest intact', () => {
150+
const state = TreeState.createFromTree(Nodes);
151+
152+
deepFreeze(state);
153+
154+
const updatedName = 'Edited node';
155+
156+
// Change 'Leaf 3'
157+
const {tree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
158+
...n,
159+
name: updatedName,
160+
}));
161+
162+
const changes = diff(state.tree, tree);
163+
164+
expect(changes.length).toBe(1);
165+
expect(changes[0]).toMatchSnapshot();
166+
});
167+
168+
test('should update a node state in the root and keep the rest intact', () => {
169+
const state = TreeState.createFromTree(Nodes);
170+
171+
deepFreeze(state);
172+
173+
// Expand 'Leaf 6'
174+
const {tree} = TreeStateModifiers.editNodeAt(state, 5, n => ({
175+
...n,
176+
state: {expanded: true},
177+
}));
178+
179+
const changes = diff(state.tree, tree);
180+
181+
expect(changes).toMatchSnapshot();
182+
});
183+
184+
test('should update a child node state and keep the rest intact', () => {
185+
const state = TreeState.createFromTree(Nodes);
186+
187+
deepFreeze(state);
188+
189+
// Collapse 'Leaf 2'
190+
const {tree} = TreeStateModifiers.editNodeAt(state, 1, n => ({
191+
...n,
192+
state: {...n.state, expanded: false},
193+
}));
194+
195+
const changes = diff(state.tree, tree);
196+
197+
expect(changes.length).toBe(1);
198+
expect(changes[0]).toMatchSnapshot();
199+
});
200+
201+
test('should create state for a child node and keep the rest intact', () => {
202+
const state = TreeState.createFromTree(Nodes);
203+
204+
deepFreeze(state);
205+
206+
// Favorite 'Leaf 5'
207+
const {tree} = TreeStateModifiers.editNodeAt(state, 4, n => ({
208+
...n,
209+
state: {...n.state, expanded: true},
210+
}));
211+
212+
const changes = diff(state.tree, tree);
213+
214+
expect(changes.length).toBe(1);
215+
expect(changes[0]).toMatchSnapshot();
216+
});
217+
218+
test('should delete state for a root node and keep the rest intact', () => {
219+
const state = TreeState.createFromTree(Nodes);
220+
221+
deepFreeze(state);
222+
223+
// Clear state for 'Leaf 6'
224+
const {tree} = TreeStateModifiers.editNodeAt(state, 5, n => {
225+
return Object.keys(n)
226+
.filter(k => k !== 'state')
227+
.reduce((node, k) => ({...node, [k]: n[k]}), {});
228+
});
229+
230+
const changes = diff(state.tree, tree);
231+
232+
expect(changes.length).toBe(1);
233+
expect(changes[0]).toMatchSnapshot();
234+
});
235+
236+
test('should delete state for a child node and keep the rest intact', () => {
237+
const state = TreeState.createFromTree(Nodes);
238+
239+
deepFreeze(state);
240+
241+
// Clear state for 'Leaf 3'
242+
const {tree} = TreeStateModifiers.editNodeAt(state, 2, n => {
243+
return Object.keys(n)
244+
.filter(k => k !== 'state')
245+
.reduce((node, k) => ({...node, [k]: n[k]}), {});
246+
});
247+
248+
const changes = diff(state.tree, tree);
249+
250+
expect(changes.length).toBe(1);
251+
expect(changes[0]).toMatchSnapshot();
252+
});
253+
});
254+
});
255+
});

0 commit comments

Comments
 (0)