@headless-tree/core 0.0.10 → 0.0.11

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 (114) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
  3. package/lib/cjs/core/build-proxified-instance.js +58 -0
  4. package/lib/cjs/core/build-static-instance.d.ts +2 -0
  5. package/lib/cjs/core/build-static-instance.js +27 -0
  6. package/lib/cjs/core/create-tree.js +55 -36
  7. package/lib/cjs/features/async-data-loader/feature.js +37 -23
  8. package/lib/cjs/features/async-data-loader/types.d.ts +2 -1
  9. package/lib/cjs/features/drag-and-drop/feature.js +64 -32
  10. package/lib/cjs/features/drag-and-drop/types.d.ts +13 -4
  11. package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -2
  12. package/lib/cjs/features/drag-and-drop/utils.js +140 -37
  13. package/lib/cjs/features/expand-all/feature.js +12 -6
  14. package/lib/cjs/features/main/types.d.ts +8 -2
  15. package/lib/cjs/features/renaming/feature.js +33 -18
  16. package/lib/cjs/features/renaming/types.d.ts +1 -1
  17. package/lib/cjs/features/search/feature.js +38 -24
  18. package/lib/cjs/features/search/types.d.ts +0 -1
  19. package/lib/cjs/features/selection/feature.js +23 -14
  20. package/lib/cjs/features/sync-data-loader/feature.js +7 -2
  21. package/lib/cjs/features/tree/feature.d.ts +2 -1
  22. package/lib/cjs/features/tree/feature.js +85 -63
  23. package/lib/cjs/features/tree/types.d.ts +5 -3
  24. package/lib/cjs/index.d.ts +3 -1
  25. package/lib/cjs/index.js +2 -1
  26. package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
  27. package/lib/cjs/test-utils/test-tree-do.js +99 -0
  28. package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
  29. package/lib/cjs/test-utils/test-tree-expect.js +62 -0
  30. package/lib/cjs/test-utils/test-tree.d.ts +47 -0
  31. package/lib/cjs/test-utils/test-tree.js +195 -0
  32. package/lib/cjs/types/core.d.ts +31 -15
  33. package/lib/cjs/utilities/errors.d.ts +1 -0
  34. package/lib/cjs/utilities/errors.js +5 -0
  35. package/lib/cjs/utilities/insert-items-at-target.js +10 -3
  36. package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
  37. package/lib/cjs/utils.d.ts +3 -3
  38. package/lib/cjs/utils.js +6 -6
  39. package/lib/esm/core/build-proxified-instance.d.ts +2 -0
  40. package/lib/esm/core/build-proxified-instance.js +54 -0
  41. package/lib/esm/core/build-static-instance.d.ts +2 -0
  42. package/lib/esm/core/build-static-instance.js +23 -0
  43. package/lib/esm/core/create-tree.js +55 -36
  44. package/lib/esm/features/async-data-loader/feature.js +37 -23
  45. package/lib/esm/features/async-data-loader/types.d.ts +2 -1
  46. package/lib/esm/features/drag-and-drop/feature.js +64 -32
  47. package/lib/esm/features/drag-and-drop/types.d.ts +13 -4
  48. package/lib/esm/features/drag-and-drop/utils.d.ts +1 -2
  49. package/lib/esm/features/drag-and-drop/utils.js +138 -34
  50. package/lib/esm/features/expand-all/feature.js +12 -6
  51. package/lib/esm/features/main/types.d.ts +8 -2
  52. package/lib/esm/features/renaming/feature.js +33 -18
  53. package/lib/esm/features/renaming/types.d.ts +1 -1
  54. package/lib/esm/features/search/feature.js +38 -24
  55. package/lib/esm/features/search/types.d.ts +0 -1
  56. package/lib/esm/features/selection/feature.js +23 -14
  57. package/lib/esm/features/sync-data-loader/feature.js +7 -2
  58. package/lib/esm/features/tree/feature.d.ts +2 -1
  59. package/lib/esm/features/tree/feature.js +86 -64
  60. package/lib/esm/features/tree/types.d.ts +5 -3
  61. package/lib/esm/index.d.ts +3 -1
  62. package/lib/esm/index.js +2 -1
  63. package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
  64. package/lib/esm/test-utils/test-tree-do.js +95 -0
  65. package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
  66. package/lib/esm/test-utils/test-tree-expect.js +58 -0
  67. package/lib/esm/test-utils/test-tree.d.ts +47 -0
  68. package/lib/esm/test-utils/test-tree.js +191 -0
  69. package/lib/esm/types/core.d.ts +31 -15
  70. package/lib/esm/utilities/errors.d.ts +1 -0
  71. package/lib/esm/utilities/errors.js +1 -0
  72. package/lib/esm/utilities/insert-items-at-target.js +10 -3
  73. package/lib/esm/utilities/remove-items-from-parents.js +14 -8
  74. package/lib/esm/utils.d.ts +3 -3
  75. package/lib/esm/utils.js +3 -3
  76. package/package.json +7 -3
  77. package/src/core/build-proxified-instance.ts +115 -0
  78. package/src/core/build-static-instance.ts +28 -0
  79. package/src/core/create-tree.ts +60 -62
  80. package/src/features/async-data-loader/async-data-loader.spec.ts +143 -0
  81. package/src/features/async-data-loader/feature.ts +33 -31
  82. package/src/features/async-data-loader/types.ts +3 -1
  83. package/src/features/drag-and-drop/drag-and-drop.spec.ts +716 -0
  84. package/src/features/drag-and-drop/feature.ts +109 -85
  85. package/src/features/drag-and-drop/types.ts +21 -7
  86. package/src/features/drag-and-drop/utils.ts +196 -55
  87. package/src/features/expand-all/expand-all.spec.ts +52 -0
  88. package/src/features/expand-all/feature.ts +8 -12
  89. package/src/features/hotkeys-core/feature.ts +1 -1
  90. package/src/features/main/types.ts +14 -1
  91. package/src/features/renaming/feature.ts +30 -29
  92. package/src/features/renaming/renaming.spec.ts +125 -0
  93. package/src/features/renaming/types.ts +1 -1
  94. package/src/features/search/feature.ts +34 -38
  95. package/src/features/search/search.spec.ts +115 -0
  96. package/src/features/search/types.ts +0 -1
  97. package/src/features/selection/feature.ts +29 -30
  98. package/src/features/selection/selection.spec.ts +220 -0
  99. package/src/features/sync-data-loader/feature.ts +8 -11
  100. package/src/features/tree/feature.ts +82 -87
  101. package/src/features/tree/tree.spec.ts +515 -0
  102. package/src/features/tree/types.ts +5 -3
  103. package/src/index.ts +4 -1
  104. package/src/test-utils/test-tree-do.ts +136 -0
  105. package/src/test-utils/test-tree-expect.ts +86 -0
  106. package/src/test-utils/test-tree.ts +217 -0
  107. package/src/types/core.ts +92 -33
  108. package/src/utilities/errors.ts +2 -0
  109. package/src/utilities/insert-items-at-target.ts +10 -3
  110. package/src/utilities/remove-items-from-parents.ts +15 -10
  111. package/src/utils.spec.ts +89 -0
  112. package/src/utils.ts +6 -6
  113. package/tsconfig.json +1 -0
  114. package/vitest.config.ts +6 -0
@@ -0,0 +1,115 @@
1
+ import { FeatureImplementation } from "../types/core";
2
+ import { InstanceBuilder, InstanceTypeMap } from "../features/main/types";
3
+ import { throwError } from "../utilities/errors";
4
+
5
+ const noop = () => {};
6
+
7
+ const findPrevInstanceMethod = (
8
+ features: FeatureImplementation[],
9
+ instanceType: keyof InstanceTypeMap,
10
+ methodKey: string,
11
+ featureSearchIndex: number,
12
+ ) => {
13
+ for (let i = featureSearchIndex; i >= 0; i--) {
14
+ const feature = features[i];
15
+ const itemInstanceMethod = feature[instanceType]?.[methodKey];
16
+ if (itemInstanceMethod) {
17
+ return i;
18
+ }
19
+ }
20
+ return null;
21
+ };
22
+
23
+ const invokeInstanceMethod = (
24
+ features: FeatureImplementation[],
25
+ instanceType: keyof InstanceTypeMap,
26
+ opts: any,
27
+ methodKey: string,
28
+ featureIndex: number,
29
+ args: any[],
30
+ ) => {
31
+ const prevIndex = findPrevInstanceMethod(
32
+ features,
33
+ instanceType,
34
+ methodKey,
35
+ featureIndex - 1,
36
+ );
37
+ const itemInstanceMethod = features[featureIndex][instanceType]?.[methodKey]!;
38
+ return itemInstanceMethod(
39
+ {
40
+ ...opts,
41
+ prev:
42
+ prevIndex !== null
43
+ ? (...newArgs) =>
44
+ invokeInstanceMethod(
45
+ features,
46
+ instanceType,
47
+ opts,
48
+ methodKey,
49
+ prevIndex,
50
+ newArgs,
51
+ )
52
+ : null,
53
+ },
54
+ ...args,
55
+ );
56
+ };
57
+
58
+ export const buildProxiedInstance: InstanceBuilder = (
59
+ features,
60
+ instanceType,
61
+ buildOpts,
62
+ ) => {
63
+ // demo with prototypes: https://jsfiddle.net/bgenc58r/
64
+ const opts = {};
65
+ const item = new Proxy(
66
+ {},
67
+ {
68
+ has(target, key: string | symbol) {
69
+ if (typeof key === "symbol") {
70
+ return false;
71
+ }
72
+ if (key === "toJSON") {
73
+ return false;
74
+ }
75
+ const hasInstanceMethod = findPrevInstanceMethod(
76
+ features,
77
+ instanceType,
78
+ key,
79
+ features.length - 1,
80
+ );
81
+ return Boolean(hasInstanceMethod);
82
+ },
83
+ get(target, key: string | symbol) {
84
+ if (typeof key === "symbol") {
85
+ return undefined;
86
+ }
87
+ if (key === "toJSON") {
88
+ return {};
89
+ }
90
+ return (...args: any[]) => {
91
+ const featureIndex = findPrevInstanceMethod(
92
+ features,
93
+ instanceType,
94
+ key,
95
+ features.length - 1,
96
+ );
97
+
98
+ if (featureIndex === null) {
99
+ throw throwError(`feature missing for method ${key}`);
100
+ }
101
+ return invokeInstanceMethod(
102
+ features,
103
+ instanceType,
104
+ opts,
105
+ key,
106
+ featureIndex,
107
+ args,
108
+ );
109
+ };
110
+ },
111
+ },
112
+ );
113
+ Object.assign(opts, buildOpts(item));
114
+ return [item as any, noop];
115
+ };
@@ -0,0 +1,28 @@
1
+ /* eslint-disable no-continue,no-labels,no-extra-label */
2
+
3
+ import { InstanceBuilder } from "../features/main/types";
4
+
5
+ export const buildStaticInstance: InstanceBuilder = (
6
+ features,
7
+ instanceType,
8
+ buildOpts,
9
+ ) => {
10
+ const instance: any = {};
11
+ const finalize = () => {
12
+ const opts = buildOpts(instance);
13
+ featureLoop: for (let i = 0; i < features.length; i++) {
14
+ // Loop goes in forward order, because later features overwrite previous ones
15
+ // TODO loop order correct? I think so...
16
+ const definition = features[i][instanceType];
17
+ if (!definition) continue featureLoop;
18
+ methodLoop: for (const [key, method] of Object.entries(definition)) {
19
+ if (!method) continue methodLoop;
20
+ const prev = instance[key];
21
+ instance[key] = (...args: any[]) => {
22
+ return method({ ...opts, prev }, ...args);
23
+ };
24
+ }
25
+ }
26
+ };
27
+ return [instance as any, finalize];
28
+ };
@@ -5,32 +5,13 @@ import {
5
5
  TreeConfig,
6
6
  TreeInstance,
7
7
  TreeState,
8
+ Updater,
8
9
  } from "../types/core";
9
10
  import { MainFeatureDef } from "../features/main/types";
10
11
  import { treeFeature } from "../features/tree/feature";
11
12
  import { ItemMeta } from "../features/tree/types";
12
-
13
- const buildItemInstance = (
14
- features: FeatureImplementation[],
15
- tree: TreeInstance<any>,
16
- itemId: string,
17
- ) => {
18
- const itemInstance = {} as ItemInstance<any>;
19
- for (const feature of features) {
20
- Object.assign(
21
- // TODO dont run createItemInstance, but assign prototype objects instead?
22
- // https://jsfiddle.net/bgenc58r/
23
- itemInstance,
24
- feature.createItemInstance?.(
25
- { ...itemInstance },
26
- itemInstance,
27
- tree,
28
- itemId,
29
- ) ?? {},
30
- );
31
- }
32
- return itemInstance;
33
- };
13
+ import { buildStaticInstance } from "./build-static-instance";
14
+ import { throwError } from "../utilities/errors";
34
15
 
35
16
  const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
36
17
  const loadedFeatures = features?.map((feature) => feature.key);
@@ -39,7 +20,7 @@ const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
39
20
  (dep) => !loadedFeatures?.includes(dep),
40
21
  );
41
22
  if (missingDependency) {
42
- throw new Error(`${feature.key} needs ${missingDependency}`);
23
+ throw throwError(`${feature.key} needs ${missingDependency}`);
43
24
  }
44
25
  }
45
26
  };
@@ -60,25 +41,32 @@ const sortFeatures = (features: FeatureImplementation[] = []) =>
60
41
  export const createTree = <T>(
61
42
  initialConfig: TreeConfig<T>,
62
43
  ): TreeInstance<T> => {
63
- const treeInstance: TreeInstance<T> = {} as any;
64
-
44
+ const buildInstance = initialConfig.instanceBuilder ?? buildStaticInstance;
65
45
  const additionalFeatures = [
66
46
  treeFeature,
67
47
  ...sortFeatures(initialConfig.features),
68
48
  ];
69
49
  verifyFeatures(additionalFeatures);
50
+ const features = [...additionalFeatures];
51
+
52
+ const [treeInstance, finalizeTree] = buildInstance(
53
+ features,
54
+ "treeInstance",
55
+ (tree) => ({ tree }),
56
+ );
70
57
 
71
58
  let state = additionalFeatures.reduce(
72
59
  (acc, feature) => feature.getInitialState?.(acc, treeInstance) ?? acc,
73
60
  initialConfig.initialState ?? initialConfig.state ?? {},
74
61
  ) as TreeState<T>;
75
62
  let config = additionalFeatures.reduce(
76
- (acc, feature) => feature.getDefaultConfig?.(acc, treeInstance) ?? acc,
63
+ (acc, feature) =>
64
+ (feature.getDefaultConfig?.(acc, treeInstance) as TreeConfig<T>) ?? acc,
77
65
  initialConfig,
78
66
  ) as TreeConfig<T>;
79
67
  const stateHandlerNames = additionalFeatures.reduce(
80
68
  (acc, feature) => ({ ...acc, ...feature.stateHandlerNames }),
81
- {} as Record<string, string>,
69
+ {} as Record<string, keyof TreeConfig<T>>,
82
70
  );
83
71
 
84
72
  let treeElement: HTMLElement | undefined | null;
@@ -92,16 +80,17 @@ export const createTree = <T>(
92
80
 
93
81
  const hotkeyPresets = {} as HotkeysConfig<T>;
94
82
 
95
- const rebuildItemMeta = (main: FeatureImplementation) => {
83
+ const rebuildItemMeta = () => {
96
84
  // TODO can we find a way to only run this for the changed substructure?
97
85
  itemInstances = [];
98
86
  itemMetaMap = {};
99
87
 
100
- const rootInstance = buildItemInstance(
101
- [main, ...additionalFeatures],
102
- treeInstance,
103
- config.rootItemId,
88
+ const [rootInstance, finalizeRootInstance] = buildInstance(
89
+ features,
90
+ "itemInstance",
91
+ (item) => ({ item, tree: treeInstance, itemId: config.rootItemId }),
104
92
  );
93
+ finalizeRootInstance();
105
94
  itemInstancesMap[config.rootItemId] = rootInstance;
106
95
  itemMetaMap[config.rootItemId] = {
107
96
  itemId: config.rootItemId,
@@ -115,11 +104,16 @@ export const createTree = <T>(
115
104
  for (const item of treeInstance.getItemsMeta()) {
116
105
  itemMetaMap[item.itemId] = item;
117
106
  if (!itemInstancesMap[item.itemId]) {
118
- const instance = buildItemInstance(
119
- [main, ...additionalFeatures],
120
- treeInstance,
121
- item.itemId,
107
+ const [instance, finalizeInstance] = buildInstance(
108
+ features,
109
+ "itemInstance",
110
+ (instance) => ({
111
+ item: instance,
112
+ tree: treeInstance,
113
+ itemId: item.itemId,
114
+ }),
122
115
  );
116
+ finalizeInstance();
123
117
  itemInstancesMap[item.itemId] = instance;
124
118
  itemInstances.push(instance);
125
119
  } else {
@@ -140,34 +134,41 @@ export const createTree = <T>(
140
134
  MainFeatureDef<T>
141
135
  > = {
142
136
  key: "main",
143
- createTreeInstance: (prev) => ({
144
- ...prev,
137
+ treeInstance: {
145
138
  getState: () => state,
146
- setState: (updater) => {
139
+ setState: ({}, updater) => {
147
140
  // Not necessary, since I think the subupdate below keeps the state fresh anyways?
148
141
  // state = typeof updater === "function" ? updater(state) : updater;
149
- config.setState?.(state);
142
+ config.setState?.(state); // TODO this cant be right... This doesnt allow external state updates
150
143
  },
151
- applySubStateUpdate: (stateName, updater) => {
144
+ applySubStateUpdate: <K extends keyof TreeState<any>>(
145
+ {},
146
+ stateName: K,
147
+ updater: Updater<TreeState<T>[K]>,
148
+ ) => {
152
149
  state[stateName] =
153
150
  typeof updater === "function" ? updater(state[stateName]) : updater;
154
- config[stateHandlerNames[stateName]]!(state[stateName]);
151
+ const externalStateSetter = config[
152
+ stateHandlerNames[stateName]
153
+ ] as Function;
154
+ externalStateSetter?.(state[stateName]);
155
155
  },
156
+ // TODO rebuildSubTree: (itemId: string) => void;
156
157
  rebuildTree: () => {
157
- rebuildItemMeta(mainFeature);
158
+ rebuildItemMeta();
158
159
  config.setState?.(state);
159
160
  },
160
161
  getConfig: () => config,
161
- setConfig: (updater) => {
162
+ setConfig: (_, updater) => {
162
163
  config = typeof updater === "function" ? updater(config) : updater;
163
164
 
164
165
  if (config.state) {
165
166
  state = { ...state, ...config.state };
166
167
  }
167
168
  },
168
- getItemInstance: (itemId) => itemInstancesMap[itemId],
169
+ getItemInstance: ({}, itemId) => itemInstancesMap[itemId],
169
170
  getItems: () => itemInstances,
170
- registerElement: (element) => {
171
+ registerElement: ({}, element) => {
171
172
  if (treeElement === element) {
172
173
  return;
173
174
  }
@@ -186,10 +187,10 @@ export const createTree = <T>(
186
187
  getElement: () => treeElement,
187
188
  getDataRef: () => treeDataRef,
188
189
  getHotkeyPresets: () => hotkeyPresets,
189
- }),
190
- createItemInstance: (prev, instance, _, itemId) => ({
191
- ...prev,
192
- registerElement: (element) => {
190
+ },
191
+ itemInstance: {
192
+ // TODO just change to a getRef method that memoizes, maybe as part of getProps
193
+ registerElement: ({ itemId, item }, element) => {
193
194
  if (itemElementsMap[itemId] === element) {
194
195
  return;
195
196
  }
@@ -197,33 +198,30 @@ export const createTree = <T>(
197
198
  const oldElement = itemElementsMap[itemId];
198
199
  if (oldElement && !element) {
199
200
  eachFeature((feature) =>
200
- feature.onItemUnmount?.(instance, oldElement!, treeInstance),
201
+ feature.onItemUnmount?.(item, oldElement!, treeInstance),
201
202
  );
202
203
  } else if (!oldElement && element) {
203
204
  eachFeature((feature) =>
204
- feature.onItemMount?.(instance, element!, treeInstance),
205
+ feature.onItemMount?.(item, element!, treeInstance),
205
206
  );
206
207
  }
207
208
  itemElementsMap[itemId] = element;
208
209
  },
209
- getElement: () => itemElementsMap[itemId],
210
+ getElement: ({ itemId }) => itemElementsMap[itemId],
210
211
  // eslint-disable-next-line no-return-assign
211
- getDataRef: () => (itemDataRefs[itemId] ??= { current: {} }),
212
- getItemMeta: () => itemMetaMap[itemId],
213
- }),
212
+ getDataRef: ({ itemId }) => (itemDataRefs[itemId] ??= { current: {} }),
213
+ getItemMeta: ({ itemId }) => itemMetaMap[itemId],
214
+ },
214
215
  };
215
216
 
216
- const features = [mainFeature, ...additionalFeatures];
217
+ features.unshift(mainFeature);
217
218
 
218
219
  for (const feature of features) {
219
- Object.assign(
220
- treeInstance,
221
- feature.createTreeInstance?.({ ...treeInstance }, treeInstance) ?? {},
222
- );
223
220
  Object.assign(hotkeyPresets, feature.hotkeys ?? {});
224
221
  }
225
222
 
226
- rebuildItemMeta(mainFeature);
223
+ finalizeTree();
224
+ rebuildItemMeta();
227
225
 
228
226
  return treeInstance;
229
227
  };
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { asyncDataLoaderFeature } from "./feature";
4
+
5
+ const tree = TestTree.default({}).withFeatures(asyncDataLoaderFeature);
6
+
7
+ describe("core-feature/selections", () => {
8
+ tree.resetBeforeEach();
9
+
10
+ describe("loading of items", () => {
11
+ it("has initial items", () => {
12
+ tree.expect.hasChildren("x", ["x1", "x2", "x3", "x4"]);
13
+ tree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
14
+ tree.expect.hasChildren("x11", ["x111", "x112", "x113", "x114"]);
15
+ tree.expect.hasChildren("x12", []);
16
+ });
17
+
18
+ it.skip("has loading items after expanding", async () => {
19
+ tree.do.selectItem("x12");
20
+ await TestTree.resolveAsyncLoaders();
21
+ // tree.debug();
22
+ tree.expect.hasChildren("x12", [
23
+ "loading",
24
+ "loading",
25
+ "loading",
26
+ "loading",
27
+ ]);
28
+ });
29
+
30
+ it("has loaded items after expanding and loading", async () => {
31
+ tree.do.selectItem("x12");
32
+ await tree.resolveAsyncVisibleItems();
33
+ tree.expect.hasChildren("x12", ["x121", "x122", "x123", "x124"]);
34
+ tree.expect.hasChildren("x12", ["x121", "x122", "x123", "x124"]);
35
+ });
36
+ });
37
+
38
+ describe("calls handlers", () => {
39
+ it("updates setLoadingItems", async () => {
40
+ const setLoadingItems = tree.mockedHandler("setLoadingItems");
41
+ tree.do.selectItem("x12");
42
+ expect(setLoadingItems).toHaveBeenCalledWith(["x12"]);
43
+ await tree.resolveAsyncVisibleItems();
44
+ expect(setLoadingItems).toHaveBeenCalledWith([]);
45
+ });
46
+
47
+ it("calls onLoadedItem", async () => {
48
+ const onLoadedItem = tree.mockedHandler("onLoadedItem");
49
+ tree.do.selectItem("x12");
50
+ await tree.resolveAsyncVisibleItems();
51
+ expect(onLoadedItem).toHaveBeenCalledWith("x121", "x121");
52
+ });
53
+
54
+ it("calls onLoadedChildren", async () => {
55
+ const onLoadedChildren = tree.mockedHandler("onLoadedChildren");
56
+ tree.do.selectItem("x12");
57
+ await tree.resolveAsyncVisibleItems();
58
+ expect(onLoadedChildren).toHaveBeenCalledWith("x12", [
59
+ "x121",
60
+ "x122",
61
+ "x123",
62
+ "x124",
63
+ ]);
64
+ });
65
+ });
66
+
67
+ describe("data invalidation", () => {
68
+ const getItem = vi.fn(async (id) => id);
69
+ const getChildren = vi.fn(async (id) => [
70
+ `${id}1`,
71
+ `${id}2`,
72
+ `${id}3`,
73
+ `${id}4`,
74
+ ]);
75
+ const suiteTree = tree.with({ asyncDataLoader: { getItem, getChildren } });
76
+ suiteTree.resetBeforeEach();
77
+
78
+ it("invalidates item data on tree instance", async () => {
79
+ getItem.mockClear();
80
+ getItem.mockResolvedValueOnce("new");
81
+ suiteTree.instance.invalidateItemData("x1");
82
+ await suiteTree.resolveAsyncVisibleItems();
83
+ expect(getItem).toHaveBeenCalledWith("x1");
84
+ expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe(
85
+ "new",
86
+ );
87
+ });
88
+
89
+ it("invalidates item data on item instance", async () => {
90
+ getItem.mockClear();
91
+ await suiteTree.resolveAsyncVisibleItems();
92
+ getItem.mockResolvedValueOnce("new");
93
+ suiteTree.instance.getItemInstance("x1").invalidateItemData();
94
+ await suiteTree.resolveAsyncVisibleItems();
95
+ expect(getItem).toHaveBeenCalledWith("x1");
96
+ expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe(
97
+ "new",
98
+ );
99
+ });
100
+
101
+ it("invalidates children ids on tree instance", async () => {
102
+ getChildren.mockClear();
103
+ await suiteTree.resolveAsyncVisibleItems();
104
+ getChildren.mockResolvedValueOnce(["new1", "new2"]);
105
+ suiteTree.instance.invalidateChildrenIds("x1");
106
+ await suiteTree.resolveAsyncVisibleItems();
107
+ expect(getChildren).toHaveBeenCalledWith("x1");
108
+ suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
109
+ });
110
+
111
+ it("invalidates children ids on item instance", async () => {
112
+ getChildren.mockClear();
113
+ await suiteTree.resolveAsyncVisibleItems();
114
+ getChildren.mockResolvedValueOnce(["new1", "new2"]);
115
+ suiteTree.instance.getItemInstance("x1").invalidateChildrenIds();
116
+ await suiteTree.resolveAsyncVisibleItems();
117
+ expect(getChildren).toHaveBeenCalledWith("x1");
118
+ suiteTree.expect.hasChildren("x1", ["new1", "new2"]);
119
+ });
120
+
121
+ it("doesnt call item data getter twice", async () => {
122
+ await suiteTree.resolveAsyncVisibleItems();
123
+ getItem.mockClear();
124
+ suiteTree.instance.invalidateItemData("x1");
125
+ await suiteTree.resolveAsyncVisibleItems();
126
+ expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe("x1");
127
+ expect(suiteTree.instance.getItemInstance("x1").getItemData()).toBe("x1");
128
+ expect(getItem).toHaveBeenCalledTimes(1);
129
+ });
130
+
131
+ it("doesnt call children getter twice", async () => {
132
+ await suiteTree.resolveAsyncVisibleItems();
133
+ getChildren.mockClear();
134
+ suiteTree.instance.invalidateChildrenIds("x1");
135
+ await suiteTree.resolveAsyncVisibleItems();
136
+ suiteTree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
137
+ suiteTree.expect.hasChildren("x1", ["x11", "x12", "x13", "x14"]);
138
+ expect(getChildren).toHaveBeenCalledTimes(1);
139
+ });
140
+ });
141
+
142
+ describe.todo("getChildrenWithData");
143
+ });
@@ -25,12 +25,10 @@ export const asyncDataLoaderFeature: FeatureImplementation<
25
25
  loadingItems: "setLoadingItems",
26
26
  },
27
27
 
28
- createTreeInstance: (prev, instance) => ({
29
- ...prev,
30
-
31
- retrieveItemData: (itemId) => {
32
- const config = instance.getConfig();
33
- const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
28
+ treeInstance: {
29
+ retrieveItemData: ({ tree }, itemId) => {
30
+ const config = tree.getConfig();
31
+ const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
34
32
  dataRef.current.itemData ??= {};
35
33
  dataRef.current.childrenIds ??= {};
36
34
 
@@ -38,15 +36,15 @@ export const asyncDataLoaderFeature: FeatureImplementation<
38
36
  return dataRef.current.itemData[itemId];
39
37
  }
40
38
 
41
- if (!instance.getState().loadingItems.includes(itemId)) {
42
- instance.applySubStateUpdate("loadingItems", (loadingItems) => [
39
+ if (!tree.getState().loadingItems.includes(itemId)) {
40
+ tree.applySubStateUpdate("loadingItems", (loadingItems) => [
43
41
  ...loadingItems,
44
42
  itemId,
45
43
  ]);
46
44
  config.asyncDataLoader?.getItem(itemId).then((item) => {
47
45
  dataRef.current.itemData[itemId] = item;
48
46
  config.onLoadedItem?.(itemId, item);
49
- instance.applySubStateUpdate("loadingItems", (loadingItems) =>
47
+ tree.applySubStateUpdate("loadingItems", (loadingItems) =>
50
48
  loadingItems.filter((id) => id !== itemId),
51
49
  );
52
50
  });
@@ -55,20 +53,20 @@ export const asyncDataLoaderFeature: FeatureImplementation<
55
53
  return config.createLoadingItemData?.() ?? null;
56
54
  },
57
55
 
58
- retrieveChildrenIds: (itemId) => {
59
- const config = instance.getConfig();
60
- const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
56
+ retrieveChildrenIds: ({ tree }, itemId) => {
57
+ const config = tree.getConfig();
58
+ const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
61
59
  dataRef.current.itemData ??= {};
62
60
  dataRef.current.childrenIds ??= {};
63
61
  if (dataRef.current.childrenIds[itemId]) {
64
62
  return dataRef.current.childrenIds[itemId];
65
63
  }
66
64
 
67
- if (instance.getState().loadingItems.includes(itemId)) {
65
+ if (tree.getState().loadingItems.includes(itemId)) {
68
66
  return [];
69
67
  }
70
68
 
71
- instance.applySubStateUpdate("loadingItems", (loadingItems) => [
69
+ tree.applySubStateUpdate("loadingItems", (loadingItems) => [
72
70
  ...loadingItems,
73
71
  itemId,
74
72
  ]);
@@ -82,45 +80,49 @@ export const asyncDataLoaderFeature: FeatureImplementation<
82
80
  const childrenIds = children.map(({ id }) => id);
83
81
  dataRef.current.childrenIds[itemId] = childrenIds;
84
82
  config.onLoadedChildren?.(itemId, childrenIds);
85
- instance.applySubStateUpdate("loadingItems", (loadingItems) =>
83
+ tree.applySubStateUpdate("loadingItems", (loadingItems) =>
86
84
  loadingItems.filter((id) => id !== itemId),
87
85
  );
88
- instance.rebuildTree();
86
+ tree.rebuildTree();
89
87
  });
90
88
  } else {
91
89
  config.asyncDataLoader?.getChildren(itemId).then((childrenIds) => {
92
90
  dataRef.current.childrenIds[itemId] = childrenIds;
93
91
  config.onLoadedChildren?.(itemId, childrenIds);
94
- instance.applySubStateUpdate("loadingItems", (loadingItems) =>
92
+ tree.applySubStateUpdate("loadingItems", (loadingItems) =>
95
93
  loadingItems.filter((id) => id !== itemId),
96
94
  );
97
- instance.rebuildTree();
95
+ tree.rebuildTree();
98
96
  });
99
97
  }
100
98
 
101
99
  return [];
102
100
  },
103
101
 
104
- invalidateItemData: (itemId) => {
105
- const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
102
+ invalidateItemData: ({ tree }, itemId) => {
103
+ const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
106
104
  delete dataRef.current.itemData?.[itemId];
107
- instance.retrieveItemData(itemId);
105
+ tree.retrieveItemData(itemId);
108
106
  },
109
107
 
110
- invalidateChildrenIds: (itemId) => {
111
- const dataRef = instance.getDataRef<AsyncDataLoaderRef>();
108
+ invalidateChildrenIds: ({ tree }, itemId) => {
109
+ const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
112
110
  delete dataRef.current.childrenIds?.[itemId];
113
- instance.retrieveChildrenIds(itemId);
111
+ tree.retrieveChildrenIds(itemId);
114
112
  },
115
- }),
113
+ },
116
114
 
117
- createItemInstance: (prev, item, tree) => ({
118
- ...prev,
119
- isLoading: () =>
115
+ itemInstance: {
116
+ isLoading: ({ tree, item }) =>
120
117
  tree.getState().loadingItems.includes(item.getItemMeta().itemId),
121
- invalidateItemData: () =>
118
+ invalidateItemData: ({ tree, item }) =>
122
119
  tree.invalidateItemData(item.getItemMeta().itemId),
123
- invalidateChildrenIds: () =>
120
+ invalidateChildrenIds: ({ tree, item }) =>
124
121
  tree.invalidateChildrenIds(item.getItemMeta().itemId),
125
- }),
122
+ updateCachedChildrenIds: ({ tree, itemId }, childrenIds) => {
123
+ const dataRef = tree.getDataRef<AsyncDataLoaderRef>();
124
+ dataRef.current.childrenIds[itemId] = childrenIds;
125
+ tree.rebuildTree();
126
+ },
127
+ },
126
128
  };
@@ -31,11 +31,13 @@ export type AsyncDataLoaderFeatureDef<T> = {
31
31
  /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible */
32
32
  invalidateItemData: (itemId: string) => void;
33
33
  invalidateChildrenIds: (itemId: string) => void;
34
+ // TODO deprecate tree instance methods, move to item instance
34
35
  };
35
36
  itemInstance: SyncDataLoaderFeatureDef<T>["itemInstance"] & {
36
37
  invalidateItemData: () => void;
37
38
  invalidateChildrenIds: () => void;
38
- isLoading: () => void; // TODO! boolean?
39
+ updateCachedChildrenIds: (childrenIds: string[]) => void;
40
+ isLoading: () => boolean;
39
41
  };
40
42
  hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
41
43
  };