@keenmate/svelte-treeview 4.8.0 → 5.0.0-rc02
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/README.md +106 -117
- package/ai/INDEX.txt +310 -0
- package/ai/advanced-patterns.txt +506 -0
- package/ai/basic-setup.txt +336 -0
- package/ai/context-menu.txt +349 -0
- package/ai/data-handling.txt +390 -0
- package/ai/drag-drop.txt +397 -0
- package/ai/events-callbacks.txt +382 -0
- package/ai/import-patterns.txt +271 -0
- package/ai/performance.txt +349 -0
- package/ai/search-features.txt +359 -0
- package/ai/styling-theming.txt +354 -0
- package/ai/tree-editing.txt +423 -0
- package/ai/typescript-types.txt +357 -0
- package/dist/components/Node.svelte +47 -40
- package/dist/components/Node.svelte.d.ts +1 -1
- package/dist/components/Tree.svelte +384 -1479
- package/dist/components/Tree.svelte.d.ts +30 -28
- package/dist/components/TreeProvider.svelte +28 -0
- package/dist/components/TreeProvider.svelte.d.ts +28 -0
- package/dist/constants.generated.d.ts +1 -1
- package/dist/constants.generated.js +1 -1
- package/dist/core/TreeController.svelte.d.ts +353 -0
- package/dist/core/TreeController.svelte.js +1503 -0
- package/dist/core/createTreeController.d.ts +9 -0
- package/dist/core/createTreeController.js +11 -0
- package/dist/global-api.d.ts +1 -1
- package/dist/global-api.js +5 -5
- package/dist/index.d.ts +10 -6
- package/dist/index.js +7 -3
- package/dist/logger.d.ts +7 -6
- package/dist/logger.js +0 -2
- package/dist/ltree/indexer.js +2 -4
- package/dist/ltree/ltree-node.svelte.d.ts +2 -1
- package/dist/ltree/ltree-node.svelte.js +1 -0
- package/dist/ltree/ltree.svelte.d.ts +1 -1
- package/dist/ltree/ltree.svelte.js +168 -175
- package/dist/ltree/types.d.ts +12 -8
- package/dist/perf-logger.d.ts +2 -1
- package/dist/perf-logger.js +0 -2
- package/dist/styles/main.scss +78 -78
- package/dist/styles.css +41 -41
- package/dist/styles.css.map +1 -1
- package/dist/vendor/loglevel/index.d.ts +55 -2
- package/dist/vendor/loglevel/prefix.d.ts +23 -2
- package/package.json +96 -95
- package/dist/ltree/ltree-demo.d.ts +0 -2
- package/dist/ltree/ltree-demo.js +0 -90
- package/dist/vendor/loglevel/loglevel-esm.d.ts +0 -2
- package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +0 -7
- package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +0 -2
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
ADVANCED PATTERNS
|
|
2
|
+
=================
|
|
3
|
+
|
|
4
|
+
Complex use cases and integration patterns for the tree component.
|
|
5
|
+
|
|
6
|
+
EXTERNAL UPDATE API
|
|
7
|
+
-------------------
|
|
8
|
+
Update props from vanilla JavaScript:
|
|
9
|
+
|
|
10
|
+
// Get reference to tree
|
|
11
|
+
const treeElement = document.querySelector('#my-tree');
|
|
12
|
+
|
|
13
|
+
// Update multiple props
|
|
14
|
+
treeElement.update({
|
|
15
|
+
searchText: 'query',
|
|
16
|
+
expandLevel: 3,
|
|
17
|
+
data: newData
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Update single prop
|
|
21
|
+
treeElement.update({ searchText: '' });
|
|
22
|
+
|
|
23
|
+
All props updatable except snippets:
|
|
24
|
+
- data, searchText, selectedNode, expandLevel
|
|
25
|
+
- Members: idMember, pathMember, displayValueMember
|
|
26
|
+
- Callbacks: sortCallback, onNodeClicked, etc.
|
|
27
|
+
- Visual: bodyClass, selectedNodeClass, etc.
|
|
28
|
+
|
|
29
|
+
LAZY LOADING CHILDREN
|
|
30
|
+
---------------------
|
|
31
|
+
Load children on demand:
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
let data = $state.raw([
|
|
35
|
+
{ id: '1', path: '1', name: 'Folder', hasChildren: true }
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
async function handleClick(node) {
|
|
39
|
+
if (node.data.hasChildren && !node.hasChildren) {
|
|
40
|
+
// Fetch children from server
|
|
41
|
+
const children = await fetchChildren(node.path);
|
|
42
|
+
|
|
43
|
+
// Add to data
|
|
44
|
+
data = [...data, ...children];
|
|
45
|
+
|
|
46
|
+
// Expand the node
|
|
47
|
+
treeRef.expandNodes(node.path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<Tree
|
|
53
|
+
{data}
|
|
54
|
+
hasChildrenMember="hasChildren"
|
|
55
|
+
onNodeClicked={handleClick}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
VIRTUALIZED TREE (EXTERNAL)
|
|
59
|
+
---------------------------
|
|
60
|
+
For extremely large trees (50,000+ nodes), implement external virtualization:
|
|
61
|
+
|
|
62
|
+
<script>
|
|
63
|
+
import { VirtualList } from 'svelte-virtual-list';
|
|
64
|
+
|
|
65
|
+
let flatNodes = $derived(getFlatVisibleNodes(treeData));
|
|
66
|
+
|
|
67
|
+
function getFlatVisibleNodes(data) {
|
|
68
|
+
// Flatten tree respecting expanded state
|
|
69
|
+
const result = [];
|
|
70
|
+
function traverse(nodes, level) {
|
|
71
|
+
for (const node of nodes) {
|
|
72
|
+
result.push({ ...node, level });
|
|
73
|
+
if (node.isExpanded && node.children) {
|
|
74
|
+
traverse(node.children, level + 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
traverse(data, 0);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<VirtualList items={flatNodes} let:item>
|
|
84
|
+
<div style="padding-left: {item.level * 20}px">
|
|
85
|
+
{item.name}
|
|
86
|
+
</div>
|
|
87
|
+
</VirtualList>
|
|
88
|
+
|
|
89
|
+
MULTI-SELECT MODE
|
|
90
|
+
-----------------
|
|
91
|
+
Implement checkbox-based multi-selection:
|
|
92
|
+
|
|
93
|
+
<script>
|
|
94
|
+
let selectedPaths = $state(new Set());
|
|
95
|
+
|
|
96
|
+
function toggleSelection(node) {
|
|
97
|
+
const newSet = new Set(selectedPaths);
|
|
98
|
+
if (newSet.has(node.path)) {
|
|
99
|
+
newSet.delete(node.path);
|
|
100
|
+
} else {
|
|
101
|
+
newSet.add(node.path);
|
|
102
|
+
}
|
|
103
|
+
selectedPaths = newSet;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isSelected(path) {
|
|
107
|
+
return selectedPaths.has(path);
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<Tree {data}>
|
|
112
|
+
{#snippet nodeTemplate(node)}
|
|
113
|
+
<label>
|
|
114
|
+
<input
|
|
115
|
+
type="checkbox"
|
|
116
|
+
checked={isSelected(node.path)}
|
|
117
|
+
onchange={() => toggleSelection(node)}
|
|
118
|
+
/>
|
|
119
|
+
{node.data.name}
|
|
120
|
+
</label>
|
|
121
|
+
{/snippet}
|
|
122
|
+
</Tree>
|
|
123
|
+
|
|
124
|
+
KEYBOARD NAVIGATION
|
|
125
|
+
-------------------
|
|
126
|
+
Add keyboard support:
|
|
127
|
+
|
|
128
|
+
<script>
|
|
129
|
+
let selectedNode = $state(null);
|
|
130
|
+
let treeRef;
|
|
131
|
+
|
|
132
|
+
function handleKeydown(e) {
|
|
133
|
+
if (!selectedNode) return;
|
|
134
|
+
|
|
135
|
+
switch (e.key) {
|
|
136
|
+
case 'ArrowDown':
|
|
137
|
+
selectNext();
|
|
138
|
+
break;
|
|
139
|
+
case 'ArrowUp':
|
|
140
|
+
selectPrevious();
|
|
141
|
+
break;
|
|
142
|
+
case 'ArrowRight':
|
|
143
|
+
treeRef.expandNodes(selectedNode.path);
|
|
144
|
+
break;
|
|
145
|
+
case 'ArrowLeft':
|
|
146
|
+
treeRef.collapseNodes(selectedNode.path);
|
|
147
|
+
break;
|
|
148
|
+
case 'Enter':
|
|
149
|
+
activateNode(selectedNode);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function selectNext() {
|
|
155
|
+
const flat = treeRef.ltree.flatTreeNodes;
|
|
156
|
+
const idx = flat.findIndex(n => n.path === selectedNode.path);
|
|
157
|
+
if (idx < flat.length - 1) {
|
|
158
|
+
selectedNode = flat[idx + 1];
|
|
159
|
+
treeRef.scrollToPath(selectedNode.path);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<div onkeydown={handleKeydown} tabindex="0">
|
|
165
|
+
<Tree bind:this={treeRef} bind:selectedNode />
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
UNDO/REDO SYSTEM
|
|
169
|
+
----------------
|
|
170
|
+
Track operations for undo:
|
|
171
|
+
|
|
172
|
+
<script>
|
|
173
|
+
let history = $state([]);
|
|
174
|
+
let historyIndex = $state(-1);
|
|
175
|
+
|
|
176
|
+
function execute(operation, undoOperation) {
|
|
177
|
+
operation();
|
|
178
|
+
|
|
179
|
+
// Clear redo stack
|
|
180
|
+
history = history.slice(0, historyIndex + 1);
|
|
181
|
+
history = [...history, { redo: operation, undo: undoOperation }];
|
|
182
|
+
historyIndex = history.length - 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function undo() {
|
|
186
|
+
if (historyIndex >= 0) {
|
|
187
|
+
history[historyIndex].undo();
|
|
188
|
+
historyIndex--;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function redo() {
|
|
193
|
+
if (historyIndex < history.length - 1) {
|
|
194
|
+
historyIndex++;
|
|
195
|
+
history[historyIndex].redo();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Example: tracked add
|
|
200
|
+
function addNode(parentPath, nodeData) {
|
|
201
|
+
let addedPath;
|
|
202
|
+
|
|
203
|
+
execute(
|
|
204
|
+
() => {
|
|
205
|
+
const result = treeRef.addNode(parentPath, nodeData);
|
|
206
|
+
addedPath = result.node?.path;
|
|
207
|
+
},
|
|
208
|
+
() => {
|
|
209
|
+
if (addedPath) treeRef.removeNode(addedPath);
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
</script>
|
|
214
|
+
|
|
215
|
+
REAL-TIME SYNC
|
|
216
|
+
--------------
|
|
217
|
+
Sync with WebSocket/server:
|
|
218
|
+
|
|
219
|
+
<script>
|
|
220
|
+
import { onMount } from 'svelte';
|
|
221
|
+
|
|
222
|
+
let data = $state.raw([]);
|
|
223
|
+
|
|
224
|
+
onMount(() => {
|
|
225
|
+
const ws = new WebSocket('ws://server/tree');
|
|
226
|
+
|
|
227
|
+
ws.onmessage = (event) => {
|
|
228
|
+
const message = JSON.parse(event.data);
|
|
229
|
+
|
|
230
|
+
switch (message.type) {
|
|
231
|
+
case 'add':
|
|
232
|
+
data = [...data, message.node];
|
|
233
|
+
break;
|
|
234
|
+
case 'remove':
|
|
235
|
+
data = data.filter(d => !d.path.startsWith(message.path));
|
|
236
|
+
break;
|
|
237
|
+
case 'update':
|
|
238
|
+
data = data.map(d =>
|
|
239
|
+
d.path === message.path ? { ...d, ...message.changes } : d
|
|
240
|
+
);
|
|
241
|
+
break;
|
|
242
|
+
case 'full':
|
|
243
|
+
data = message.nodes;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return () => ws.close();
|
|
249
|
+
});
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
FILTERED EXTERNAL VIEW
|
|
253
|
+
----------------------
|
|
254
|
+
Keep full data but show filtered view:
|
|
255
|
+
|
|
256
|
+
<script>
|
|
257
|
+
let allData = $state.raw([]);
|
|
258
|
+
let filter = $state('all');
|
|
259
|
+
|
|
260
|
+
let filteredData = $derived(() => {
|
|
261
|
+
if (filter === 'all') return allData;
|
|
262
|
+
return allData.filter(item => item.type === filter);
|
|
263
|
+
});
|
|
264
|
+
</script>
|
|
265
|
+
|
|
266
|
+
<select bind:value={filter}>
|
|
267
|
+
<option value="all">All</option>
|
|
268
|
+
<option value="folder">Folders</option>
|
|
269
|
+
<option value="file">Files</option>
|
|
270
|
+
</select>
|
|
271
|
+
|
|
272
|
+
<Tree data={filteredData} ... />
|
|
273
|
+
|
|
274
|
+
Note: This loses hierarchy for filtered items. Better to use internal search.
|
|
275
|
+
|
|
276
|
+
DRAG BETWEEN MULTIPLE TREES
|
|
277
|
+
---------------------------
|
|
278
|
+
<script>
|
|
279
|
+
let sourceData = $state.raw([...]);
|
|
280
|
+
let targetData = $state.raw([...]);
|
|
281
|
+
|
|
282
|
+
function handleSourceDrop(dropNode, draggedNode, position, event, operation) {
|
|
283
|
+
// Item dropped back to source
|
|
284
|
+
if (draggedNode.data.sourceTree === 'source') {
|
|
285
|
+
// Handle internal move
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function handleTargetDrop(dropNode, draggedNode, position, event, operation) {
|
|
290
|
+
// Item dropped to target from source
|
|
291
|
+
if (draggedNode.data.sourceTree === 'source') {
|
|
292
|
+
// Remove from source
|
|
293
|
+
sourceData = sourceData.filter(d => d.path !== draggedNode.path);
|
|
294
|
+
|
|
295
|
+
// Add to target
|
|
296
|
+
const newItem = {
|
|
297
|
+
...draggedNode.data,
|
|
298
|
+
path: calculateNewPath(dropNode, position),
|
|
299
|
+
sourceTree: 'target'
|
|
300
|
+
};
|
|
301
|
+
targetData = [...targetData, newItem];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
</script>
|
|
305
|
+
|
|
306
|
+
<Tree
|
|
307
|
+
data={sourceData}
|
|
308
|
+
treeId="source"
|
|
309
|
+
dragDropMode="cross"
|
|
310
|
+
onNodeDrop={handleSourceDrop}
|
|
311
|
+
/>
|
|
312
|
+
|
|
313
|
+
<Tree
|
|
314
|
+
data={targetData}
|
|
315
|
+
treeId="target"
|
|
316
|
+
dragDropMode="both"
|
|
317
|
+
onNodeDrop={handleTargetDrop}
|
|
318
|
+
/>
|
|
319
|
+
|
|
320
|
+
FORM INTEGRATION
|
|
321
|
+
----------------
|
|
322
|
+
Use tree as form input:
|
|
323
|
+
|
|
324
|
+
<script>
|
|
325
|
+
let selectedPaths = $state([]);
|
|
326
|
+
|
|
327
|
+
function handleSubmit(e) {
|
|
328
|
+
const formData = new FormData(e.target);
|
|
329
|
+
formData.set('selectedNodes', JSON.stringify(selectedPaths));
|
|
330
|
+
// Submit form
|
|
331
|
+
}
|
|
332
|
+
</script>
|
|
333
|
+
|
|
334
|
+
<form onsubmit={handleSubmit}>
|
|
335
|
+
<input type="hidden" name="selectedNodes" value={JSON.stringify(selectedPaths)} />
|
|
336
|
+
|
|
337
|
+
<Tree {data}>
|
|
338
|
+
{#snippet nodeTemplate(node)}
|
|
339
|
+
<label>
|
|
340
|
+
<input
|
|
341
|
+
type="checkbox"
|
|
342
|
+
checked={selectedPaths.includes(node.path)}
|
|
343
|
+
onchange={(e) => {
|
|
344
|
+
if (e.target.checked) {
|
|
345
|
+
selectedPaths = [...selectedPaths, node.path];
|
|
346
|
+
} else {
|
|
347
|
+
selectedPaths = selectedPaths.filter(p => p !== node.path);
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
/>
|
|
351
|
+
{node.data.name}
|
|
352
|
+
</label>
|
|
353
|
+
{/snippet}
|
|
354
|
+
</Tree>
|
|
355
|
+
|
|
356
|
+
<button type="submit">Submit</button>
|
|
357
|
+
</form>
|
|
358
|
+
|
|
359
|
+
BREADCRUMB NAVIGATION
|
|
360
|
+
---------------------
|
|
361
|
+
<script>
|
|
362
|
+
let selectedNode = $state(null);
|
|
363
|
+
|
|
364
|
+
let breadcrumbs = $derived(() => {
|
|
365
|
+
if (!selectedNode) return [];
|
|
366
|
+
|
|
367
|
+
const crumbs = [];
|
|
368
|
+
let currentPath = selectedNode.path;
|
|
369
|
+
|
|
370
|
+
while (currentPath) {
|
|
371
|
+
const node = treeRef.getNodeByPath(currentPath);
|
|
372
|
+
if (node) crumbs.unshift(node);
|
|
373
|
+
currentPath = node?.parentPath;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return crumbs;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
function navigateTo(node) {
|
|
380
|
+
selectedNode = node;
|
|
381
|
+
treeRef.scrollToPath(node.path, { expand: true, highlight: true });
|
|
382
|
+
}
|
|
383
|
+
</script>
|
|
384
|
+
|
|
385
|
+
<nav class="breadcrumbs">
|
|
386
|
+
{#each breadcrumbs as crumb, i}
|
|
387
|
+
{#if i > 0} / {/if}
|
|
388
|
+
<button onclick={() => navigateTo(crumb)}>
|
|
389
|
+
{crumb.data?.name}
|
|
390
|
+
</button>
|
|
391
|
+
{/each}
|
|
392
|
+
</nav>
|
|
393
|
+
|
|
394
|
+
<Tree bind:this={treeRef} bind:selectedNode />
|
|
395
|
+
|
|
396
|
+
PERMISSION-BASED ACTIONS
|
|
397
|
+
------------------------
|
|
398
|
+
<script>
|
|
399
|
+
const permissions = {
|
|
400
|
+
canEdit: true,
|
|
401
|
+
canDelete: false,
|
|
402
|
+
canCreate: true
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
function createContextMenu(node, closeMenuCallback) {
|
|
406
|
+
const items = [];
|
|
407
|
+
|
|
408
|
+
items.push({
|
|
409
|
+
icon: '📂',
|
|
410
|
+
title: 'Open',
|
|
411
|
+
callback: () => { openItem(node); closeMenuCallback(); }
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (permissions.canEdit) {
|
|
415
|
+
items.push({
|
|
416
|
+
icon: '✏️',
|
|
417
|
+
title: 'Edit',
|
|
418
|
+
callback: () => editItem(node)
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (permissions.canCreate && node.data.type === 'folder') {
|
|
423
|
+
items.push({
|
|
424
|
+
icon: '➕',
|
|
425
|
+
title: 'New Item',
|
|
426
|
+
callback: () => createItem(node)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (permissions.canDelete) {
|
|
431
|
+
items.push({ isDivider: true });
|
|
432
|
+
items.push({
|
|
433
|
+
icon: '🗑️',
|
|
434
|
+
title: 'Delete',
|
|
435
|
+
callback: () => deleteItem(node)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return items;
|
|
440
|
+
}
|
|
441
|
+
</script>
|
|
442
|
+
|
|
443
|
+
TREE DIFF/COMPARE
|
|
444
|
+
-----------------
|
|
445
|
+
Compare two tree states:
|
|
446
|
+
|
|
447
|
+
<script>
|
|
448
|
+
function diffTrees(oldData, newData) {
|
|
449
|
+
const oldPaths = new Set(oldData.map(d => d.path));
|
|
450
|
+
const newPaths = new Set(newData.map(d => d.path));
|
|
451
|
+
|
|
452
|
+
const added = newData.filter(d => !oldPaths.has(d.path));
|
|
453
|
+
const removed = oldData.filter(d => !newPaths.has(d.path));
|
|
454
|
+
const modified = newData.filter(d => {
|
|
455
|
+
const old = oldData.find(o => o.path === d.path);
|
|
456
|
+
return old && JSON.stringify(old) !== JSON.stringify(d);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return { added, removed, modified };
|
|
460
|
+
}
|
|
461
|
+
</script>
|
|
462
|
+
|
|
463
|
+
SSR CONSIDERATIONS
|
|
464
|
+
------------------
|
|
465
|
+
Tree works with SSR (SvelteKit):
|
|
466
|
+
|
|
467
|
+
// +page.svelte
|
|
468
|
+
<script>
|
|
469
|
+
import { Tree } from '@keenmate/svelte-treeview';
|
|
470
|
+
import { browser } from '$app/environment';
|
|
471
|
+
|
|
472
|
+
export let data; // From +page.server.ts
|
|
473
|
+
</script>
|
|
474
|
+
|
|
475
|
+
{#if browser}
|
|
476
|
+
<Tree data={data.items} ... />
|
|
477
|
+
{:else}
|
|
478
|
+
<div class="tree-placeholder">Loading tree...</div>
|
|
479
|
+
{/if}
|
|
480
|
+
|
|
481
|
+
Or with onMount:
|
|
482
|
+
|
|
483
|
+
<script>
|
|
484
|
+
import { onMount } from 'svelte';
|
|
485
|
+
|
|
486
|
+
let mounted = $state(false);
|
|
487
|
+
onMount(() => { mounted = true; });
|
|
488
|
+
</script>
|
|
489
|
+
|
|
490
|
+
{#if mounted}
|
|
491
|
+
<Tree ... />
|
|
492
|
+
{/if}
|
|
493
|
+
|
|
494
|
+
BEST PRACTICES
|
|
495
|
+
--------------
|
|
496
|
+
✅ Use $state.raw() for large datasets
|
|
497
|
+
✅ Debounce expensive operations
|
|
498
|
+
✅ Handle async operations properly
|
|
499
|
+
✅ Implement undo for destructive actions
|
|
500
|
+
✅ Use WebSocket for real-time updates
|
|
501
|
+
✅ Consider SSR implications
|
|
502
|
+
|
|
503
|
+
❌ Don't block UI with sync operations
|
|
504
|
+
❌ Don't forget error handling
|
|
505
|
+
❌ Don't implement virtual scroll unless needed (50K+ nodes)
|
|
506
|
+
❌ Don't mutate data directly
|