Skip to content

Commit 478d398

Browse files
committed
Merge branch 'bugfix/toggle-active-trail'
2 parents d5ca8bc + 94f30d0 commit 478d398

File tree

5 files changed

+38
-21
lines changed

5 files changed

+38
-21
lines changed

README.md

+8-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ With `useTree` you can focus on how to render your tree structure and forget abo
1616
- [useTreeContent()](#usetreecontent)
1717
- [useTreeLoader()](#usetreeloader)
1818
- [Rendering a tree](#rendering-a-tree)
19+
- [Type RootTree<T>](#type-roottreet)
1920
- [Type Tree<T>](#type-treet)
2021
- [Type TreeNode<T>](#type-treenodet)
2122
- [Typescript](#typescript)
@@ -89,7 +90,7 @@ This interface describes the current display state of a tree. It contains two pr
8990
### useTreeController()
9091
Get an object that lets you control the current tree (from context). Provides these methods:
9192

92-
* `updateState(updater: (oldState: TreeState) => TreeState): void`: update the tree state with your own updater function.
93+
* `updateState(updater: (oldState: TreeState, tree: RootTree<T>) => TreeState): void`: update the tree state with your own updater function.
9394
* `setExpanded(id: string, expanded?: boolean): void`: expanded or collapse a tree node.
9495
* `toggleExpanded(id: string): void`: toggle the expanded state of a tree node.
9596
* `setActiveId(id: string | null): void`: set which (if any) tree node is active.
@@ -105,13 +106,17 @@ Same as `useTreeController()`, but only for one tree node. You should, for insta
105106
When used inside the context of a tree, returns the current data of the tree. This can be used if you have several components nested inside your `<TreeContainer>` that all want to display the tree (or parts thereof).
106107

107108
### useTreeLoader()
108-
Use this if you want full control and don't want to use `TreeContainer`. This hook takes a `TreeSource` and a `TreeState` and returns the most up-to-date tree data structure. It will load data from the source if necessary and re-render as that data comes in.
109+
Use this if you want full control and don't want to use `TreeContainer`. This hook takes a `TreeSource` and a `TreeState` and returns the most up-to-date tree data structure with type `RootTree<T>`. It will load data from the source if necessary and re-render as that data comes in.
109110

110111
## Rendering a tree
111112
When rendering a tree through the `rootElement` of `TreeContainer` or by passing the result of `useTreeLoader()` directly to your component, your component should accept these data types. We will assume that your `TreeSource` is of type `TreeSource<T>` where `T` is your own type that contains your own properties for tree nodes.
112113

113114
Root element properties:
114-
* `tree: Tree<T>`
115+
* `tree: RootTree<T>`
116+
117+
### Type RootTree<T>
118+
* All properties from `Tree<T>`
119+
* `allNodes: {[k: string]: TreeNode<T>}`: all currently loaded tree nodes, indexed by ID.
115120

116121
### Type Tree<T>
117122
* `isLoading: boolean`: whether the items are still being loaded.

src/TreeContainer.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ interface TreeContainerProps<T> {
1515

1616
export function TreeContainer<T>(props: PropsWithChildren<TreeContainerProps<T>>, context?: any): ReactElement | null {
1717
const { source, defaultState, state, onStateChange, rootElement, children } = props;
18-
const controller = useRef<TreeController>(treeControllerFromUpdateState(noopUpdateState));
18+
const controller = useRef<TreeController<unknown>>(treeControllerFromUpdateState(noopUpdateState));
1919
const [innerState, setInnerState] = useBinding(defaultState, state, onStateChange, {});
2020

2121
const tree = useTreeLoader(source, innerState);
2222

2323
controller.current.updateState = useCallback((updater) => {
24-
setInnerState(updater(innerState));
25-
}, [innerState, setInnerState]);
24+
setInnerState(updater(innerState, tree));
25+
}, [innerState, setInnerState, tree]);
2626

2727
return (
2828
<TreeContentContext.Provider value={tree}>

src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ export type TreeNode<T> = TreeSourceNode<T> & {
2626
};
2727

2828
export type Tree<T> = LoadableArray<TreeNode<T>>;
29+
30+
export type RootTree<T> = Tree<T> & {
31+
allNodes: {[k: string]: TreeNode<T>};
32+
};

src/use-tree-controller.ts

+20-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { createContext, useContext, useMemo } from 'react';
2-
import { TreeSourceNode, TreeState } from './types';
2+
import { RootTree, TreeSourceNode, TreeState } from './types';
33

4-
export interface TreeController {
5-
updateState(f: (st: TreeState) => TreeState): void;
4+
type TreeStateUpdater<T> = (state: TreeState, tree: RootTree<T>) => TreeState;
5+
6+
export interface TreeController<T> {
7+
updateState(f: TreeStateUpdater<T>): void;
68
toggleExpanded(id: string): void;
79
setExpanded(id: string, expanded?: boolean): void;
810
setActiveId(id: string | null): void;
@@ -14,8 +16,8 @@ export interface TreeNodeController {
1416
setActive(active?: boolean): void;
1517
}
1618

17-
export function treeControllerFromUpdateState(updateState: (f: (st: TreeState) => TreeState) => void): TreeController {
18-
const obj: Partial<TreeController> = {
19+
export function treeControllerFromUpdateState<T>(updateState: (f: TreeStateUpdater<T>) => void): TreeController<T> {
20+
const obj: Partial<TreeController<T>> = {
1921
updateState,
2022
};
2123
obj.setExpanded = (id: string, expanded?: boolean) => {
@@ -25,24 +27,29 @@ export function treeControllerFromUpdateState(updateState: (f: (st: TreeState) =
2527
}));
2628
};
2729
obj.toggleExpanded = (id: string) => {
28-
obj.updateState!(({ expandedIds, ...rest }) => ({
29-
...rest,
30-
expandedIds: { ...expandedIds, [id]: !expandedIds || !expandedIds[id] },
31-
}));
30+
obj.updateState!(({ expandedIds, ...rest }, { allNodes }) => {
31+
const explicitExpandedState = expandedIds ? expandedIds[id] : undefined;
32+
const isExpanded = (explicitExpandedState === true)
33+
|| (allNodes[id] && allNodes[id].isActiveTrail && explicitExpandedState === undefined);
34+
return {
35+
...rest,
36+
expandedIds: { ...expandedIds, [id]: !isExpanded },
37+
};
38+
});
3239
};
3340
obj.setActiveId = (id: string | null) => {
3441
obj.updateState!((st) => ({ ...st, activeId: id }));
3542
};
36-
return obj as TreeController;
43+
return obj as TreeController<T>;
3744
}
3845

39-
export const noopUpdateState = (f: (st: TreeState) => TreeState) => { /* noop */ };
46+
export const noopUpdateState = <T, >(f: TreeStateUpdater<T>) => { /* noop */ };
4047

4148
export const noopTreeController = treeControllerFromUpdateState(noopUpdateState);
4249

43-
export const TreeControllerContext = createContext<TreeController>(noopTreeController);
50+
export const TreeControllerContext = createContext<TreeController<unknown>>(noopTreeController);
4451

45-
export function useTreeController(): TreeController {
52+
export function useTreeController(): TreeController<unknown> {
4653
return useContext(TreeControllerContext);
4754
}
4855

src/use-tree-loader.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2-
import { LoadableArray, Tree, TreeNode, TreeSource, TreeSourceNode, TreeState } from './types';
2+
import { LoadableArray, RootTree, TreeNode, TreeSource, TreeSourceNode, TreeState } from './types';
33

44
interface StringMap<V> {
55
[k: string]: V;
@@ -29,7 +29,7 @@ function valuesEqual(arr1: unknown[], arr2: unknown[]): boolean {
2929
return true;
3030
}
3131

32-
export function useTreeLoader<T>(source: TreeSource<T>, state: TreeState): Tree<T> {
32+
export function useTreeLoader<T>(source: TreeSource<T>, state: TreeState): RootTree<T> {
3333
const [rootNodes, setRootNodes] = useState<LoadableArray<TreeSourceNode<T>>>({ isLoading: true, items: [] });
3434
const [children, setChildren] = useState<StringMap<LoadableArray<TreeSourceNode<T>>>>({});
3535
const [trails, setTrails] = useState<StringMap<Array<TreeSourceNode<T>>>>({});
@@ -135,6 +135,7 @@ export function useTreeLoader<T>(source: TreeSource<T>, state: TreeState): Tree<
135135
return {
136136
items: rootNodes.items.map(buildOutputNode),
137137
isLoading: rootNodes.isLoading,
138+
allNodes: statefulNodes.current,
138139
};
139140
}, [activeId, expandedIds, rootNodes, children, activeTrailIds, statefulNodes]);
140141
}

0 commit comments

Comments
 (0)