---
title: Vorlagen speichern & laden
description: Verbinden Sie den Speicher-/Ladezyklus des Editors mit Ihrem eigenen Speicher — inklusive Name, Speichern-Schaltfläche, Autosave und Warnung bei ungespeicherten Änderungen.
---

# Vorlagen speichern & laden

Geben Sie dem Editor einen Ort zum Speichern, und er ergänzt die passende Bedienoberfläche: einen direkt bearbeitbaren Vorlagennamen, eine Speichern-Schaltfläche, eine Statusanzeige, `Cmd`/`Strg`+`S`, optionales Autosave sowie eine Warnung, bevor der Tab mit ungespeicherter Arbeit geschlossen wird.

Das alles übernimmt der Editor. **Die Persistenz liegt bei Ihnen** — drei Methoden gegen Ihre eigene API.

## Schnellstart

```ts
import { init } from "@templatical/editor";

const editor = await init({
  container: "#editor",
  templates: {
    load: async (id) => {
      const res = await fetch(`/api/templates/${id}`);
      return res.json();
    },

    create: async (input) => {
      const res = await fetch("/api/templates", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(input),
      });
      return res.json();
    },

    save: async (id, patch) => {
      const res = await fetch(`/api/templates/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(patch),
      });
      return res.json();
    },
  },
});

// Das Öffnen einer Vorlage erfolgt imperativ — Ihre Anwendung entscheidet, welche.
await editor.load("tpl_123");
```

**Lassen Sie `templates` weg, fehlt die Funktion** — kein Namensfeld, keine Speichern-Schaltfläche, keine Statusanzeige. `create()` / `load()` / `save()` werden dann mit einer erklärenden Fehlermeldung abgelehnt, und Sie speichern den Inhalt selbst über [`onChange`](#speichern-ohne-provider).

## Der Vertrag

```ts
interface Template {
  id: string;
  name?: string;
  createdAt?: string;
  updatedAt?: string;
  content: TemplateContent;
}

type TemplatePatch = Partial<{ name: string; content: TemplateContent }>;

interface TemplatesProvider {
  load(id: string): Promise<Template>;
  create:
    | false
    | ((input: {
        name?: string;
        content: TemplateContent;
      }) => Promise<Template>);
  save: false | ((id: string, patch: TemplatePatch) => Promise<Template>);
}
```

- **Die `id` kommt aus Ihrem Speicher**, zurückgegeben von `create()`. Der Editor erzeugt nie selbst eine — ein Datenbankschlüssel, ein Slug, eine Dokument-ID, was auch immer Ihr Speicher bereits verwendet.
- **`save` erhält einen Patch**, nicht den bloßen Inhalt. So kann eine Umbenennung ohne Inhalt übertragen werden, und ein künftiges Feld lässt sich ergänzen, ohne Ihre Implementierung zu brechen. Der Editor sendet `name` (sofern die Vorlage einen hat) und `content` gemeinsam, in einem Aufruf.
- **`createdAt` / `updatedAt` sind optional, ISO 8601 und dienen nur der Anzeige.** Beide fehlen in `TemplatePatch`, der Editor schreibt sie also nie — angezeigt wird, was `load` oder `save` zurückgegeben hat. Siehe [Der Zeitstempel](#der-zeitstempel).
- **`name` ist optional.** Ohne Namensspalte lassen Sie ihn weg — der Header zeigt stattdessen ein gedimmtes „Unbenannt". Das Feld bleibt bearbeitbar, solange `save` eine Funktion ist; eine Umbenennung wird also weiterhin als `save(id, { name, content })` gesendet. Ein Store, der sie ignoriert, gibt eine Vorlage ohne Namen zurück, und der Header zeigt wieder „Unbenannt". Wenn Sie Namen gar nicht nutzen, [blenden Sie das Feld aus](#das-namensfeld-ausblenden).
- **Persistenzseitige Effekte gehören in den Rumpf von `save()`.** Er läuft bei jedem Schreibvorgang, sodass Logging, Cache-Invalidierung oder ein Webhook-Aufruf dort ihren festen Platz haben. Siehe [Events](#events) für das, was der Editor freigibt, sobald dieser Schreibvorgang abgeschlossen ist.

Jede Methode darf ablehnen. Der Editor meldet den Fehler über `onError`, zeigt ihn im Header an und lässt seinen Zustand unberührt — nichts wird als gespeichert markiert, was es nicht ist.

### Kein `list`, kein `delete`

Der Editor hat keinen Vorlagen-Browser. Die Auswahl, _welche_ Vorlage geöffnet wird, gehört in Ihre Anwendung; die Aufgabe des Editors beginnt, sobald Sie ihm eine ID übergeben.

## Events

```ts
templates: {
  load, create, save,
  onSaved:   (template, { trigger }) => {},
  onCreated: (template) => {},
  onLoaded:  (template) => {},
}
```

`trigger` ist eines von `manual`, `autosave`, `rename`, `restore`, `api`. `manual` deckt die Speichern-Schaltfläche im Header sowie `Cmd`/`Strg`+`S` ab.

::: tip Wohin Logik nach dem Speichern gehört
Persistenz gehört in `save()` — das ist Ihre Funktion, und ihr Rumpf läuft, bevor der Editor die Antwort übernommen hat. `onSaved` ist für die zwei Dinge da, die dieser Rumpf nicht liefern kann: welches Bedienelement das Speichern ausgelöst hat, und ein Zeitpunkt, zu dem sich der Editor stabilisiert hat. Eine Navigation aus `save()` heraus erfolgt, während `isDirty` noch `true` ist, sodass Ihre eigene Router-Sperre sie blockiert.
:::

Eine Handler-Funktion, die einen Fehler wirft, wird abgefangen und an `onError` gemeldet — sie lässt das auslösende Speichern, Erstellen oder Laden nie fehlschlagen.

## Erstellen oder Speichern deaktivieren

`create` und `save` nehmen `false | fn` entgegen. `load` lässt sich nicht abschalten — ohne es gäbe es nichts zu öffnen.

```ts
templates: {
  load: (id) => fetchTemplate(id),
  create: false,  // editor.create() lehnt ab
  save: false,    // nur lesend: lädt und bearbeitet lokal, speichert nichts
}
```

**`save: false`** verbirgt die Speichern-Schaltfläche und die Statusanzeige und macht den Namen schreibgeschützt: Eine Änderung hätte nirgendwo hin. `Cmd`/`Strg`+`S` und Autosave laufen dann wirkungslos durch, statt einen Fehler zu erzeugen. Eine Vorlage zu laden und lokal zu bearbeiten funktioniert weiterhin.

**`create: false`** bewirkt, dass `editor.create()` ablehnt. Es verbirgt nichts, denn der Editor hat kein eigenes Bedienelement zum Anlegen — erzeugt wird immer über `editor.create()` aus Ihrer Anwendung heraus. Da Sie den Schalter selbst setzen, koppeln Sie ein eigenes Bedienelement für neue Vorlagen an denselben Wert oder fangen den Aufruf mit `try` / `catch` ab.

::: warning Keine Sicherheitsgrenze
Diese Schalter leben im Browser des Nutzers. Sie prägen die Oberfläche; sie schützen Ihre API nicht. Erzwingen Sie Berechtigungen serverseitig.
:::

## Der Header

<!-- prettier-ignore -->
| Position | Inhalt |
| --- | --- |
| links | der Vorlagenname, per Klick bearbeitbar, darunter der Zeitstempel |
| rechts | der Speicherstatus, dann die Speichern-Schaltfläche |

Der Name wird mit `Enter` oder beim Verlassen des Feldes übernommen, mit `Escape` verworfen; ein leerer Wert wird abgelehnt und der vorherige wiederhergestellt — ein geleertes Feld ist weit eher ein Versehen als eine Absicht. Eine Umbenennung ist eine gewöhnliche ungespeicherte Änderung: sie markiert den Editor als geändert und wird beim nächsten Speichern übertragen, im selben Patch wie der Inhalt.

Die Statusanzeige kennt drei Zustände:

| Zustand                      | Wird gezeigt, wenn                                                             |
| ---------------------------- | ------------------------------------------------------------------------------ |
| **Nicht gespeichert**        | es Änderungen gibt, von denen der Editor weiß, dass sie nicht gespeichert sind |
| **Gespeichert**              | ein Speichern gerade erfolgreich war (für einige Sekunden)                     |
| **Speichern fehlgeschlagen** | der letzte Versuch abgelehnt wurde — Ihre Fehlermeldung steht im Tooltip       |

Die Speichern-Schaltfläche ist deaktiviert, solange keine Vorlage existiert, denn `save()` aktualisiert eine ID. Rufen Sie zuerst `create()` oder `load()` auf.

### Der Zeitstempel

Eine Vorlage mit `updatedAt` zeigt unter dem Namen eine relative Zeitangabe, das vollständige Datum erscheint beim Überfahren:

> Aktualisiert vor 5 Min.

`createdAt` dient als Rückfallwert, wenn `updatedAt` fehlt, und die Formulierung folgt dem verwendeten Feld — eine Vorlage, die Ihr Store nie neu geschrieben hat, zeigt „Erstellt", nie „Aktualisiert". Fehlen beide Felder, oder lässt sich der Wert nicht lesen, erscheint keine Zeile. Die Angabe aktualisiert sich, während der Editor geöffnet bleibt.

::: warning `updatedAt` nicht in `create` setzen
Setzen Sie beim ersten Schreiben eines Templates ausschließlich `createdAt`. Werden beide Felder gestempelt, zeigt ein brandneues Template **„Gerade aktualisiert“** an, bevor es überhaupt bearbeitet wurde – der Editor bevorzugt `updatedAt` und beschriftet die Zeile entsprechend, gibt also genau das wieder, was Ihr Store behauptet hat.

Das passiert leicht versehentlich: ein Spalten-Default `updated_at = created_at` beim Insert oder ein einzelnes `now()`, das beiden Feldern zugewiesen wird. Lassen Sie das erste `save` das erste sein, was `updatedAt` setzt.
:::

::: tip
Sie erscheint unabhängig davon, ob `save` verfügbar ist — bei einer schreibgeschützten Vorlage tritt sie an die Stelle der Statusanzeige.
:::

### Das Namensfeld ausblenden

```ts
init({
  templates: {
    nameField: false,
    /* … */
  },
});
```

Entfernt das Feld aus dem Header, unabhängig davon, ob Ihr Provider speichern kann. `editor.create({ name })`, `setName()` und der `name` in jedem Speicher-Patch funktionieren weiter, Ihre eigene Bedienoberfläche kann den Namen also weiterhin verwalten. `initCloud()` akzeptiert denselben Schlüssel auf seinem eigenen `templates`-Objekt.

Ist das Feld ausgeblendet, bleibt der Zeitstempel der einzige Inhalt der linken Header-Spalte.

## Autosave

```ts
await init({
  container: "#editor",
  templates: {
    autoSave: true,
    /* … */
  },
  changeDebounce: 5000, // Standardwert 2000
});
```

`templates.autoSave` schaltet das Speichern ein. `changeDebounce` legt die Taktung fest — an der Wurzel der Konfiguration statt daneben.

::: tip `changeDebounce` taktet auch `onChange`
Derselbe Timer treibt beides, und [`onChange`](#speichern-ohne-provider) löst unabhängig davon aus, ob `templates` konfiguriert ist — die Taktung muss also auch aus einer Konfiguration ohne jeden Provider erreichbar bleiben.
:::

Die Verzögerung beginnt bei jeder Änderung neu, sodass aus einer Tippfolge ein einziges Speichern wird. Sie pausiert, während der Nutzer durch Undo/Redo navigiert, und überspringt das Speichern vollständig, wenn nichts geändert wurde.

::: warning `autoSave` braucht ein Ziel zum Speichern
`true` mit einem Provider, dessen `save` `false` ist, protokolliert eine Warnung und speichert nichts. Speichern Sie in diesem Fall über `onChange`.
:::

## Cmd+S

`Cmd`/`Strg`+`S` bedeutet immer „jetzt speichern":

- mit einem `templates`-Provider wird `save()` aufgerufen;
- ohne ihn wird der `onChange`-Debounce sofort ausgelöst, sodass der Tastendruck auch dann ankommt, wenn Sie über `onChange` speichern.

## Ungespeicherte Änderungen

Zwei Mechanismen, denn keiner deckt den anderen ab:

```ts
await init({
  container: "#editor",
  templates: {
    unsavedChangesGuard: true, // der Standard
    /* … */
  },
  onDirtyChange: (isDirty) => {
    hasUnsavedWork.value = isDirty;
  },
});
```

**`templates.unsavedChangesGuard`** ist eine `beforeunload`-Rückfrage, standardmäßig aktiv, sobald ein Provider konfiguriert ist. Sie deckt das Schließen oder Neuladen des Tabs ab. Setzen Sie sie auf `false`, um die Rückfrage selbst zu übernehmen. Ohne Provider warnt der Editor nie — er kann nicht wissen, ob Sie die Änderung bereits gespeichert haben.

**`onDirtyChange`** (und sein abfragbares Gegenstück `editor.isDirty()`) ist das, womit Sie einen clientseitigen Router absichern, denn `beforeunload` löst bei einer Navigation innerhalb der Anwendung nicht aus:

```ts
router.beforeEach((to, from, next) => {
  if (editor.isDirty() && !confirm("Ungespeicherte Änderungen verwerfen?"))
    return next(false);
  next();
});
```

`onDirtyChange` funktioniert mit und ohne Provider, in beiden Fällen an der Wurzel der Konfiguration. `initCloud()` akzeptiert `unsavedChangesGuard` auf seinem eigenen `templates`-Objekt, zu denselben Bedingungen — Cloud hat immer einen Speicherort, die Rückfrage ist dort also aktiv, sofern Sie sie nicht abschalten.

## Die Instanz-API

```ts
const template = await editor.create({ name: "Willkommens-E-Mail" });
await editor.load(template.id);
await editor.save();
editor.isDirty(); // boolean
```

- **`create(input?)`** speichert den aktuellen Inhalt als neue Vorlage. Übergeben Sie `content`, um zuvor den Inhalt des Editors zu ersetzen — `create({ content })` lädt und speichert damit in einem Schritt.
- **`load(id)`** holt eine Vorlage und macht sie zum Inhalt des Editors; lokale Änderungen werden verworfen.
- **`save()`** speichert Name und Inhalt der geladenen Vorlage als einen Patch.

Alle drei sind stets im Typ vorhanden und werden mit einer erklärenden Fehlermeldung abgelehnt, wenn kein Provider konfiguriert ist oder der Provider die jeweilige Methode zurückhält. Sichern Sie sie mit `try` / `catch` ab, wenn Sie sie aus einer eigenen Schaltfläche aufrufen. Der Header blendet seine eigenen Speichern-Bedienelemente aus, wenn `save` zurückgehalten wird; `create()` und `load()` haben dagegen überhaupt keine Editor-Oberfläche — ein Bedienelement dafür koppeln Sie selbst.

## Speichern ohne Provider

Ein Provider ist nicht der einzige Weg, eine Vorlage zu behalten. `onChange` löst verzögert bei jeder Inhaltsänderung aus:

```ts
await init({
  container: "#editor",
  onChange: (content) => myStore.save(content),
});
```

`Cmd`/`Strg`+`S` löst diesen Debounce sofort aus, sodass der Tastendruck bei Ihnen ankommt. Dann liegen auch die Speichern-Schaltfläche, der Status und die Rückfrage bei ungespeicherten Änderungen bei Ihnen. Der Provider existiert, damit Sie das nicht bauen müssen; `onChange` existiert für die Fälle, in denen die Oberfläche des Editors nicht das ist, was Sie wollen.

## Referenz

- [`init()`-Optionen](/de/api/editor)
- [Rendering & Export](/de/backend/render) — Bring-your-own-Rendering für MJML/HTML
- [Gespeicherte Blöcke](/de/backend/saved-blocks) — dieselbe Bring-your-own-Storage-Form, für wiederverwendbare Blockgruppen
- [Test-E-Mails](/de/backend/test-email) — Bring-your-own-Versand

**Sie nutzen Templatical Cloud?** Cloud implementiert diesen Vertrag ohne jede Konfiguration — siehe [Templates auf Cloud](/de/cloud/templates).
