Guide

Arrays & Lists

Manage collections of editable items with Editable.Each โ€” type-safe array editing with automatic indexing.

Basic Usage

Editable.Each iterates over an array and provides editor snippets for each item. Each item gets its own set of text, multiline, rich, and image snippets, automatically scoped to the correct index.

Live Example

First Note

Content of the first note.

Second Note

Content of the second note.

<script lang="ts">
  import * as Editable from 'editable-kit/editable';
  import type { ProseMirrorJSON } from 'editable-kit';

  type Note = { title: ProseMirrorJSON; body: ProseMirrorJSON };

  let editing = $state(false);
  let notes: Note[] = $state([
    {
      title: { type: 'doc', content: [{ type: 'text', text: 'First Note' }] },
      body: {
        type: 'doc',
        content: [{ type: 'paragraph',
          content: [{ type: 'text', text: 'Content here.' }] }]
      }
    }
  ]);
</script>

<Editable.Root {editing}>
  {#snippet children({ save })}
    <Editable.Each key="notes" data={notes} onsave={(items) => {
      items.forEach((item, i) => {
        if (item.title.type === 'text') notes[i].title = item.title.content;
        if (item.body.type === 'text') notes[i].body = item.body.content;
      });
    }}>
      {#snippet each(item, index, { text, multiline })}
        <div class="border rounded-lg p-4">
          <h3>{@render text('title')}</h3>
          <div>{@render multiline('body')}</div>
        </div>
      {/snippet}
    </Editable.Each>
  {/snippet}
</Editable.Root>

Add & Remove

Since data is a reactive array, you can add or remove items while editing. The editor components will mount and unmount automatically.

Live Example

Shopping List

Eggs, milk, bread

Ideas

Learn Svelte 5 runes

<script lang="ts">
  // ... type Note and imports ...

  function addNote() {
    notes = [...notes, {
      title: { type: 'doc', content: [{ type: 'text', text: 'New Note' }] },
      body: {
        type: 'doc',
        content: [{ type: 'paragraph',
          content: [{ type: 'text', text: '' }] }]
      }
    }];
  }

  function removeNote(index: number) {
    notes = notes.filter((_, i) => i !== index);
  }
</script>

<Editable.Root {editing}>
  {#snippet children({ save })}
    <Editable.Each key="notes" data={notes}>
      {#snippet each(item, index, { text, multiline })}
        <div class="border rounded-lg p-4 relative">
          {#if editing}
            <button onclick={() => removeNote(index)}
              class="absolute top-2 right-2">&times;</button>
          {/if}
          <h3>{@render text('title')}</h3>
          <div>{@render multiline('body')}</div>
        </div>
      {/snippet}
    </Editable.Each>
    {#if editing}
      <button onclick={addNote}>+ Add Note</button>
    {/if}
  {/snippet}
</Editable.Root>

Save Handling

The onsave callback on Editable.Each receives an EditorSaveData<T>[] array โ€” one entry per item, in the same order as the input data array. Each entry contains the editor content for every field on that item, using the same EditorContent discriminated union as Editable.Data.

<!-- Editable.Each manages arrays of items -->
<Editable.Each key="notes" data={notes} onsave={(items) => {
  // items is EditorSaveData<Note>[]
  items.forEach((item, i) => {
    if (item.title.type === 'text') notes[i].title = item.title.content;
    if (item.body.type === 'text') notes[i].body = item.body.content;
  });
}}>
  {#snippet each(item, index, { text, multiline })}
    <div class="rounded-lg border p-4">
      <h3>{@render text('title')}</h3>
      <div>{@render multiline('body')}</div>
    </div>
  {/snippet}
</Editable.Each>

Use forEach to iterate the save array and update your local state. Check item.field.type to narrow the content type before accessing the value.

Type Safety

Editable.Each infers JSONKeys<T> and ImageKeys<T> the same way as Editable.Data. The snippet selectors are type-checked against the item type, so invalid property names are caught at compile time.

For example, given Note = { title: ProseMirrorJSON; body: ProseMirrorJSON }, calling text('title') and multiline('body') are valid, but image('title') would be a compile error since ProseMirrorJSON does not extend ImageState.