Skip to content

Commit a911646

Browse files
committed
docs: new data handling storybook instances
1 parent b4b7a4a commit a911646

File tree

4 files changed

+440
-173
lines changed

4 files changed

+440
-173
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import type { Meta } from "@storybook/react";
2+
import React, { Fragment } from "react";
3+
import {
4+
ItemInstance,
5+
asyncDataLoaderFeature,
6+
createOnDropHandler,
7+
dragAndDropFeature,
8+
hotkeysCoreFeature,
9+
insertItemsAtTarget,
10+
keyboardDragAndDropFeature,
11+
renamingFeature,
12+
searchFeature,
13+
selectionFeature,
14+
} from "@headless-tree/core";
15+
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
16+
import cn from "classnames";
17+
import { DemoItem, createDemoData } from "../../utils/data";
18+
19+
const meta = {
20+
title: "React/Guides/External Data Management/Async Data",
21+
tags: ["guide", "external data"],
22+
} satisfies Meta;
23+
24+
export default meta;
25+
26+
let newItemId = 0;
27+
28+
const getCssClass = (item: ItemInstance<DemoItem>) =>
29+
cn("treeitem", {
30+
focused: item.isFocused(),
31+
expanded: item.isExpanded(),
32+
selected: item.isSelected(),
33+
folder: item.isFolder(),
34+
drop: item.isDragTarget(),
35+
searchmatch: item.isMatchingSearch(),
36+
});
37+
38+
// story-start
39+
// Data is of type `Record<string, DemoItem>`
40+
const { asyncDataLoader, data } = createDemoData();
41+
42+
export const AsyncData = () => {
43+
const tree = useTree<DemoItem>({
44+
initialState: {
45+
expandedItems: ["fruit"],
46+
selectedItems: ["banana", "orange"],
47+
},
48+
rootItemId: "root",
49+
createLoadingItemData: () => ({ name: "Loading..." }),
50+
getItemName: (item) => item.getItemData().name,
51+
isItemFolder: (item) => !!item.getItemData().children,
52+
canReorder: true,
53+
onDrop: createOnDropHandler((item, newChildren) => {
54+
data[item.getId()].children = newChildren;
55+
}),
56+
onRename: (item, value) => {
57+
data[item.getId()].name = value;
58+
item.updateCachedData({ ...item.getItemData(), name: value }); // TODO mention in rename docs
59+
},
60+
indent: 20,
61+
dataLoader: asyncDataLoader,
62+
features: [
63+
asyncDataLoaderFeature,
64+
selectionFeature,
65+
hotkeysCoreFeature,
66+
dragAndDropFeature,
67+
keyboardDragAndDropFeature,
68+
renamingFeature,
69+
searchFeature,
70+
],
71+
72+
// For dragging foreign items into the tree:
73+
canDropForeignDragObject: (_, target) => target.item.isFolder(),
74+
onDropForeignDragObject: async (dataTransfer, target) => {
75+
const newId = `new-${newItemId++}`;
76+
data[newId] = { name: dataTransfer.getData("text/plain") };
77+
await insertItemsAtTarget([newId], target, (item, newChildrenIds) => {
78+
data[item.getId()].children = newChildrenIds;
79+
});
80+
},
81+
});
82+
83+
return (
84+
<>
85+
{tree.isSearchOpen() && (
86+
<div className="searchbox">
87+
<input {...tree.getSearchInputElementProps()} />
88+
<span>({tree.getSearchMatchingItems().length} matches)</span>
89+
</div>
90+
)}
91+
<div {...tree.getContainerProps()} className="tree">
92+
<AssistiveTreeDescription tree={tree} />
93+
{tree.getItems().map((item) => (
94+
<Fragment key={item.getId()}>
95+
{item.isRenaming() ? (
96+
<div
97+
className="renaming-item"
98+
style={{ marginLeft: `${item.getItemMeta().level * 20}px` }}
99+
>
100+
<input {...item.getRenameInputProps()} />
101+
</div>
102+
) : (
103+
<div className="outeritem">
104+
<button
105+
{...item.getProps()}
106+
style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
107+
>
108+
<div className={getCssClass(item)}>{item.getItemName()}</div>
109+
</button>
110+
<button
111+
onClick={() => {
112+
const parent = item.getParent();
113+
if (!parent) return;
114+
delete data[item.getId()];
115+
const newParentChildren = data[
116+
parent.getId()
117+
].children?.filter((id) => id !== item.getId());
118+
data[parent.getId()].children = newParentChildren;
119+
parent.updateCachedChildrenIds(newParentChildren ?? []);
120+
tree.rebuildTree();
121+
}}
122+
>
123+
delete
124+
</button>
125+
<button onClick={() => item.startRenaming()}>rename</button>
126+
</div>
127+
)}
128+
</Fragment>
129+
))}
130+
<div style={tree.getDragLineStyle()} className="dragline" />
131+
</div>
132+
133+
<div className="actionbar">
134+
<button className="actionbtn" onClick={() => tree.openSearch()}>
135+
Search items
136+
</button>
137+
<button
138+
className="actionbtn"
139+
onClick={() => tree.getItemInstance("fruit").startRenaming()}
140+
>
141+
Rename Fruit
142+
</button>
143+
<button
144+
className="actionbtn"
145+
onClick={() => {
146+
const parent = tree.getItemInstance("fruit").getParent();
147+
if (!parent) return;
148+
delete data.fruit;
149+
const newParentChildren = data[parent.getId()].children?.filter(
150+
(id) => id !== "fruit",
151+
);
152+
data[parent.getId()].children = newParentChildren;
153+
parent.updateCachedChildrenIds(newParentChildren ?? []);
154+
tree.rebuildTree();
155+
}}
156+
>
157+
Delete Fruit
158+
</button>
159+
<button
160+
className="actionbtn"
161+
onClick={() => {
162+
if (!data.fruit) return;
163+
const newId = `new-${newItemId++}`;
164+
data[newId] = { name: "New item" };
165+
data.fruit.children = [...(data.fruit.children || []), newId];
166+
tree
167+
.getItemInstance("fruit")
168+
.updateCachedChildrenIds(data.fruit.children);
169+
tree.rebuildTree();
170+
}}
171+
>
172+
Insert new item into fruit
173+
</button>
174+
<div
175+
className="foreign-dragsource"
176+
draggable
177+
onDragStart={(e) => {
178+
e.dataTransfer.setData("text/plain", "hello world");
179+
}}
180+
>
181+
Drag me into the tree!
182+
</div>
183+
</div>
184+
</>
185+
);
186+
};

packages/sb-react/src/guides/data-in-react-state.stories.tsx renamed to packages/sb-react/src/guides/external-data-management/data-in-react-state.stories.tsx

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createOnDropHandler,
66
dragAndDropFeature,
77
hotkeysCoreFeature,
8+
insertItemsAtTarget,
89
keyboardDragAndDropFeature,
910
renamingFeature,
1011
searchFeature,
@@ -13,11 +14,11 @@ import {
1314
} from "@headless-tree/core";
1415
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
1516
import cn from "classnames";
16-
import { DemoItem, createDemoData } from "../utils/data";
17+
import { DemoItem, createDemoData } from "../../utils/data";
1718

1819
const meta = {
19-
title: "React/Guides/Data In React State",
20-
tags: ["feature/dnd", "homepage"],
20+
title: "React/Guides/External Data Management/Data In React State",
21+
tags: ["guide", "external data"],
2122
} satisfies Meta;
2223

2324
export default meta;
@@ -38,15 +39,6 @@ const getCssClass = (item: ItemInstance<DemoItem>) =>
3839
export const DataInReactState = () => {
3940
const [treeData, setTreeData] = useState(() => createDemoData().data);
4041

41-
const insertNewItem = (dataTransfer: DataTransfer) => {
42-
const newId = `new-${newItemId++}`;
43-
setTreeData((prev) => ({
44-
...prev,
45-
[newId]: { name: dataTransfer.getData("text/plain") },
46-
}));
47-
return newId;
48-
};
49-
5042
const tree = useTree<DemoItem>({
5143
initialState: {
5244
expandedItems: ["fruit"],
@@ -87,6 +79,27 @@ export const DataInReactState = () => {
8779
renamingFeature,
8880
searchFeature,
8981
],
82+
83+
// For dragging foreign items into the tree:
84+
canDropForeignDragObject: (_, target) => target.item.isFolder(),
85+
onDropForeignDragObject: async (dataTransfer, target) => {
86+
const newId = `new-${newItemId++}`;
87+
setTreeData((prev) => ({
88+
...prev,
89+
[newId]: { name: dataTransfer.getData("text/plain") },
90+
}));
91+
await insertItemsAtTarget([newId], target, (item, newChildrenIds) => {
92+
setTreeData((prev) => {
93+
const newData = { ...prev };
94+
newData[item.getId()] = {
95+
...newData[item.getId()],
96+
children: newChildrenIds,
97+
};
98+
return newData;
99+
});
100+
});
101+
tree.scheduleRebuildTree();
102+
},
90103
});
91104

92105
return (
@@ -109,12 +122,34 @@ export const DataInReactState = () => {
109122
<input {...item.getRenameInputProps()} />
110123
</div>
111124
) : (
112-
<button
113-
{...item.getProps()}
114-
style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
115-
>
116-
<div className={getCssClass(item)}>{item.getItemName()}</div>
117-
</button>
125+
<div className="outeritem">
126+
<button
127+
{...item.getProps()}
128+
style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
129+
>
130+
<div className={getCssClass(item)}>{item.getItemName()}</div>
131+
</button>
132+
<button
133+
onClick={() => {
134+
const parent = item.getParent()?.getId();
135+
if (!parent) return;
136+
setTreeData((prev) => {
137+
// This only removes the item itself, not nested children. Depending
138+
// on your use case, you might want to do that as well.
139+
const newData = { ...prev };
140+
delete newData[item.getId()];
141+
newData[parent].children = newData[
142+
parent
143+
].children?.filter((id) => id !== item.getId());
144+
return newData;
145+
});
146+
tree.scheduleRebuildTree();
147+
}}
148+
>
149+
delete
150+
</button>
151+
<button onClick={() => item.startRenaming()}>rename</button>
152+
</div>
118153
)}
119154
</Fragment>
120155
))}
@@ -149,6 +184,30 @@ export const DataInReactState = () => {
149184
>
150185
Delete Fruit
151186
</button>
187+
<button
188+
className="actionbtn"
189+
onClick={() => {
190+
setTreeData((prev) => {
191+
const newData = { ...prev };
192+
const newId = `new-${newItemId++}`;
193+
newData[newId] = { name: "New item" };
194+
newData.fruit.children = [...newData.fruit.children!, newId];
195+
return newData;
196+
});
197+
tree.scheduleRebuildTree();
198+
}}
199+
>
200+
Insert new item into Fruit
201+
</button>
202+
<div
203+
className="foreign-dragsource"
204+
draggable
205+
onDragStart={(e) => {
206+
e.dataTransfer.setData("text/plain", "hello world");
207+
}}
208+
>
209+
Drag me into the tree!
210+
</div>
152211
</div>
153212
<pre>{JSON.stringify(treeData, null, 2)}</pre>
154213
</>

0 commit comments

Comments
 (0)