Ember

Ember Logo GitHub Logo

A collection of headless svelte-5 components

Most implementations use arrays here and loop over everything to find the right node, but this implementation uses a record for O(1) access. Use this implementation for example when you have to render a large and deeply nested file tree. Additionally this library provides insert, update and delete actions to the node snippet as helpers. They are not required to use the component, but they make it easier to work with the tree.

The component expects to arguments

  1. A snippet, which renders each entry
  2. A tree structure which represents the nodes in the accordion. This has to be a binded state

The tree structure it expects is as follows


/**
 * A node in the tree. Can have any extra properties
 */	
export type Node<T extends object = object> = T & {
	/*
	 * A unique identifier for the node. This is
	 * required to perform actions on the tree. If it
	 * is not unique, actions will not work as expected.
	 * Most implementations use arrays here and loop over everything
	 * to find the right node, but this implementation
	 * uses a record for O(1) access.
	 */
	id: string;
	/*
	 * This property gets tacked on to the tree
	 * which is passed in and lets you determine
	 * if a node is expanded in the node snippet
	 */
	expanded?: boolean; 
	/*
	 * Any other properties you want to add to the node
	 */
	[key: string | number]: unknown;
};
/**
 * A node which can have children
 */
export type NodeWithChildren<T extends object = object> = Node<T> & {
	children?: Record<string, NodeWithChildren<T>>;
};
/**
 * The complete accordion tree. This is a record of nodes with children
 */
export type Tree<T extends object = object> = Record<string, NodeWithChildren<T>>;

The `node` snippet gets loaded with four helper methods. Update, Insert, Delete and Toggle

  • Update accepts a new node and will replace the current one in the tree
  • Delete removes the current node and all it's children from the tree
  • Insert accepts a new node and will add it as a child of the current node
  • Toggle toggles the expanded state of the current node

All actions are performed on the tree and any changes are reflected immediatelly.

Example

Select an action or toggle individual nodes

office

bedroom

Working Code

<script lang="ts">
    import { Tree, type NodeProps } from '@dle.dev/ember';
	import clsx from 'clsx';
	import { v4 as uuid } from 'uuid';
	const a = uuid();
	const b = uuid();
	const c = uuid();
     // Just an example of a tree, the actual mock tree can be 
     // found in the repository for this example
	let tree = $state({
		[a]: {
			id: a,
			name: 'Root',
			children: {
				[c]: {
					id: c,
					name: 'Child of root'
				}
			}
		},
		[b]: {
			id: b,
			name: 'Another Root',
			children: {}
		}
	});

</script>	
	
// You're in charge here. The node snippet gets loaded 
// with three actions. Insert, Delete and Update.
{#snippet node(content: nodeProps<{}>)}
	{@const disabled = !(
		content.children && Object.keys(content.children).length > 0
	)}
	<div
		transition:fly
		class={clsx(
			'mb-2 inline-flex nodes-baseline',
			'justify-baseline rounded-2xl border-[1px]',
			'border-primary-200 bg-primary-50 p-4',
			'align-baseline'
		)}
	>
		<button
			{disabled}
			type="button"
			class={clsx(
				'cursor-pointer',
				'mr-2 rounded-full border-[1px]',
				{
					'bg-primary-100 hover:bg-primary-200': !disabled,
					'cursor-not-allowed bg-gray-200': disabled
				},

				'px-2 ',
				'flex nodes-baseline justify-baseline'
			)}
			onclick={(e) => {
				e.preventDefault();
				content.actions.toggle();
			}}
		>
			<div
				class={clsx('m-x-1 my-2 h-4 w-4 cursor-pointer transition-all  ', {
					'rotate-90': content.expanded
				})}
			>
				<img src="/chevron.svg" alt="Chevron icon" />
			</div>
		</button>
		<h1>{content.name}</h1>

		<button
			class="hover:bg-primary-100flex-col ml-2 flex cursor-pointer rounded-sm border-[1px] px-4 py-2"
			type="button"
			onclick={() => {
				if (action === 'insert') {
					content.actions.insert({
						id: crypto.randomUUID(),
						name: 'New Node',
						children: {}
					});
				} else if (action === 'update') {
					content.actions.update({
						...content,
						name: 'Updated Node'
					});
				} else if (action === 'delete') {
					content.actions.delete();
				}
			}}>{action}</button
		>
	</div>
{/snippet}

<Tree
	{node}
	bind:tree
	wrapperProps={{
		class: 'ml-6'
	}}
	wrapperElement="span"
/>