Skip to content
SortableAPI Reference

createSortableTree

comins-sortable@0.1.6Framework neutral

Three tree methods for mapping nested data to areas and applying immutable updates.

import { createSortableTree } from "comins-sortable/core";
export declare function createSortableTree<T>(options: SortableTreeOptions<T>): SortableTree<T>;
export interface SortableTreeOptions<T> {
rootAreaId: string;
getNodeId(node: T): SortableId;
getChildren(node: T): readonly T[];
withChildren(node: T, children: readonly T[]): T;
getChildrenAreaId(node: T): string;
}
Option Input and responsibility
rootAreaId Non-empty root area ID, unique across all areas.
getNodeId Stable string or number ID, unique across the whole tree.
getChildren Returns a child array; leaves return an empty array.
withChildren Returns a node with the supplied children without mutating the existing node.
getChildrenAreaId Child area ID, distinct from root and every other child area ID.

All options are required. Do not recreate identities from array order or render counts. The helper cannot prevent mutation inside a consumer’s withChildren implementation.

export interface SortableTreeArea<T> {
areaId: string;
items: readonly T[];
parent?: SortableParentLocation;
}
getAreas(nodes: readonly T[]): readonly SortableTreeArea<T>[];

Traverses the tree and returns the root area plus every node’s child area, including empty ones. items references the corresponding input array. Root has no parent; child areas identify the parent item’s containing area and item ID. Does not register DOM or change state.

updateArea(nodes: readonly T[], areaId: string, items: readonly T[]): readonly T[];

Replaces the item array of one existing areaId and returns the full resulting tree. With an immutable withChildren, it preserves the input and reuses unchanged branches. Apply cross-area transfers together through applyChange; separate updates can create intermediate duplicate IDs.

applyChange(nodes: readonly T[], change: FrameworkSortableChange<T>): readonly T[];

Folds the framework adapter’s change.updates into one tree reconstruction. Store the returned tree in consumer state. Does not infer data from orders alone. Duplicate area IDs in one update batch or unknown areas fail. This is a data helper, not a DOM drag or mounting API.

Condition Error code
Empty area ID, non-array children, unknown update area, or duplicate update area INVALID_OPTION
Reused area ID anywhere in the tree DUPLICATE_AREA_ID
Reused node ID anywhere in the tree DUPLICATE_ITEM_ID
Node ID is neither string nor number MISSING_ITEM_ID

SortableError is thrown synchronously. Consumers must also handle exceptions thrown by their callbacks. Do not pass malformed or cyclic trees. Results are inspected again, but this is not a sanitizer that converts arbitrary input into a safe tree.

This is a tree data operation example. updateArea reverses root order; applyChange moves one child from a to b. For a real drag, apply the adapter callback’s change using setNodes(current => tree.applyChange(current, change)). See nested trees for DOM wiring and dragging.

sortable-tree.ts
import { createSortableTree, type FrameworkSortableChange } from 'comins-sortable/core';
export type TreeNode = { id: string; children: readonly TreeNode[] };
export const tree = createSortableTree<TreeNode>({
rootAreaId: 'root',
getNodeId: node => node.id,
getChildren: node => node.children,
withChildren: (node, children) => ({ ...node, children }),
getChildrenAreaId: node => `children:${node.id}`,
});
export const initialNodes: readonly TreeNode[] = [
{ id: 'a', children: [{ id: 'child', children: [] }] },
{ id: 'b', children: [] },
];
export function reverseRoots(nodes: readonly TreeNode[]) {
return tree.updateArea(nodes, 'root', [...nodes].reverse());
}
// A controlled adapter emits this shape after moving child from a to b.
export function moveChild(nodes: readonly TreeNode[]) {
const source = nodes.find(node => node.id === 'a');
const destination = nodes.find(node => node.id === 'b');
const child = source?.children.find(node => node.id === 'child');
if (!source || !destination || !child) return nodes;
const sourceItems = source.children.filter(node => node.id !== child.id);
const destinationItems = [...destination.children, child];
const change: FrameworkSortableChange<TreeNode> = {
operation: 'transfer', itemId: child.id,
source: { areaId: 'children:a', index: source.children.indexOf(child) },
destination: { areaId: 'children:b', index: destination.children.length },
orders: [
{ areaId: 'children:a', itemIds: sourceItems.map(node => node.id) },
{ areaId: 'children:b', itemIds: destinationItems.map(node => node.id) },
],
updates: [
{ areaId: 'children:a', items: sourceItems },
{ areaId: 'children:b', items: destinationItems },
],
};
// Real integrations pass the adapter's change directly in a state updater.
return tree.applyChange(nodes, change);
}

The npm declarations below were compared with a fixed source snapshot. The snapshot is not asserted to be the npm publishing commit.