@headless-tree/core 0.0.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/cjs/core/create-tree.js +13 -4
  3. package/lib/cjs/features/async-data-loader/feature.js +73 -48
  4. package/lib/cjs/features/async-data-loader/types.d.ts +17 -14
  5. package/lib/cjs/features/drag-and-drop/feature.js +98 -93
  6. package/lib/cjs/features/drag-and-drop/types.d.ts +17 -29
  7. package/lib/cjs/features/drag-and-drop/types.js +7 -7
  8. package/lib/cjs/features/drag-and-drop/utils.d.ts +25 -3
  9. package/lib/cjs/features/drag-and-drop/utils.js +51 -51
  10. package/lib/cjs/features/expand-all/feature.js +26 -3
  11. package/lib/cjs/features/expand-all/types.d.ts +3 -1
  12. package/lib/cjs/features/hotkeys-core/feature.js +7 -3
  13. package/lib/cjs/features/hotkeys-core/types.d.ts +4 -5
  14. package/lib/cjs/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  15. package/lib/cjs/features/keyboard-drag-and-drop/feature.js +206 -0
  16. package/lib/cjs/features/keyboard-drag-and-drop/types.d.ts +27 -0
  17. package/lib/cjs/features/keyboard-drag-and-drop/types.js +11 -0
  18. package/lib/cjs/features/prop-memoization/feature.js +33 -11
  19. package/lib/cjs/features/prop-memoization/types.d.ts +8 -3
  20. package/lib/cjs/features/renaming/feature.js +1 -1
  21. package/lib/cjs/features/search/feature.js +2 -0
  22. package/lib/cjs/features/search/types.d.ts +2 -2
  23. package/lib/cjs/features/selection/feature.js +4 -4
  24. package/lib/cjs/features/selection/types.d.ts +1 -1
  25. package/lib/cjs/features/sync-data-loader/feature.js +31 -5
  26. package/lib/cjs/features/sync-data-loader/types.d.ts +5 -5
  27. package/lib/cjs/features/tree/feature.js +4 -9
  28. package/lib/cjs/features/tree/types.d.ts +7 -5
  29. package/lib/cjs/index.d.ts +2 -0
  30. package/lib/cjs/index.js +2 -0
  31. package/lib/cjs/mddocs-entry.d.ts +10 -0
  32. package/lib/cjs/test-utils/test-tree-do.d.ts +2 -2
  33. package/lib/cjs/test-utils/test-tree-do.js +19 -6
  34. package/lib/cjs/test-utils/test-tree-expect.d.ts +5 -3
  35. package/lib/cjs/test-utils/test-tree-expect.js +3 -0
  36. package/lib/cjs/test-utils/test-tree.d.ts +2 -1
  37. package/lib/cjs/test-utils/test-tree.js +24 -21
  38. package/lib/cjs/types/core.d.ts +2 -1
  39. package/lib/cjs/utilities/create-on-drop-handler.d.ts +2 -2
  40. package/lib/cjs/utilities/create-on-drop-handler.js +13 -4
  41. package/lib/cjs/utilities/insert-items-at-target.d.ts +2 -2
  42. package/lib/cjs/utilities/insert-items-at-target.js +21 -12
  43. package/lib/cjs/utilities/remove-items-from-parents.d.ts +1 -1
  44. package/lib/cjs/utilities/remove-items-from-parents.js +12 -3
  45. package/lib/esm/core/create-tree.js +13 -4
  46. package/lib/esm/features/async-data-loader/feature.js +73 -48
  47. package/lib/esm/features/async-data-loader/types.d.ts +17 -14
  48. package/lib/esm/features/drag-and-drop/feature.js +99 -94
  49. package/lib/esm/features/drag-and-drop/types.d.ts +17 -29
  50. package/lib/esm/features/drag-and-drop/types.js +6 -6
  51. package/lib/esm/features/drag-and-drop/utils.d.ts +25 -3
  52. package/lib/esm/features/drag-and-drop/utils.js +45 -49
  53. package/lib/esm/features/expand-all/feature.js +26 -3
  54. package/lib/esm/features/expand-all/types.d.ts +3 -1
  55. package/lib/esm/features/hotkeys-core/feature.js +7 -3
  56. package/lib/esm/features/hotkeys-core/types.d.ts +4 -5
  57. package/lib/esm/features/keyboard-drag-and-drop/feature.d.ts +2 -0
  58. package/lib/esm/features/keyboard-drag-and-drop/feature.js +203 -0
  59. package/lib/esm/features/keyboard-drag-and-drop/types.d.ts +27 -0
  60. package/lib/esm/features/keyboard-drag-and-drop/types.js +8 -0
  61. package/lib/esm/features/prop-memoization/feature.js +33 -11
  62. package/lib/esm/features/prop-memoization/types.d.ts +8 -3
  63. package/lib/esm/features/renaming/feature.js +1 -1
  64. package/lib/esm/features/search/feature.js +2 -0
  65. package/lib/esm/features/search/types.d.ts +2 -2
  66. package/lib/esm/features/selection/feature.js +4 -4
  67. package/lib/esm/features/selection/types.d.ts +1 -1
  68. package/lib/esm/features/sync-data-loader/feature.js +31 -5
  69. package/lib/esm/features/sync-data-loader/types.d.ts +5 -5
  70. package/lib/esm/features/tree/feature.js +4 -9
  71. package/lib/esm/features/tree/types.d.ts +7 -5
  72. package/lib/esm/index.d.ts +2 -0
  73. package/lib/esm/index.js +2 -0
  74. package/lib/esm/mddocs-entry.d.ts +10 -0
  75. package/lib/esm/test-utils/test-tree-do.d.ts +2 -2
  76. package/lib/esm/test-utils/test-tree-do.js +19 -6
  77. package/lib/esm/test-utils/test-tree-expect.d.ts +5 -3
  78. package/lib/esm/test-utils/test-tree-expect.js +3 -0
  79. package/lib/esm/test-utils/test-tree.d.ts +2 -1
  80. package/lib/esm/test-utils/test-tree.js +24 -21
  81. package/lib/esm/types/core.d.ts +2 -1
  82. package/lib/esm/utilities/create-on-drop-handler.d.ts +2 -2
  83. package/lib/esm/utilities/create-on-drop-handler.js +13 -4
  84. package/lib/esm/utilities/insert-items-at-target.d.ts +2 -2
  85. package/lib/esm/utilities/insert-items-at-target.js +21 -12
  86. package/lib/esm/utilities/remove-items-from-parents.d.ts +1 -1
  87. package/lib/esm/utilities/remove-items-from-parents.js +12 -3
  88. package/package.json +2 -2
  89. package/src/core/core.spec.ts +31 -0
  90. package/src/core/create-tree.ts +15 -5
  91. package/src/features/async-data-loader/async-data-loader.spec.ts +10 -6
  92. package/src/features/async-data-loader/feature.ts +76 -48
  93. package/src/features/async-data-loader/types.ts +18 -11
  94. package/src/features/drag-and-drop/drag-and-drop.spec.ts +75 -89
  95. package/src/features/drag-and-drop/feature.ts +26 -22
  96. package/src/features/drag-and-drop/types.ts +23 -35
  97. package/src/features/drag-and-drop/utils.ts +70 -57
  98. package/src/features/expand-all/feature.ts +29 -5
  99. package/src/features/expand-all/types.ts +3 -1
  100. package/src/features/hotkeys-core/feature.ts +4 -0
  101. package/src/features/hotkeys-core/types.ts +4 -13
  102. package/src/features/keyboard-drag-and-drop/feature.ts +255 -0
  103. package/src/features/keyboard-drag-and-drop/keyboard-drag-and-drop.spec.ts +402 -0
  104. package/src/features/keyboard-drag-and-drop/types.ts +30 -0
  105. package/src/features/prop-memoization/feature.ts +27 -8
  106. package/src/features/prop-memoization/prop-memoization.spec.ts +2 -2
  107. package/src/features/prop-memoization/types.ts +8 -3
  108. package/src/features/renaming/feature.ts +8 -2
  109. package/src/features/search/feature.ts +2 -0
  110. package/src/features/search/types.ts +2 -2
  111. package/src/features/selection/feature.ts +4 -4
  112. package/src/features/selection/types.ts +1 -1
  113. package/src/features/sync-data-loader/feature.ts +26 -7
  114. package/src/features/sync-data-loader/types.ts +5 -5
  115. package/src/features/tree/feature.ts +8 -13
  116. package/src/features/tree/types.ts +7 -5
  117. package/src/index.ts +2 -0
  118. package/src/mddocs-entry.ts +16 -0
  119. package/src/test-utils/test-tree-do.ts +3 -3
  120. package/src/test-utils/test-tree-expect.ts +7 -2
  121. package/src/test-utils/test-tree.ts +26 -22
  122. package/src/types/core.ts +2 -0
  123. package/src/utilities/create-on-drop-handler.ts +4 -4
  124. package/src/utilities/insert-items-at-target.ts +18 -14
  125. package/src/utilities/remove-items-from-parents.ts +6 -3
@@ -1,3 +1,3 @@
1
1
  import { ItemInstance } from "../types/core";
2
- import { DropTarget } from "../features/drag-and-drop/types";
3
- export declare const insertItemsAtTarget: <T>(itemIds: string[], target: DropTarget<T>, onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void) => void;
2
+ import { DragTarget } from "../features/drag-and-drop/types";
3
+ export declare const insertItemsAtTarget: <T>(itemIds: string[], target: DragTarget<T>, onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => Promise<void> | void) => Promise<void>;
@@ -1,11 +1,21 @@
1
- export const insertItemsAtTarget = (itemIds, target, onChangeChildren) => {
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ export const insertItemsAtTarget = (itemIds, target, onChangeChildren) => __awaiter(void 0, void 0, void 0, function* () {
11
+ yield target.item.getTree().waitForItemChildrenLoaded(target.item.getId());
12
+ const oldChildrenIds = target.item
13
+ .getTree()
14
+ .retrieveChildrenIds(target.item.getId());
2
15
  // add moved items to new common parent, if dropped onto parent
3
- if (target.childIndex === null) {
4
- const newChildren = [
5
- ...target.item.getChildren().map((item) => item.getId()),
6
- ...itemIds,
7
- ];
8
- onChangeChildren(target.item, newChildren);
16
+ if (!("childIndex" in target)) {
17
+ const newChildren = [...oldChildrenIds, ...itemIds];
18
+ yield onChangeChildren(target.item, newChildren);
9
19
  if (target.item && "updateCachedChildrenIds" in target.item) {
10
20
  target.item.updateCachedChildrenIds(newChildren);
11
21
  }
@@ -13,15 +23,14 @@ export const insertItemsAtTarget = (itemIds, target, onChangeChildren) => {
13
23
  return;
14
24
  }
15
25
  // add moved items to new common parent, if dropped between siblings
16
- const oldChildren = target.item.getChildren();
17
26
  const newChildren = [
18
- ...oldChildren.slice(0, target.insertionIndex).map((item) => item.getId()),
27
+ ...oldChildrenIds.slice(0, target.insertionIndex),
19
28
  ...itemIds,
20
- ...oldChildren.slice(target.insertionIndex).map((item) => item.getId()),
29
+ ...oldChildrenIds.slice(target.insertionIndex),
21
30
  ];
22
- onChangeChildren(target.item, newChildren);
31
+ yield onChangeChildren(target.item, newChildren);
23
32
  if (target.item && "updateCachedChildrenIds" in target.item) {
24
33
  target.item.updateCachedChildrenIds(newChildren);
25
34
  }
26
35
  target.item.getTree().rebuildTree();
27
- };
36
+ });
@@ -1,2 +1,2 @@
1
1
  import { ItemInstance } from "../types/core";
2
- export declare const removeItemsFromParents: <T>(movedItems: ItemInstance<T>[], onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void) => void;
2
+ export declare const removeItemsFromParents: <T>(movedItems: ItemInstance<T>[], onChangeChildren: (item: ItemInstance<T>, newChildrenIds: string[]) => void | Promise<void>) => Promise<void>;
@@ -1,4 +1,13 @@
1
- export const removeItemsFromParents = (movedItems, onChangeChildren) => {
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ export const removeItemsFromParents = (movedItems, onChangeChildren) => __awaiter(void 0, void 0, void 0, function* () {
2
11
  const movedItemsIds = movedItems.map((item) => item.getId());
3
12
  const uniqueParents = [
4
13
  ...new Set(movedItems.map((item) => item.getParent())),
@@ -9,11 +18,11 @@ export const removeItemsFromParents = (movedItems, onChangeChildren) => {
9
18
  const newChildren = siblings
10
19
  .filter((sibling) => !movedItemsIds.includes(sibling.getId()))
11
20
  .map((i) => i.getId());
12
- onChangeChildren(parent, newChildren);
21
+ yield onChangeChildren(parent, newChildren);
13
22
  if (parent && "updateCachedChildrenIds" in parent) {
14
23
  parent === null || parent === void 0 ? void 0 : parent.updateCachedChildrenIds(newChildren);
15
24
  }
16
25
  }
17
26
  }
18
27
  movedItems[0].getTree().rebuildTree();
19
- };
28
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.14",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "devDependencies": {
23
23
  "jsdom": "^26.0.0",
24
- "typescript": "^5.7.3",
24
+ "typescript": "^5.7.2",
25
25
  "vitest": "^3.0.3"
26
26
  }
27
27
  }
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { TestTree } from "../test-utils/test-tree";
3
+ import { buildStaticInstance } from "./build-static-instance";
3
4
 
4
5
  declare module "../types/core" {
5
6
  export interface TreeInstance<T> {
@@ -17,6 +18,36 @@ const factory = TestTree.default({});
17
18
 
18
19
  describe("core-feature/prop-memoization", () => {
19
20
  factory.forSuits((tree) => {
21
+ describe("rebuilds item instance", () => {
22
+ it("rebuilds when explicitly invoked", async () => {
23
+ const instanceBuilder = vi.fn().mockImplementation(buildStaticInstance);
24
+ const testTree = await tree
25
+ .with({ instanceBuilder })
26
+ .createTestCaseTree();
27
+ expect(instanceBuilder).toBeCalled();
28
+ instanceBuilder.mockClear();
29
+ testTree.instance.rebuildTree();
30
+ // if tree structure doesnt mutate, only root tree item is rebuilt actually
31
+ expect(instanceBuilder).toBeCalled();
32
+ });
33
+
34
+ it("rebuilds when config changes with new expanded items", async () => {
35
+ const instanceBuilder = vi.fn().mockImplementation(buildStaticInstance);
36
+ const testTree = await tree
37
+ .with({ instanceBuilder })
38
+ .createTestCaseTree();
39
+ expect(instanceBuilder).toBeCalled();
40
+ instanceBuilder.mockClear();
41
+ testTree.instance.setConfig((oldCfg) => ({
42
+ ...oldCfg,
43
+ state: {
44
+ expandedItems: ["x4"],
45
+ },
46
+ }));
47
+ expect(instanceBuilder).toBeCalled();
48
+ });
49
+ });
50
+
20
51
  describe("calls prev in correct order", () => {
21
52
  it("tree instance with overwrite marks, order 1", async () => {
22
53
  const testTree = await tree
@@ -137,6 +137,7 @@ export const createTree = <T>(
137
137
  // Not necessary, since I think the subupdate below keeps the state fresh anyways?
138
138
  // state = typeof updater === "function" ? updater(state) : updater;
139
139
  config.setState?.(state); // TODO this cant be right... This doesnt allow external state updates
140
+ // TODO this is never used, remove
140
141
  },
141
142
  applySubStateUpdate: <K extends keyof TreeState<any>>(
142
143
  {},
@@ -157,10 +158,20 @@ export const createTree = <T>(
157
158
  },
158
159
  getConfig: () => config,
159
160
  setConfig: (_, updater) => {
160
- config = typeof updater === "function" ? updater(config) : updater;
161
-
162
- if (config.state) {
163
- state = { ...state, ...config.state };
161
+ const newConfig =
162
+ typeof updater === "function" ? updater(config) : updater;
163
+ const hasChangedExpandedItems =
164
+ newConfig.state?.expandedItems &&
165
+ newConfig.state?.expandedItems !== state.expandedItems;
166
+ config = newConfig;
167
+
168
+ if (newConfig.state) {
169
+ state = { ...state, ...newConfig.state };
170
+ }
171
+ if (hasChangedExpandedItems) {
172
+ // if expanded items where changed from the outside
173
+ rebuildItemMeta();
174
+ config.setState?.(state);
164
175
  }
165
176
  },
166
177
  getItemInstance: ({}, itemId) => itemInstancesMap[itemId],
@@ -186,7 +197,6 @@ export const createTree = <T>(
186
197
  getHotkeyPresets: () => hotkeyPresets,
187
198
  },
188
199
  itemInstance: {
189
- // TODO just change to a getRef method that memoizes, maybe as part of getProps
190
200
  registerElement: ({ itemId, item }, element) => {
191
201
  if (itemElementsMap[itemId] === element) {
192
202
  return;
@@ -41,11 +41,17 @@ describe("core-feature/selections", () => {
41
41
 
42
42
  describe("calls handlers", () => {
43
43
  it("updates setLoadingItems", async () => {
44
- const setLoadingItems = tree.mockedHandler("setLoadingItems");
44
+ const setLoadingItemChildrens = tree.mockedHandler(
45
+ "setLoadingItemChildrens",
46
+ );
47
+ const setLoadingItemData = tree.mockedHandler("setLoadingItemData");
45
48
  tree.do.selectItem("x12");
46
- expect(setLoadingItems).toHaveBeenCalledWith(["x12"]);
49
+ expect(setLoadingItemChildrens).toHaveBeenCalledWith(["x12"]);
50
+ expect(setLoadingItemData).not.toHaveBeenCalled();
47
51
  await tree.resolveAsyncVisibleItems();
48
- expect(setLoadingItems).toHaveBeenCalledWith([]);
52
+ expect(setLoadingItemChildrens).toHaveBeenCalledWith([]);
53
+ expect(setLoadingItemData).toHaveBeenCalledWith(["x121"]);
54
+ expect(setLoadingItemData).toHaveBeenCalledWith([]);
49
55
  });
50
56
 
51
57
  it("calls onLoadedItem", async () => {
@@ -76,7 +82,7 @@ describe("core-feature/selections", () => {
76
82
  `${id}3`,
77
83
  `${id}4`,
78
84
  ]);
79
- const suiteTree = tree.with({ asyncDataLoader: { getItem, getChildren } });
85
+ const suiteTree = tree.with({ dataLoader: { getItem, getChildren } });
80
86
  suiteTree.resetBeforeEach();
81
87
 
82
88
  it("invalidates item data on item instance", async () => {
@@ -119,6 +125,4 @@ describe("core-feature/selections", () => {
119
125
  expect(getChildren).toHaveBeenCalledTimes(1);
120
126
  });
121
127
  });
122
-
123
- describe.todo("getChildrenWithData");
124
128
  });
@@ -1,28 +1,57 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { AsyncDataLoaderRef } from "./types";
2
+ import { AsyncDataLoaderDataRef } from "./types";
3
3
  import { makeStateUpdater } from "../../utils";
4
4
 
5
5
  export const asyncDataLoaderFeature: FeatureImplementation = {
6
6
  key: "async-data-loader",
7
7
 
8
8
  getInitialState: (initialState) => ({
9
- loadingItems: [],
9
+ loadingItemData: [],
10
+ loadingItemChildrens: [],
10
11
  ...initialState,
11
12
  }),
12
13
 
13
14
  getDefaultConfig: (defaultConfig, tree) => ({
14
- setLoadingItems: makeStateUpdater("loadingItems", tree),
15
+ setLoadingItemData: makeStateUpdater("loadingItemData", tree),
16
+ setLoadingItemChildrens: makeStateUpdater("loadingItemChildrens", tree),
15
17
  ...defaultConfig,
16
18
  }),
17
19
 
18
20
  stateHandlerNames: {
19
- loadingItems: "setLoadingItems",
21
+ loadingItemData: "setLoadingItemData",
22
+ loadingItemChildrens: "setLoadingItemChildrens",
20
23
  },
21
24
 
22
25
  treeInstance: {
26
+ waitForItemDataLoaded: async ({ tree }, itemId) => {
27
+ tree.retrieveItemData(itemId);
28
+ if (!tree.getState().loadingItemData.includes(itemId)) {
29
+ return;
30
+ }
31
+ await new Promise<void>((resolve) => {
32
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
33
+ dataRef.current.awaitingItemDataLoading ??= {};
34
+ dataRef.current.awaitingItemDataLoading[itemId] ??= [];
35
+ dataRef.current.awaitingItemDataLoading[itemId].push(resolve);
36
+ });
37
+ },
38
+
39
+ waitForItemChildrenLoaded: async ({ tree }, itemId) => {
40
+ tree.retrieveChildrenIds(itemId);
41
+ if (!tree.getState().loadingItemChildrens.includes(itemId)) {
42
+ return;
43
+ }
44
+ await new Promise<void>((resolve) => {
45
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
46
+ dataRef.current.awaitingItemChildrensLoading ??= {};
47
+ dataRef.current.awaitingItemChildrensLoading[itemId] ??= [];
48
+ dataRef.current.awaitingItemChildrensLoading[itemId].push(resolve);
49
+ });
50
+ },
51
+
23
52
  retrieveItemData: ({ tree }, itemId) => {
24
53
  const config = tree.getConfig();
25
- const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
54
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
26
55
  dataRef.current.itemData ??= {};
27
56
  dataRef.current.childrenIds ??= {};
28
57
 
@@ -30,18 +59,25 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
30
59
  return dataRef.current.itemData[itemId];
31
60
  }
32
61
 
33
- if (!tree.getState().loadingItems.includes(itemId)) {
34
- tree.applySubStateUpdate("loadingItems", (loadingItems) => [
35
- ...loadingItems,
62
+ if (!tree.getState().loadingItemData.includes(itemId)) {
63
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
64
+ ...loadingItemData,
36
65
  itemId,
37
66
  ]);
38
- config.asyncDataLoader?.getItem(itemId).then((item) => {
67
+
68
+ (async () => {
69
+ const item = await config.dataLoader.getItem(itemId);
39
70
  dataRef.current.itemData[itemId] = item;
40
71
  config.onLoadedItem?.(itemId, item);
41
- tree.applySubStateUpdate("loadingItems", (loadingItems) =>
42
- loadingItems.filter((id) => id !== itemId),
72
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
73
+ loadingItemData.filter((id) => id !== itemId),
43
74
  );
44
- });
75
+
76
+ dataRef.current.awaitingItemDataLoading?.[itemId].forEach((cb) =>
77
+ cb(),
78
+ );
79
+ delete dataRef.current.awaitingItemDataLoading?.[itemId];
80
+ })();
45
81
  }
46
82
 
47
83
  return config.createLoadingItemData?.() ?? null;
@@ -49,46 +85,37 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
49
85
 
50
86
  retrieveChildrenIds: ({ tree }, itemId) => {
51
87
  const config = tree.getConfig();
52
- const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
53
- dataRef.current.itemData ??= {};
88
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
54
89
  dataRef.current.childrenIds ??= {};
55
90
  if (dataRef.current.childrenIds[itemId]) {
56
91
  return dataRef.current.childrenIds[itemId];
57
92
  }
58
93
 
59
- if (tree.getState().loadingItems.includes(itemId)) {
94
+ if (tree.getState().loadingItemChildrens.includes(itemId)) {
60
95
  return [];
61
96
  }
62
97
 
63
- tree.applySubStateUpdate("loadingItems", (loadingItems) => [
64
- ...loadingItems,
65
- itemId,
66
- ]);
67
-
68
- if (config.asyncDataLoader?.getChildrenWithData) {
69
- config.asyncDataLoader?.getChildrenWithData(itemId).then((children) => {
70
- for (const { id, data } of children) {
71
- dataRef.current.itemData[id] = data;
72
- config.onLoadedItem?.(id, data);
73
- }
74
- const childrenIds = children.map(({ id }) => id);
75
- dataRef.current.childrenIds[itemId] = childrenIds;
76
- config.onLoadedChildren?.(itemId, childrenIds);
77
- tree.applySubStateUpdate("loadingItems", (loadingItems) =>
78
- loadingItems.filter((id) => id !== itemId),
79
- );
80
- tree.rebuildTree();
81
- });
82
- } else {
83
- config.asyncDataLoader?.getChildren(itemId).then((childrenIds) => {
84
- dataRef.current.childrenIds[itemId] = childrenIds;
85
- config.onLoadedChildren?.(itemId, childrenIds);
86
- tree.applySubStateUpdate("loadingItems", (loadingItems) =>
87
- loadingItems.filter((id) => id !== itemId),
88
- );
89
- tree.rebuildTree();
90
- });
91
- }
98
+ tree.applySubStateUpdate(
99
+ "loadingItemChildrens",
100
+ (loadingItemChildrens) => [...loadingItemChildrens, itemId],
101
+ );
102
+
103
+ (async () => {
104
+ const childrenIds = await config.dataLoader.getChildren(itemId);
105
+ dataRef.current.childrenIds[itemId] = childrenIds;
106
+ config.onLoadedChildren?.(itemId, childrenIds);
107
+ tree.applySubStateUpdate(
108
+ "loadingItemChildrens",
109
+ (loadingItemChildrens) =>
110
+ loadingItemChildrens.filter((id) => id !== itemId),
111
+ );
112
+ tree.rebuildTree();
113
+
114
+ dataRef.current.awaitingItemChildrensLoading?.[itemId]?.forEach((cb) =>
115
+ cb(),
116
+ );
117
+ delete dataRef.current.awaitingItemChildrensLoading?.[itemId];
118
+ })();
92
119
 
93
120
  return [];
94
121
  },
@@ -96,19 +123,20 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
96
123
 
97
124
  itemInstance: {
98
125
  isLoading: ({ tree, item }) =>
99
- tree.getState().loadingItems.includes(item.getItemMeta().itemId),
126
+ tree.getState().loadingItemData.includes(item.getItemMeta().itemId) ||
127
+ tree.getState().loadingItemChildrens.includes(item.getItemMeta().itemId),
100
128
  invalidateItemData: ({ tree, itemId }) => {
101
- const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
129
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
102
130
  delete dataRef.current.itemData?.[itemId];
103
131
  tree.retrieveItemData(itemId);
104
132
  },
105
133
  invalidateChildrenIds: ({ tree, itemId }) => {
106
- const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
134
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
107
135
  delete dataRef.current.childrenIds?.[itemId];
108
136
  tree.retrieveChildrenIds(itemId);
109
137
  },
110
138
  updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
111
- const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
139
+ const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
112
140
  dataRef.current.childrenIds[itemId] = childrenIds;
113
141
  tree.rebuildTree();
114
142
  },
@@ -1,33 +1,40 @@
1
1
  import { SetStateFn } from "../../types/core";
2
2
  import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
3
3
 
4
- export type AsyncTreeDataLoader<T> = {
5
- getItem: (itemId: string) => Promise<T>;
6
- getChildren: (itemId: string) => Promise<string[]>;
7
- getChildrenWithData?: (itemId: string) => Promise<{ id: string; data: T }[]>;
8
- };
4
+ type AwaitingLoaderCallbacks = Record<string, (() => void)[]>;
9
5
 
10
- export type AsyncDataLoaderRef<T = any> = {
6
+ export interface AsyncDataLoaderDataRef<T = any> {
11
7
  itemData: Record<string, T>;
12
8
  childrenIds: Record<string, string[]>;
13
- };
9
+ awaitingItemDataLoading: AwaitingLoaderCallbacks;
10
+ awaitingItemChildrensLoading: AwaitingLoaderCallbacks;
11
+ }
14
12
 
15
13
  /**
16
14
  * @category Async Data Loader/General
17
15
  * */
18
16
  export type AsyncDataLoaderFeatureDef<T> = {
19
17
  state: {
20
- loadingItems: string[];
18
+ loadingItemData: string[];
19
+ loadingItemChildrens: string[];
21
20
  };
22
21
  config: {
23
22
  rootItemId: string;
23
+
24
+ /** Will be called when HT retrieves item data for an item whose item data is asynchronously being loaded.
25
+ * Can be used to create placeholder data to use for rendering the tree item while it is loaded. If not defined,
26
+ * the tree item data will be null. */
24
27
  createLoadingItemData?: () => T;
25
- setLoadingItems?: SetStateFn<string[]>;
28
+
29
+ setLoadingItemData?: SetStateFn<string[]>;
30
+ setLoadingItemChildrens?: SetStateFn<string[]>;
26
31
  onLoadedItem?: (itemId: string, item: T) => void;
27
32
  onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
28
- asyncDataLoader?: AsyncTreeDataLoader<T>;
29
33
  };
30
- treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"];
34
+ treeInstance: SyncDataLoaderFeatureDef<T>["treeInstance"] & {
35
+ waitForItemDataLoaded: (itemId: string) => Promise<void>;
36
+ waitForItemChildrenLoaded: (itemId: string) => Promise<void>;
37
+ };
31
38
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
32
39
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
33
40
  invalidateItemData: () => void;