Skip to content

Commit

Permalink
feat: 馃幐 added TreeStateModifiers
Browse files Browse the repository at this point in the history
added TreeStateModifiers, a set of functions that help modify state
  • Loading branch information
diogofcunha committed Jan 27, 2019
1 parent d578c06 commit d467c8f
Show file tree
Hide file tree
Showing 6 changed files with 544 additions and 0 deletions.
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,8 @@ export interface TreeState {
getNumberOfVisibleDescendants: (state: State, index: number) => number;
}

export interface TreeStateModifiers {
editNodeAt: (state: State, index: number, setNode: (oldNode: Node) => Node) => State;
}

export const selectors: Selectors;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-react-app": "^3.1.0",
"deep-diff": "^1.0.2",
"deep-freeze": "^0.0.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
Expand Down
47 changes: 47 additions & 0 deletions src/state/TreeStateModifiers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {getFlattenedTreePaths, doesChangeAffectFlattenedTree, isNodeExpanded} from '../selectors/getFlattenedTree';
import TreeState, {validateState, State} from './TreeState';
import {replaceNodeFromTree} from '../selectors/nodes';

/**
* @callback setNode
* @param {Node} node - current node value
* @return {Node} The updated node
*/

/**
* Set of Tree State Modifiers
*/
export default class TreeStateModifiers {
/**
* Given a state, finds a node at a certain row index.
* @param {State} state - The current state
* @param {number} index - The visible row index
* @param {setNode} setNode - A function to update the node
* @return {State} An internal state representation
*/
static editNodeAt = (state, index, setNode) => {
validateState(state);

const node = TreeState.getNodeAt(state, index);
const updatedNode = setNode(node);
const flattenedTree = [...state.flattenedTree];
const flattenedNodeMap = flattenedTree[index];
const parents = flattenedNodeMap.slice(0, flattenedNodeMap.length - 1);

if (doesChangeAffectFlattenedTree(node, updatedNode)) {
const numberOfVisibleDescendants = TreeState.getNumberOfVisibleDescendants(state, index);

if (isNodeExpanded(updatedNode)) {
const updatedNodeSubTree = getFlattenedTreePaths([updatedNode], parents);

flattenedTree.splice(index + 1, 0, ...updatedNodeSubTree.slice(1));
} else {
flattenedTree.splice(index + 1, numberOfVisibleDescendants);
}
}

const tree = replaceNodeFromTree(state.tree, {...updatedNode, parents});

return new State(tree, flattenedTree);
};
}
255 changes: 255 additions & 0 deletions src/state/__tests__/TreeStateModifiers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import deepFreeze from 'deep-freeze';
import {diff} from 'deep-diff';

import TreeStateModifiers from '../TreeStateModifiers';
import {Nodes} from '../../../testData/sampleTree';
import TreeState from '../TreeState';

describe('TreeStateModifiers', () => {
const noop = () => {};

describe('editNodeAt', () => {
test('should fail when invalid state is supplied', () => {
expect(() => TreeStateModifiers.editNodeAt('state', 0, noop)).toThrowError(
'Expected a State instance but got string',
);
expect(() => TreeStateModifiers.editNodeAt(1225, 0, noop)).toThrowError(
'Expected a State instance but got number',
);
expect(() => TreeStateModifiers.editNodeAt([], 0, noop)).toThrowError('Expected a State instance but got object');
expect(() => TreeStateModifiers.editNodeAt({}, 0, noop)).toThrowError('Expected a State instance but got object');
expect(() => TreeStateModifiers.editNodeAt(true, 0, noop)).toThrowError(
'Expected a State instance but got boolean',
);
expect(() => TreeStateModifiers.editNodeAt(() => {}, 0, noop)).toThrowError(
'Expected a State instance but got function',
);
});

test('should fail with descriptive error when node at index does not exist', () => {
expect(() =>
TreeStateModifiers.editNodeAt(TreeState.createFromTree(Nodes), 20, noop),
).toThrowErrorMatchingSnapshot();
});

describe('flattened tree', () => {
test('should collapse a node in a root node', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 0, n => ({
...n,
state: {...n.state, expanded: false},
}));

expect(flattenedTree).toMatchSnapshot();
});

test('should collapse a node in a children node', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 1, n => ({
...n,
state: {...n.state, expanded: false},
}));

expect(flattenedTree).toMatchSnapshot();
});

test('should expand a node in a root node', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 5, n => ({
...n,
state: {...n.state, expanded: true},
}));

expect(flattenedTree).toMatchSnapshot();
});

test('should expand a node in a children node', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
...n,
state: {...n.state, expanded: true},
}));

expect(flattenedTree).toMatchSnapshot();
});

test('should not change for updates that do not change state', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
...n,
name: 'node',
}));

expect(flattenedTree).toEqual(state.flattenedTree);
});

test('should not change for updates that change state but not expansion', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
...n,
state: {...n.state, favorite: true},
}));

expect(flattenedTree).toEqual(state.flattenedTree);

const {flattenedTree: flattenedTree2} = TreeStateModifiers.editNodeAt(state, 0, n => ({
...n,
state: {...n.state, deletable: true},
}));

expect(flattenedTree2).toEqual(state.flattenedTree);

const {flattenedTree: flattenedTree3} = TreeStateModifiers.editNodeAt(state, 0, n => ({
...n,
state: {...n.state, randomKey: true},
}));

expect(flattenedTree3).toEqual(state.flattenedTree);
});
});

describe('tree', () => {
test('should update a node in the root and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const updatedName = 'Edit node 1';

// Change 'Leaf 1'
const {tree} = TreeStateModifiers.editNodeAt(state, 0, n => ({
...n,
name: updatedName,
}));

const changes = diff(state.tree, tree);

expect(changes.length).toBe(1);
expect(changes[0]).toMatchSnapshot();
});

test('should update a child node and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

const updatedName = 'Edited node';

// Change 'Leaf 3'
const {tree} = TreeStateModifiers.editNodeAt(state, 2, n => ({
...n,
name: updatedName,
}));

const changes = diff(state.tree, tree);

expect(changes.length).toBe(1);
expect(changes[0]).toMatchSnapshot();
});

test('should update a node state in the root and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

// Expand 'Leaf 6'
const {tree} = TreeStateModifiers.editNodeAt(state, 5, n => ({
...n,
state: {expanded: true},
}));

const changes = diff(state.tree, tree);

expect(changes).toMatchSnapshot();
});

test('should update a child node state and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

// Collapse 'Leaf 2'
const {tree} = TreeStateModifiers.editNodeAt(state, 1, n => ({
...n,
state: {...n.state, expanded: false},
}));

const changes = diff(state.tree, tree);

expect(changes.length).toBe(1);
expect(changes[0]).toMatchSnapshot();
});

test('should create state for a child node and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

// Favorite 'Leaf 5'
const {tree} = TreeStateModifiers.editNodeAt(state, 4, n => ({
...n,
state: {...n.state, expanded: true},
}));

const changes = diff(state.tree, tree);

expect(changes.length).toBe(1);
expect(changes[0]).toMatchSnapshot();
});

test('should delete state for a root node and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

// Clear state for 'Leaf 6'
const {tree} = TreeStateModifiers.editNodeAt(state, 5, n => {
return Object.keys(n)
.filter(k => k !== 'state')
.reduce((node, k) => ({...node, [k]: n[k]}), {});
});

const changes = diff(state.tree, tree);

expect(changes.length).toBe(1);
expect(changes[0]).toMatchSnapshot();
});

test('should delete state for a child node and keep the rest intact', () => {
const state = TreeState.createFromTree(Nodes);

deepFreeze(state);

// Clear state for 'Leaf 3'
const {tree} = TreeStateModifiers.editNodeAt(state, 2, n => {
return Object.keys(n)
.filter(k => k !== 'state')
.reduce((node, k) => ({...node, [k]: n[k]}), {});
});

const changes = diff(state.tree, tree);

expect(changes.length).toBe(1);
expect(changes[0]).toMatchSnapshot();
});
});
});
});

0 comments on commit d467c8f

Please sign in to comment.