@headless-tree/core 1.4.0 → 1.5.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.
package/dist/index.mjs CHANGED
@@ -441,6 +441,7 @@ var createTree = (initialConfig) => {
441
441
  );
442
442
  let treeElement;
443
443
  const treeDataRef = { current: {} };
444
+ let rebuildScheduled = false;
444
445
  const itemInstancesMap = {};
445
446
  let itemInstances = [];
446
447
  const itemElementsMap = {};
@@ -484,6 +485,7 @@ var createTree = (initialConfig) => {
484
485
  itemInstances.push(itemInstancesMap[item.itemId]);
485
486
  }
486
487
  }
488
+ rebuildScheduled = false;
487
489
  };
488
490
  const eachFeature = (fn) => {
489
491
  for (const feature of additionalFeatures) {
@@ -498,16 +500,48 @@ var createTree = (initialConfig) => {
498
500
  var _a2;
499
501
  (_a2 = config.setState) == null ? void 0 : _a2.call(config, state);
500
502
  },
503
+ setMounted: ({}, isMounted) => {
504
+ var _a2;
505
+ const ref = treeDataRef.current;
506
+ ref.isMounted = isMounted;
507
+ if (isMounted) {
508
+ (_a2 = ref.waitingForMount) == null ? void 0 : _a2.forEach((cb) => cb());
509
+ ref.waitingForMount = [];
510
+ }
511
+ },
501
512
  applySubStateUpdate: ({}, stateName, updater) => {
502
- state[stateName] = typeof updater === "function" ? updater(state[stateName]) : updater;
503
- const externalStateSetter = config[stateHandlerNames[stateName]];
504
- externalStateSetter == null ? void 0 : externalStateSetter(state[stateName]);
513
+ var _a2;
514
+ const apply = () => {
515
+ state[stateName] = typeof updater === "function" ? updater(state[stateName]) : updater;
516
+ const externalStateSetter = config[stateHandlerNames[stateName]];
517
+ externalStateSetter == null ? void 0 : externalStateSetter(state[stateName]);
518
+ };
519
+ const ref = treeDataRef.current;
520
+ if (ref.isMounted) {
521
+ apply();
522
+ } else {
523
+ (_a2 = ref.waitingForMount) != null ? _a2 : ref.waitingForMount = [];
524
+ ref.waitingForMount.push(apply);
525
+ }
505
526
  },
506
527
  // TODO rebuildSubTree: (itemId: string) => void;
507
528
  rebuildTree: () => {
508
- var _a2;
509
- rebuildItemMeta();
510
- (_a2 = config.setState) == null ? void 0 : _a2.call(config, state);
529
+ var _a2, _b2;
530
+ const ref = treeDataRef.current;
531
+ if (ref.isMounted) {
532
+ rebuildItemMeta();
533
+ (_a2 = config.setState) == null ? void 0 : _a2.call(config, state);
534
+ } else {
535
+ (_b2 = ref.waitingForMount) != null ? _b2 : ref.waitingForMount = [];
536
+ ref.waitingForMount.push(() => {
537
+ var _a3;
538
+ rebuildItemMeta();
539
+ (_a3 = config.setState) == null ? void 0 : _a3.call(config, state);
540
+ });
541
+ }
542
+ },
543
+ scheduleRebuildTree: () => {
544
+ rebuildScheduled = true;
511
545
  },
512
546
  getConfig: () => config,
513
547
  setConfig: (_, updater) => {
@@ -540,7 +574,10 @@ var createTree = (initialConfig) => {
540
574
  }
541
575
  return existingInstance;
542
576
  },
543
- getItems: () => itemInstances,
577
+ getItems: () => {
578
+ if (rebuildScheduled) rebuildItemMeta();
579
+ return itemInstances;
580
+ },
544
581
  registerElement: ({}, element) => {
545
582
  if (treeElement === element) {
546
583
  return;
@@ -773,33 +810,60 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
773
810
  if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
774
811
  return [itemId];
775
812
  }
776
- const descendants = tree.retrieveChildrenIds(itemId).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
813
+ const descendants = tree.retrieveChildrenIds(itemId, true).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
777
814
  return includeFolders ? [itemId, ...descendants] : descendants;
778
815
  };
816
+ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
817
+ yield tree.loadItemData(itemId);
818
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
819
+ return [itemId];
820
+ }
821
+ const childrenIds = yield tree.loadChildrenIds(itemId);
822
+ const descendants = (yield Promise.all(
823
+ childrenIds.map(
824
+ (child) => getAllDescendants(tree, child, includeFolders)
825
+ )
826
+ )).flat();
827
+ return includeFolders ? [itemId, ...descendants] : descendants;
828
+ });
829
+ var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
830
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
831
+ ...items,
832
+ itemId
833
+ ]);
834
+ try {
835
+ yield callback();
836
+ } finally {
837
+ tree.applySubStateUpdate(
838
+ "loadingCheckPropagationItems",
839
+ (items) => items.filter((id) => id !== itemId)
840
+ );
841
+ }
842
+ });
779
843
  var checkboxesFeature = {
780
844
  key: "checkboxes",
781
845
  overwrites: ["selection"],
782
846
  getInitialState: (initialState) => __spreadValues({
783
- checkedItems: []
847
+ checkedItems: [],
848
+ loadingCheckPropagationItems: []
784
849
  }, initialState),
785
850
  getDefaultConfig: (defaultConfig, tree) => {
786
- var _a, _b, _c;
787
- const hasAsyncLoader = (_a = defaultConfig.features) == null ? void 0 : _a.some(
788
- (f) => f.key === "async-data-loader"
789
- );
790
- if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
791
- throwError(`propagateCheckedState not supported with async trees`);
792
- }
793
- const propagateCheckedState = (_b = defaultConfig.propagateCheckedState) != null ? _b : !hasAsyncLoader;
794
- const canCheckFolders = (_c = defaultConfig.canCheckFolders) != null ? _c : !propagateCheckedState;
851
+ var _a, _b;
852
+ const propagateCheckedState = (_a = defaultConfig.propagateCheckedState) != null ? _a : true;
853
+ const canCheckFolders = (_b = defaultConfig.canCheckFolders) != null ? _b : !propagateCheckedState;
795
854
  return __spreadValues({
796
855
  setCheckedItems: makeStateUpdater("checkedItems", tree),
856
+ setLoadingCheckPropagationItems: makeStateUpdater(
857
+ "loadingCheckPropagationItems",
858
+ tree
859
+ ),
797
860
  propagateCheckedState,
798
861
  canCheckFolders
799
862
  }, defaultConfig);
800
863
  },
801
864
  stateHandlerNames: {
802
- checkedItems: "setCheckedItems"
865
+ checkedItems: "setCheckedItems",
866
+ loadingCheckPropagationItems: "setLoadingCheckPropagationItems"
803
867
  },
804
868
  treeInstance: {
805
869
  setCheckedItems: ({ tree }, checkedItems) => {
@@ -819,13 +883,13 @@ var checkboxesFeature = {
819
883
  }
820
884
  };
821
885
  },
822
- toggleCheckedState: ({ item }) => {
886
+ toggleCheckedState: (_0) => __async(null, [_0], function* ({ item }) {
823
887
  if (item.getCheckedState() === "checked" /* Checked */) {
824
- item.setUnchecked();
888
+ yield item.setUnchecked();
825
889
  } else {
826
- item.setChecked();
890
+ yield item.setChecked();
827
891
  }
828
- },
892
+ }),
829
893
  getCheckedState: ({ item, tree }) => {
830
894
  const { checkedItems } = tree.getState();
831
895
  const { propagateCheckedState } = tree.getConfig();
@@ -835,6 +899,7 @@ var checkboxesFeature = {
835
899
  }
836
900
  if (item.isFolder() && propagateCheckedState) {
837
901
  const descendants = getAllLoadedDescendants(tree, itemId);
902
+ if (descendants.length === 0) return "unchecked" /* Unchecked */;
838
903
  if (descendants.every((d) => checkedItems.includes(d))) {
839
904
  return "checked" /* Checked */;
840
905
  }
@@ -844,36 +909,48 @@ var checkboxesFeature = {
844
909
  }
845
910
  return "unchecked" /* Unchecked */;
846
911
  },
847
- setChecked: ({ item, tree, itemId }) => {
848
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
849
- if (item.isFolder() && propagateCheckedState) {
850
- tree.applySubStateUpdate("checkedItems", (items) => [
851
- ...items,
852
- ...getAllLoadedDescendants(tree, itemId, canCheckFolders)
853
- ]);
854
- } else if (!item.isFolder() || canCheckFolders) {
855
- tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
856
- }
857
- },
858
- setUnchecked: ({ item, tree, itemId }) => {
859
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
860
- if (item.isFolder() && propagateCheckedState) {
861
- const descendants = getAllLoadedDescendants(
862
- tree,
863
- itemId,
864
- canCheckFolders
865
- );
866
- tree.applySubStateUpdate(
867
- "checkedItems",
868
- (items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
869
- );
870
- } else {
871
- tree.applySubStateUpdate(
872
- "checkedItems",
873
- (items) => items.filter((id) => id !== itemId)
874
- );
875
- }
876
- }
912
+ setChecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
913
+ yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
914
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
915
+ if (item.isFolder() && propagateCheckedState) {
916
+ const descendants = yield getAllDescendants(
917
+ tree,
918
+ itemId,
919
+ canCheckFolders
920
+ );
921
+ tree.applySubStateUpdate("checkedItems", (items) => [
922
+ ...items,
923
+ ...descendants
924
+ ]);
925
+ } else if (!item.isFolder() || canCheckFolders) {
926
+ tree.applySubStateUpdate("checkedItems", (items) => [
927
+ ...items,
928
+ itemId
929
+ ]);
930
+ }
931
+ }));
932
+ }),
933
+ setUnchecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
934
+ yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
935
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
936
+ if (item.isFolder() && propagateCheckedState) {
937
+ const descendants = yield getAllDescendants(
938
+ tree,
939
+ itemId,
940
+ canCheckFolders
941
+ );
942
+ tree.applySubStateUpdate(
943
+ "checkedItems",
944
+ (items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
945
+ );
946
+ } else {
947
+ tree.applySubStateUpdate(
948
+ "checkedItems",
949
+ (items) => items.filter((id) => id !== itemId)
950
+ );
951
+ }
952
+ }));
953
+ })
877
954
  }
878
955
  };
879
956
 
@@ -885,7 +962,8 @@ var specialKeys = {
885
962
  plus: /^(NumpadAdd|Plus)$/,
886
963
  minus: /^(NumpadSubtract|Minus)$/,
887
964
  control: /^(ControlLeft|ControlRight)$/,
888
- shift: /^(ShiftLeft|ShiftRight)$/
965
+ shift: /^(ShiftLeft|ShiftRight)$/,
966
+ metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/
889
967
  };
890
968
  var testHotkeyMatch = (pressedKeys, tree, hotkey) => {
891
969
  const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
@@ -990,6 +1068,12 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
990
1068
  var _a;
991
1069
  const config = tree.getConfig();
992
1070
  const dataRef = getDataRef(tree);
1071
+ if (!dataRef.current.itemData[itemId]) {
1072
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1073
+ ...loadingItemData,
1074
+ itemId
1075
+ ]);
1076
+ }
993
1077
  const item = yield config.dataLoader.getItem(itemId);
994
1078
  dataRef.current.itemData[itemId] = item;
995
1079
  (_a = config.onLoadedItem) == null ? void 0 : _a.call(config, itemId, item);
@@ -1004,6 +1088,12 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
1004
1088
  const config = tree.getConfig();
1005
1089
  const dataRef = getDataRef(tree);
1006
1090
  let childrenIds;
1091
+ if (!dataRef.current.childrenIds[itemId]) {
1092
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
1093
+ ...loadingItemChildrens,
1094
+ itemId
1095
+ ]);
1096
+ }
1007
1097
  if ("getChildrenWithData" in config.dataLoader) {
1008
1098
  const children = yield config.dataLoader.getChildrenWithData(itemId);
1009
1099
  childrenIds = children.map((c) => c.id);
@@ -1064,11 +1154,7 @@ var asyncDataLoaderFeature = {
1064
1154
  return dataRef.current.itemData[itemId];
1065
1155
  }
1066
1156
  if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
1067
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1068
- ...loadingItemData,
1069
- itemId
1070
- ]);
1071
- loadItemData(tree, itemId);
1157
+ setTimeout(() => loadItemData(tree, itemId));
1072
1158
  }
1073
1159
  return (_b = (_a = config.createLoadingItemData) == null ? void 0 : _a.call(config)) != null ? _b : null;
1074
1160
  },
@@ -1080,11 +1166,7 @@ var asyncDataLoaderFeature = {
1080
1166
  if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
1081
1167
  return [];
1082
1168
  }
1083
- tree.applySubStateUpdate(
1084
- "loadingItemChildrens",
1085
- (loadingItemChildrens) => [...loadingItemChildrens, itemId]
1086
- );
1087
- loadChildrenIds(tree, itemId);
1169
+ setTimeout(() => loadChildrenIds(tree, itemId));
1088
1170
  return [];
1089
1171
  }
1090
1172
  },
@@ -1094,10 +1176,6 @@ var asyncDataLoaderFeature = {
1094
1176
  var _a;
1095
1177
  if (!optimistic) {
1096
1178
  (_a = getDataRef(tree).current.itemData) == null ? true : delete _a[itemId];
1097
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
1098
- ...loadingItemData,
1099
- itemId
1100
- ]);
1101
1179
  }
1102
1180
  yield loadItemData(tree, itemId);
1103
1181
  }),
@@ -1105,10 +1183,6 @@ var asyncDataLoaderFeature = {
1105
1183
  var _a;
1106
1184
  if (!optimistic) {
1107
1185
  (_a = getDataRef(tree).current.childrenIds) == null ? true : delete _a[itemId];
1108
- tree.applySubStateUpdate(
1109
- "loadingItemChildrens",
1110
- (loadingItemChildrens) => [...loadingItemChildrens, itemId]
1111
- );
1112
1186
  }
1113
1187
  yield loadChildrenIds(tree, itemId);
1114
1188
  }),
@@ -1560,6 +1634,10 @@ var dragAndDropFeature = {
1560
1634
  const target = tree.getDragTarget();
1561
1635
  return target ? target.item.getId() === item.getId() : false;
1562
1636
  },
1637
+ isUnorderedDragTarget: ({ tree, item }) => {
1638
+ const target = tree.getDragTarget();
1639
+ return target ? !isOrderedDragTarget(target) && target.item.getId() === item.getId() : false;
1640
+ },
1563
1641
  isDragTargetAbove: ({ tree, item }) => {
1564
1642
  const target = tree.getDragTarget();
1565
1643
  if (!target || !isOrderedDragTarget(target) || target.item !== item.getParent())
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "1.4.0",
3
+ "description": "The definitive tree component for the Web",
4
+ "keywords": [
5
+ "tree",
6
+ "component",
7
+ "web",
8
+ "headless",
9
+ "ui",
10
+ "react",
11
+ "nested",
12
+ "async",
13
+ "checkbox",
14
+ "hook"
15
+ ],
16
+ "version": "1.5.0",
4
17
  "main": "dist/index.d.ts",
5
18
  "module": "dist/index.mjs",
6
19
  "types": "dist/index.d.mts",
@@ -11,13 +24,16 @@
11
24
  "default": "./dist/index.mjs"
12
25
  },
13
26
  "require": {
14
- "types": "./dist/index.js",
15
- "default": "./dist/index.d.ts"
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
16
29
  }
17
30
  },
18
31
  "./package.json": "./package.json"
19
32
  },
20
33
  "sideEffects": false,
34
+ "publishConfig": {
35
+ "provenance": true
36
+ },
21
37
  "scripts": {
22
38
  "build": "tsup ./src/index.ts --format esm,cjs --dts",
23
39
  "start": "tsup ./src/index.ts --format esm,cjs --dts --watch",
package/readme.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  [![Documentation](https://img.shields.io/badge/docs-1e1f22?style=flat)](https://headless-tree.lukasbach.com/)
4
4
  [![Chat on Discord](https://img.shields.io/badge/discord-4c57d9?style=flat&logo=discord&logoColor=ffffff)](https://discord.gg/KuZ6EezzVw)
5
- [![Follow on BLuesky](https://img.shields.io/badge/bluesky-0285FF?style=flat&logo=bluesky&logoColor=ffffff)](https://bsky.app/profile/lukasbach.bsky.social)
5
+ [![Follow on Bluesky](https://img.shields.io/badge/bluesky-0285FF?style=flat&logo=bluesky&logoColor=ffffff)](https://bsky.app/profile/lukasbach.bsky.social)
6
+ [![Follow on X](https://img.shields.io/badge/x-000000?style=flat&logo=x&logoColor=ffffff)](https://x.com/lukasmbach)
6
7
  [![Support on Github Sponsors](https://img.shields.io/badge/sponsor-EA4AAA?style=flat&logo=githubsponsors&logoColor=ffffff)](https://github.com/sponsors/lukasbach)
7
8
  [![Follow on Github](https://img.shields.io/badge/follow-181717?style=flat&logo=github&logoColor=ffffff)](https://github.com/lukasbach)
8
9
  [![NPM Core package](https://img.shields.io/badge/core-CB3837?style=flat&logo=npm&logoColor=ffffff)](https://www.npmjs.com/package/@headless-tree/core)
9
10
  [![NPM React package](https://img.shields.io/badge/react-CB3837?style=flat&logo=npm&logoColor=ffffff)](https://www.npmjs.com/package/@headless-tree/react)
10
11
 
11
-
12
- Super-easy integration of complex tree components into React. Supports ordered
12
+ Super-easy integration of complex tree components into React. Supports ordered
13
13
  and unordered drag-and-drop, extensive keybindings, search, renaming and more.
14
14
  Fully customizable and accessible. Headless Tree is the official successor for
15
15
  [react-complex-tree](https://github.com/lukasbach/react-complex-tree).
@@ -18,7 +18,7 @@ It aims to bring the many features of complex tree views, like multi-select,
18
18
  drag-and-drop, keyboard navigation, tree search, renaming and more, while
19
19
  being unopinionated about the styling and rendering of the tree itself.
20
20
  Accessibility is ensured by default, and the integration is extremely
21
- simple and flexible.
21
+ simple and flexible.
22
22
 
23
23
  The interface gives you a flat list of tree nodes
24
24
  that you can easily render yourself, which keeps the complexity of the
@@ -39,7 +39,7 @@ to get an idea of what you can do with it.
39
39
  > I have collected feedback and fixed any bugs that might arise. I've written
40
40
  > [a blog post](https://medium.com/@lukasbach/headless-tree-and-the-future-of-react-complex-tree-fc920700e82a)
41
41
  > about the details of the change, and the future of the library.
42
- >
42
+ >
43
43
  > Join
44
44
  > [the Discord](https://discord.gg/KuZ6EezzVw) to get involved, and
45
45
  > [follow on Bluesky](https://bsky.app/profile/lukasbach.bsky.social) to
@@ -154,4 +154,4 @@ Then, render your tree based on the tree instance returned from the hook:
154
154
  ```
155
155
 
156
156
  Read on in the [get started guide](https://headless-tree.lukasbach.com/getstarted) to learn more about
157
- how to use Headless Tree, and how to customize it to your needs.
157
+ how to use Headless Tree, and how to customize it to your needs.
@@ -11,6 +11,7 @@ import { treeFeature } from "../features/tree/feature";
11
11
  import { ItemMeta } from "../features/tree/types";
12
12
  import { buildStaticInstance } from "./build-static-instance";
13
13
  import { throwError } from "../utilities/errors";
14
+ import type { TreeDataRef } from "../features/main/types";
14
15
 
15
16
  const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
16
17
  const loadedFeatures = features?.map((feature) => feature.key);
@@ -92,6 +93,7 @@ export const createTree = <T>(
92
93
  let treeElement: HTMLElement | undefined | null;
93
94
  const treeDataRef: { current: any } = { current: {} };
94
95
 
96
+ let rebuildScheduled = false;
95
97
  const itemInstancesMap: Record<string, ItemInstance<T>> = {};
96
98
  let itemInstances: ItemInstance<T>[] = [];
97
99
  const itemElementsMap: Record<string, HTMLElement | undefined | null> = {};
@@ -140,6 +142,8 @@ export const createTree = <T>(
140
142
  itemInstances.push(itemInstancesMap[item.itemId]);
141
143
  }
142
144
  }
145
+
146
+ rebuildScheduled = false;
143
147
  };
144
148
 
145
149
  const eachFeature = (fn: (feature: FeatureImplementation<any>) => void) => {
@@ -158,22 +162,51 @@ export const createTree = <T>(
158
162
  config.setState?.(state); // TODO this cant be right... This doesnt allow external state updates
159
163
  // TODO this is never used, remove
160
164
  },
165
+ setMounted: ({}, isMounted) => {
166
+ const ref = treeDataRef.current as TreeDataRef;
167
+ ref.isMounted = isMounted;
168
+ if (isMounted) {
169
+ ref.waitingForMount?.forEach((cb) => cb());
170
+ ref.waitingForMount = [];
171
+ }
172
+ },
161
173
  applySubStateUpdate: <K extends keyof TreeState<any>>(
162
174
  {},
163
175
  stateName: K,
164
176
  updater: Updater<TreeState<T>[K]>,
165
177
  ) => {
166
- state[stateName] =
167
- typeof updater === "function" ? updater(state[stateName]) : updater;
168
- const externalStateSetter = config[
169
- stateHandlerNames[stateName]
170
- ] as Function;
171
- externalStateSetter?.(state[stateName]);
178
+ const apply = () => {
179
+ state[stateName] =
180
+ typeof updater === "function" ? updater(state[stateName]) : updater;
181
+ const externalStateSetter = config[
182
+ stateHandlerNames[stateName]
183
+ ] as Function;
184
+ externalStateSetter?.(state[stateName]);
185
+ };
186
+ const ref = treeDataRef.current as TreeDataRef;
187
+ if (ref.isMounted) {
188
+ apply();
189
+ } else {
190
+ ref.waitingForMount ??= [];
191
+ ref.waitingForMount.push(apply);
192
+ }
172
193
  },
173
194
  // TODO rebuildSubTree: (itemId: string) => void;
174
195
  rebuildTree: () => {
175
- rebuildItemMeta();
176
- config.setState?.(state);
196
+ const ref = treeDataRef.current as TreeDataRef;
197
+ if (ref.isMounted) {
198
+ rebuildItemMeta();
199
+ config.setState?.(state);
200
+ } else {
201
+ ref.waitingForMount ??= [];
202
+ ref.waitingForMount.push(() => {
203
+ rebuildItemMeta();
204
+ config.setState?.(state);
205
+ });
206
+ }
207
+ },
208
+ scheduleRebuildTree: () => {
209
+ rebuildScheduled = true;
177
210
  },
178
211
  getConfig: () => config,
179
212
  setConfig: (_, updater) => {
@@ -210,7 +243,10 @@ export const createTree = <T>(
210
243
  }
211
244
  return existingInstance;
212
245
  },
213
- getItems: () => itemInstances,
246
+ getItems: () => {
247
+ if (rebuildScheduled) rebuildItemMeta();
248
+ return itemInstances;
249
+ },
214
250
  registerElement: ({}, element) => {
215
251
  if (treeElement === element) {
216
252
  return;
@@ -46,6 +46,7 @@ describe("core-feature/selections", () => {
46
46
  );
47
47
  const setLoadingItemData = tree.mockedHandler("setLoadingItemData");
48
48
  tree.do.selectItem("x12");
49
+ await tree.do.awaitNextTick();
49
50
  expect(setLoadingItemChildrens).toHaveBeenCalledWith(["x12"]);
50
51
  expect(setLoadingItemData).not.toHaveBeenCalled();
51
52
  await tree.resolveAsyncVisibleItems();
@@ -13,6 +13,13 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
13
13
  const config = tree.getConfig();
14
14
  const dataRef = getDataRef(tree);
15
15
 
16
+ if (!dataRef.current.itemData[itemId]) {
17
+ tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
18
+ ...loadingItemData,
19
+ itemId,
20
+ ]);
21
+ }
22
+
16
23
  const item = await config.dataLoader.getItem(itemId);
17
24
  dataRef.current.itemData[itemId] = item;
18
25
  config.onLoadedItem?.(itemId, item);
@@ -28,6 +35,13 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
28
35
  const dataRef = getDataRef(tree);
29
36
  let childrenIds: string[];
30
37
 
38
+ if (!dataRef.current.childrenIds[itemId]) {
39
+ tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
40
+ ...loadingItemChildrens,
41
+ itemId,
42
+ ]);
43
+ }
44
+
31
45
  if ("getChildrenWithData" in config.dataLoader) {
32
46
  const children = await config.dataLoader.getChildrenWithData(itemId);
33
47
  childrenIds = children.map((c) => c.id);
@@ -104,12 +118,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
104
118
  }
105
119
 
106
120
  if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
107
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
108
- ...loadingItemData,
109
- itemId,
110
- ]);
111
-
112
- loadItemData(tree, itemId);
121
+ setTimeout(() => loadItemData(tree, itemId));
113
122
  }
114
123
 
115
124
  return config.createLoadingItemData?.() ?? null;
@@ -125,12 +134,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
125
134
  return [];
126
135
  }
127
136
 
128
- tree.applySubStateUpdate(
129
- "loadingItemChildrens",
130
- (loadingItemChildrens) => [...loadingItemChildrens, itemId],
131
- );
132
-
133
- loadChildrenIds(tree, itemId);
137
+ setTimeout(() => loadChildrenIds(tree, itemId));
134
138
 
135
139
  return [];
136
140
  },
@@ -143,20 +147,12 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
143
147
  invalidateItemData: async ({ tree, itemId }, optimistic) => {
144
148
  if (!optimistic) {
145
149
  delete getDataRef(tree).current.itemData?.[itemId];
146
- tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
147
- ...loadingItemData,
148
- itemId,
149
- ]);
150
150
  }
151
151
  await loadItemData(tree, itemId);
152
152
  },
153
153
  invalidateChildrenIds: async ({ tree, itemId }, optimistic) => {
154
154
  if (!optimistic) {
155
155
  delete getDataRef(tree).current.childrenIds?.[itemId];
156
- tree.applySubStateUpdate(
157
- "loadingItemChildrens",
158
- (loadingItemChildrens) => [...loadingItemChildrens, itemId],
159
- );
160
156
  }
161
157
  await loadChildrenIds(tree, itemId);
162
158
  },
@@ -47,7 +47,8 @@ export type AsyncDataLoaderFeatureDef<T> = {
47
47
  * the tree will continue to display the old data until the new data has loaded. */
48
48
  invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
49
49
 
50
- updateCachedData: (data: T) => void;
50
+ /** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
51
+ updateCachedData: (data: T | undefined) => void;
51
52
  updateCachedChildrenIds: (childrenIds: string[]) => void;
52
53
  isLoading: () => boolean;
53
54
  };