Benutzerdefinierte Blöcke
Plan Feature
Diese Funktion ist nur im Scale-Plan verfügbar.
Benutzerdefinierte Blöcke ermöglichen es Ihnen, eigene Inhaltstypen zu definieren — Produktkarten, Veranstaltungslisten, Testimonials, Preistabellen — die neben den integrierten Blöcken in der Editor-Seitenleiste erscheinen. Ihre Benutzer ziehen sie auf die Arbeitsfläche, füllen die Felder aus und sehen eine Live-Vorschau, die aus einer von Ihnen bereitgestellten Liquid-Vorlage gerendert wird.
Benutzerdefinierte Blöcke sind vollständig deklarativ: Sie übergeben eine JSON-Konfiguration bei der Initialisierung. Keine JavaScript-Bundles, keine iframes, kein Build-Schritt. Das SDK übernimmt Rendering, Feldbearbeitung, Validierung und Export.
Mit Ihren Daten verbinden
Benutzerdefinierte Blöcke werden noch leistungsfähiger in Kombination mit Datenquellen. Anstatt Feldwerte manuell einzugeben, klicken Ihre Benutzer auf einen Button und der Block befüllt sich automatisch aus Ihrer Anwendung — ein Produkt aus Ihrem Katalog, ein Artikel aus Ihrem CMS, ein Eintrag aus Ihrem Inventar.
Häufige Anwendungsfälle:
- E-Commerce — Produktkarte hineinziehen, auf „Produkt auswählen" klicken, und Name, Preis und Bild werden aus Ihrem Shop geladen
- Immobilien — Einen Immobilien-Block hinzufügen, der Adresse, Fotos und Preis aus Ihrem MLS-Feed bezieht
- Veranstaltungsmanagement — Einen Veranstaltungs-Block einfügen, der Datum, Ort und Programm aus Ihrer Veranstaltungsdatenbank lädt
- Content-Marketing — Einen Artikel-Block verwenden, der Titel, Auszug und Vorschaubild aus Ihrem CMS abruft
- HR / Interne Kommunikation — Einen Mitarbeiter-Spotlight-Block erstellen, der Profildaten aus Ihrem Verzeichnis bezieht
Datenquellen sind optional — jeder benutzerdefinierte Block funktioniert auch ohne. Wenn Sie eine Datenquelle hinzufügen, zeigt das SDK ein Call-to-Action-Overlay, das den Benutzer zum Laden von Inhalten auffordert, und Felder, die Sie als readOnly markieren, werden auf die geladenen Werte gesperrt. Siehe Datenquellen für die vollständige Konfigurationsreferenz.
Schnellstart
Hier ist ein minimaler benutzerdefinierter Block — ein Ankündigungsbanner mit Titel, Nachricht und Hintergrundfarbe:
import { init } from '@templatical/embedded';
const editor = await init({
container: '#email-editor',
auth: { url: 'https://your-app.com/api/token' },
customBlocks: [
{
type: 'announcement',
name: 'Announcement',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m3 11 18-5v12L3 13v-2z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>',
description: 'A simple announcement banner',
fields: [
{ key: 'title', type: 'text', label: 'Title', default: 'Announcement' },
{ key: 'message', type: 'textarea', label: 'Message', default: 'Your message here' },
{ key: 'bgColor', type: 'color', label: 'Background Color', default: '#4f46e5' },
],
template: `
<div style="background: {{ bgColor }}; padding: 24px; border-radius: 8px; text-align: center;">
<h2 style="color: #ffffff; margin: 0 0 8px;">{{ title }}</h2>
<p style="color: #e0e7ff; margin: 0;">{{ message }}</p>
</div>
`,
},
],
});Was passiert:
- Seitenleiste — Der „Announcement"-Block erscheint in der Seitenleiste mit Ihrem SVG-Icon
- Arbeitsfläche — Beim Hineinziehen wird die Liquid-Vorlage mit den Standard-Feldwerten gerendert
- Toolbar — Beim Klicken auf den Block wird ein Eigenschafts-Panel mit Texteingaben und einem Farbwähler angezeigt
- Live-Vorschau — Das Bearbeiten eines Feldes rendert die Vorlage in Echtzeit neu
- Export — Beim Speichern wird die Liquid-Vorlage zu statischem HTML für den E-Mail-Versand gerendert
Block-Definition Referenz
Jeder benutzerdefinierte Block wird durch ein CustomBlockDefinition-Objekt definiert:
{
type: 'product-card', // Eindeutiger Bezeichner
name: 'Product Card', // Anzeigename in Seitenleiste/Toolbar
icon: '<svg>...</svg>', // SVG-String oder Bild-URL (optional)
description: 'A product card', // Tooltip/Beschreibung (optional)
fields: [ /* ... */ ], // Array von Felddefinitionen
template: '...', // Liquid-Vorlagen-String
}| Eigenschaft | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
type | string | Ja | Eindeutiger Bezeichner für diesen Blocktyp. Darf nicht mit integrierten Typen kollidieren: text, image, button, divider, spacer, social, video, html, section. |
name | string | Ja | Lesbarer Name, der in der Seitenleiste und im Toolbar-Header angezeigt wird. |
icon | string | Nein | SVG-Markup-String (inline gerendert) oder eine Bild-URL (als <img> gerendert). Fällt auf ein Standard-Block-Icon zurück, wenn weggelassen. |
description | string | Nein | Kurze Beschreibung als Tooltip oder Untertitel. |
fields | array | Ja | Array von Felddefinitionen, die die Toolbar-Steuerelemente und Template-Variablen bestimmen. |
template | string | Ja | Liquid-Vorlagen-String, der mit den Feldwerten gerendert wird. Muss E-Mail-kompatibles HTML erzeugen (Inline-Styles, keine nicht unterstützten Tags). |
Icon-Beispiele
SVG-String (empfohlen für scharfes Rendering):
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>',Bild-URL:
icon: 'https://your-cdn.com/icons/product-card.png',Liquid-Template-Syntax
Benutzerdefinierte Block-Templates verwenden Liquid — eine weit verbreitete Template-Sprache, bekannt durch Shopify. Die Templates werden clientseitig im Browser verarbeitet.
Variablen
Feldwerte mit doppelten geschweiften Klammern einfügen:
{{ fieldKey }}Bedingungen
Inhalte basierend auf Feldwerten ein- oder ausblenden:
{% if description %}
<p>{{ description }}</p>
{% endif %}
{% unless showPrice %}
<p>Kontaktieren Sie uns für Preise</p>
{% endunless %}
{% if layout == 'horizontal' %}
<table><tr>...</tr></table>
{% else %}
<div>...</div>
{% endif %}Schleifen
Über Repeatable-Felder iterieren:
{% for item in features %}
<li>{{ item.icon }} {{ item.text }}</li>
{% endfor %}Schleifenvariablen sind innerhalb des {% for %}-Blocks verfügbar:
{{ forloop.index }}— Aktuelle Iteration (1-basiert){{ forloop.index0 }}— Aktuelle Iteration (0-basiert){{ forloop.first }}—truebei der ersten Iteration{{ forloop.last }}—truebei der letzten Iteration{{ forloop.length }}— Gesamtanzahl der Einträge
Filter
Werte mit Pipe-Syntax transformieren:
{{ name | upcase }}
{{ description | truncate: 100 }}
{{ price | default: 'N/A' }}
{{ name | strip_html }}Sicherheit
Ihre Templates werden ohne Bereinigung gerendert — Sie haben die volle Kontrolle über die HTML-Ausgabe. Feldwerte, die von Ihren Benutzern eingegeben werden, erhalten automatisch <script>-Tags entfernt, um XSS-Injection zu verhindern.
Mehr erfahren
Liquid unterstützt viele weitere Funktionen, darunter Operatoren, erweiterte Filter und zusätzliche Tags. Die vollständige Sprachreferenz finden Sie in der offiziellen Liquid-Dokumentation.
Feldtypen
Felder definieren die Steuerelemente, die in der Toolbar angezeigt werden, wenn ein benutzerdefinierter Block ausgewählt ist. Jedes Feld wird einer Liquid-Template-Variable zugeordnet.
text
Einzeilige Texteingabe.
{
key: 'name',
type: 'text',
label: 'Product Name',
default: 'Product',
placeholder: 'Enter name',
required: true,
}Template-Verwendung: {{ name }}
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
key | string | Variablenname im Template |
label | string | Beschriftung über der Eingabe |
default | string | Standardwert bei Block-Erstellung |
placeholder | string | Platzhaltertext in der Eingabe |
required | boolean | Zeigt einen Pflichtfeld-Indikator an |
textarea
Mehrzeilige Texteingabe mit automatischer Größenanpassung.
{
key: 'description',
type: 'textarea',
label: 'Description',
default: '',
}Template-Verwendung: {{ description }}
image
URL-Eingabe mit Medienbibliothek-Integration. Beim Klicken öffnet sich die Medienbibliothek des SDKs zur Bildauswahl.
{
key: 'photo',
type: 'image',
label: 'Photo',
default: 'https://placehold.co/300x200',
}Template-Verwendung: <img src="{{ photo }}" alt="Foto" style="width: 100%; height: auto;" />
color
Farbwähler mit Hex-Texteingabe nebeneinander. Gibt einen Hex-Farbstring aus.
{
key: 'bgColor',
type: 'color',
label: 'Background Color',
default: '#007bff',
}Template-Verwendung: <div style="background: {{ bgColor }};">...</div>
number
Zahleneingabe mit optionalen min, max und step-Einschränkungen.
{
key: 'rating',
type: 'number',
label: 'Star Rating',
default: 5,
min: 1,
max: 5,
step: 1,
}Template-Verwendung: {{ rating }} von 5 Sternen
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
min | number | Minimal erlaubter Wert |
max | number | Maximal erlaubter Wert |
step | number | Schrittweite für die Eingabe |
default | number | Standardwert |
select
Dropdown mit vordefinierten Optionen.
{
key: 'layout',
type: 'select',
label: 'Layout',
options: [
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Vertical', value: 'vertical' },
],
default: 'horizontal',
}Template-Verwendung: {% if layout == 'horizontal' %}...{% endif %}
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
options | array | Array von { label, value }-Objekten |
default | string | Standard-Auswahlwert |
boolean
Kippschalter, der true oder false ausgibt.
{
key: 'showPrice',
type: 'boolean',
label: 'Show Price',
default: true,
}Template-Verwendung: {% if showPrice %}<p>{{ price }}</p>{% endif %}
repeatable
Ein Array von Unterfeld-Gruppen. Benutzer können Einträge hinzufügen, entfernen und neu anordnen. Unterstützt minItems und maxItems-Einschränkungen.
{
key: 'features',
type: 'repeatable',
label: 'Features',
fields: [
{ key: 'icon', type: 'text', label: 'Icon' },
{ key: 'text', type: 'text', label: 'Feature Text' },
],
default: [{ icon: '✓', text: 'Feature 1' }],
minItems: 1,
maxItems: 5,
}Template-Verwendung:
{% for f in features %}
<li>{{ f.icon }} {{ f.text }}</li>
{% endfor %}| Eigenschaft | Typ | Beschreibung |
|---|---|---|
fields | array | Unterfeld-Definitionen (jeder Typ außer repeatable) |
default | array | Standard-Einträge als Array von Objekten |
minItems | number | Mindestanzahl der Einträge |
maxItems | number | Maximale Anzahl der Einträge |
Repeatable-Felder
Repeatable-Felder eignen sich für Listen, Raster und Sammlungen. Hier ist eine vollständige Anleitung.
Ein Repeatable-Feld definieren
{
key: 'items',
type: 'repeatable',
label: 'Menu Items',
fields: [
{ key: 'name', type: 'text', label: 'Item Name', required: true },
{ key: 'price', type: 'text', label: 'Price' },
{ key: 'description', type: 'textarea', label: 'Description' },
{ key: 'isVegetarian', type: 'boolean', label: 'Vegetarian', default: false },
],
default: [
{ name: 'Starter', price: '$9.99', description: 'A delicious starter', isVegetarian: false },
],
minItems: 1,
maxItems: 10,
}Template mit Repeatable
<table style="width: 100%; border-collapse: collapse;">
{% for item in items %}
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px;">
<strong>{{ item.name }}</strong>
{% if item.isVegetarian %} 🌿{% endif %}
{% if item.description %}
<br /><span style="color: #666;">{{ item.description }}</span>
{% endif %}
</td>
<td style="padding: 12px; text-align: right; font-weight: bold;">
{{ item.price }}
</td>
</tr>
{% endfor %}
</table>Einschränkungen
minItems— Die Schaltfläche „Entfernen" ist deaktiviert, wenn die Anzahl der EinträgeminItemsentsprichtmaxItems— Die Schaltfläche „Hinzufügen" ist deaktiviert, wenn die Anzahl der EinträgemaxItemsentspricht- Verschachtelte Repeatables werden nicht unterstützt — ein Repeatable-Feld kann kein weiteres Repeatable-Feld enthalten
Beispiele
Produktkarte
Eine Produktkarte mit Bild, Details, Feature-Liste und CTA-Button.
{
type: 'product-card',
name: 'Product Card',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><line x1="3" x2="21" y1="6" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>',
description: 'Display a product with image, name, price, and CTA',
fields: [
{ key: 'image', type: 'image', label: 'Product Image', required: true },
{ key: 'name', type: 'text', label: 'Name', default: 'Product Name' },
{ key: 'price', type: 'text', label: 'Price', default: '$0.00' },
{ key: 'description', type: 'textarea', label: 'Description', default: '' },
{ key: 'ctaText', type: 'text', label: 'Button Text', default: 'Shop Now' },
{ key: 'ctaUrl', type: 'text', label: 'Button URL', default: '#' },
{ key: 'ctaColor', type: 'color', label: 'Button Color', default: '#007bff' },
{
key: 'features',
type: 'repeatable',
label: 'Features',
fields: [
{ key: 'icon', type: 'text', label: 'Icon' },
{ key: 'text', type: 'text', label: 'Feature Text' },
],
default: [{ icon: '✓', text: 'Free shipping' }],
maxItems: 5,
},
],
template: `
<div style="border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; font-family: Arial, sans-serif;">
<img src="{{ image }}" alt="{{ name }}" style="width: 100%; height: auto; display: block;" />
<div style="padding: 16px;">
<h3 style="margin: 0 0 8px; font-size: 18px; color: #333;">{{ name }}</h3>
{% if description %}
<p style="color: #666; margin: 0 0 8px; font-size: 14px;">{{ description }}</p>
{% endif %}
<p style="font-size: 24px; font-weight: bold; margin: 0 0 16px; color: #111;">{{ price }}</p>
{% if features %}
<ul style="list-style: none; padding: 0; margin: 0 0 16px;">
{% for f in features %}
<li style="padding: 4px 0; font-size: 14px; color: #555;">{{ f.icon }} {{ f.text }}</li>
{% endfor %}
</ul>
{% endif %}
<a href="{{ ctaUrl }}" style="background: {{ ctaColor }}; color: #ffffff; padding: 12px 24px; border-radius: 4px; text-decoration: none; display: inline-block; font-weight: bold;">{{ ctaText }}</a>
</div>
</div>
`,
}Veranstaltungskarte
Eine Veranstaltungskarte mit Datum, Veranstaltungsort, Programm und RSVP-Button. Verwendet ein Repeatable-Feld für die Agenda, Bedingungen um die Adresse nur bei Eingabe anzuzeigen, und forloop.last zur Steuerung der Trennlinien.
{
type: 'event-card',
name: 'Event Card',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="4" rx="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>',
description: 'Event details with date, venue, and RSVP',
fields: [
{ key: 'eventName', type: 'text', label: 'Event Name', default: 'Annual Conference', required: true },
{ key: 'date', type: 'text', label: 'Date', default: 'March 15, 2026' },
{ key: 'venue', type: 'text', label: 'Venue Name', default: 'Convention Center' },
{ key: 'venueAddress', type: 'text', label: 'Venue Address', default: '' },
{
key: 'schedule',
type: 'repeatable',
label: 'Schedule',
fields: [
{ key: 'time', type: 'text', label: 'Time' },
{ key: 'title', type: 'text', label: 'Session Title' },
],
default: [
{ time: '9:00 AM', title: 'Registration & Coffee' },
{ time: '10:00 AM', title: 'Keynote' },
{ time: '12:00 PM', title: 'Lunch Break' },
],
minItems: 1,
maxItems: 10,
},
{ key: 'rsvpUrl', type: 'text', label: 'RSVP URL', default: '#' },
{ key: 'accentColor', type: 'color', label: 'Accent Color', default: '#6366f1' },
],
template: `
<div style="border: 2px solid {{ accentColor }}; border-radius: 8px; overflow: hidden; font-family: Arial, sans-serif;">
<div style="background: {{ accentColor }}; padding: 16px; text-align: center;">
<h2 style="margin: 0; color: #ffffff; font-size: 20px;">{{ eventName }}</h2>
<p style="margin: 4px 0 0; color: #e0e7ff; font-size: 14px;">{{ date }}{% if venue %} · {{ venue }}{% endif %}</p>
</div>
<div style="padding: 16px;">
{% if venueAddress %}
<p style="color: #888; font-size: 13px; margin: 0 0 16px;">{{ venueAddress }}</p>
{% endif %}
{% if schedule %}
<table style="width: 100%; border-collapse: collapse;">
{% for item in schedule %}
<tr style="{% unless forloop.last %}border-bottom: 1px solid #eee;{% endunless %}">
<td style="padding: 8px 0; color: {{ accentColor }}; font-size: 13px; font-weight: bold; width: 90px;">{{ item.time }}</td>
<td style="padding: 8px 0; font-size: 14px; color: #333;">{{ item.title }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
<div style="text-align: center; padding-top: 16px;">
<a href="{{ rsvpUrl }}" style="background: {{ accentColor }}; color: #ffffff; padding: 12px 32px; border-radius: 4px; text-decoration: none; display: inline-block; font-weight: bold;">Jetzt anmelden</a>
</div>
</div>
</div>
`,
}Testimonial
Ein Testimonial mit Zitat, Autorendetails, Avatar und Sternebewertung. Verwendet das Zahlenfeld mit min/max und bedingtes Rendering für die Sterne.
{
type: 'testimonial',
name: 'Testimonial',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg>',
description: 'Customer testimonial with rating',
fields: [
{ key: 'quote', type: 'textarea', label: 'Quote', default: 'This product changed our workflow completely.', required: true },
{ key: 'authorName', type: 'text', label: 'Author Name', default: 'Jane Smith' },
{ key: 'authorTitle', type: 'text', label: 'Author Title', default: 'CEO, Acme Corp' },
{ key: 'avatar', type: 'image', label: 'Avatar', default: 'https://placehold.co/64x64' },
{ key: 'rating', type: 'number', label: 'Star Rating', default: 5, min: 1, max: 5, step: 1 },
{ key: 'showRating', type: 'boolean', label: 'Show Star Rating', default: true },
],
template: `
<div style="background: #f9fafb; border-radius: 8px; padding: 24px; font-family: Arial, sans-serif;">
{% if showRating %}
<div style="margin-bottom: 12px; font-size: 20px;">
{% if rating >= 1 %}★{% else %}☆{% endif %}
{% if rating >= 2 %}★{% else %}☆{% endif %}
{% if rating >= 3 %}★{% else %}☆{% endif %}
{% if rating >= 4 %}★{% else %}☆{% endif %}
{% if rating >= 5 %}★{% else %}☆{% endif %}
</div>
{% endif %}
<p style="font-size: 16px; line-height: 1.6; color: #374151; margin: 0 0 16px; font-style: italic;">„{{ quote }}"</p>
<table>
<tr>
<td style="vertical-align: middle; padding-right: 12px;">
<img src="{{ avatar }}" alt="{{ authorName }}" style="width: 48px; height: 48px; border-radius: 50%;" />
</td>
<td style="vertical-align: middle;">
<strong style="color: #111827; font-size: 14px;">{{ authorName }}</strong>
{% if authorTitle %}
<br /><span style="color: #6b7280; font-size: 13px;">{{ authorTitle }}</span>
{% endif %}
</td>
</tr>
</table>
</div>
`,
}Speichern und Aktualisieren
Wenn Ihr Benutzer ein Template speichert, wird die Liquid-Vorlage jedes benutzerdefinierten Blocks mit den aktuellen Feldwerten zu statischem HTML gerendert. Das exportierte HTML enthält die finale gerenderte Ausgabe — bereit für den E-Mail-Versand.
Das gespeicherte Template bewahrt auch die Feldwerte des Blocks, sodass Blöcke beim erneuten Laden des Templates vollständig bearbeitbar bleiben. Wenn Sie Ihre customBlocks-Konfiguration zwischen Sitzungen ändern, zeigen Blöcke, deren Definitionen nicht mehr registriert sind, einen „Definition nicht gefunden"-Platzhalter — die Daten bleiben jedoch erhalten. Das erneute Hinzufügen der Block-Definition stellt die Bearbeitung wieder her.
Tipps zur E-Mail-Kompatibilität
Da das HTML benutzerdefinierter Blöcke in der finalen E-Mail-Ausgabe enthalten ist, befolgen Sie diese Richtlinien für beste E-Mail-Client-Kompatibilität:
- Verwenden Sie Inline-Styles für alle Gestaltung (keine
<style>-Blöcke) - Verwenden Sie Tabellen für das Layout anstelle von Flexbox oder Grid
- Vermeiden Sie nicht unterstützte Tags wie verschachteltes
<div>in einigen Clients — testen Sie mit Ihren Ziel-E-Mail-Clients - Halten Sie Bilder in angemessener Größe und setzen Sie immer
widthundheight
Datenquellen
Plan Feature
Diese Funktion ist nur im Scale-Plan verfügbar.
Datenquellen ermöglichen es Ihnen, einen benutzerdefinierten Block mit externen Inhalten zu verbinden — Produkte, Artikel, Veranstaltungen oder beliebige Daten aus Ihrer Anwendung. Anstatt jedes Feld manuell auszufüllen, wählen Ihre Benutzer einen Eintrag über Ihre eigene Oberfläche aus, und die Felder werden automatisch befüllt.
Eine Datenquelle ist optional für jede Block-Definition. Wenn vorhanden, wird der Block mit einem Call-to-Action-Overlay gerendert, das den Benutzer zum Laden von Inhalten auffordert. Nach dem Laden werden Felder mit readOnly-Markierung auf die zurückgegebenen Werte gesperrt, während andere Felder bearbeitbar bleiben.
So funktioniert es
- Block hinzugefügt — Der Block wird mit Standard-Feldwerten und einem CTA-Overlay gerendert
- Benutzer klickt „Inhalt laden" — Ihr
onFetch-Callback wird ausgelöst und öffnet Ihre eigene Auswahl-Oberfläche - Daten zurückgegeben — Das SDK ordnet die zurückgegebenen Schlüssel den passenden Feldschlüsseln zu und befüllt die Werte
- Schreibgeschützte Felder werden gesperrt — Felder mit
readOnly: truewerden mit einem Schloss-Symbol deaktiviert - Bearbeitbare Felder bleiben offen — Felder ohne
readOnly(z. B. Button-Text) bleiben bearbeitbar - „Ändern"-Button — Nach dem ersten Laden ermöglicht ein „Ändern"-Button in der Toolbar ein erneutes Laden
Konfiguration
Fügen Sie ein dataSource-Objekt zu Ihrer Block-Definition hinzu:
{
type: 'product-card',
name: 'Product Card',
fields: [ /* ... */ ],
template: '...',
dataSource: {
label: 'Produkt auswählen',
onFetch: async ({ fieldValues, blockId }) => {
const product = await showProductPicker();
if (!product) return null;
return { name: product.title, price: product.price };
},
},
}| Eigenschaft | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
label | string | Ja | Text auf dem Lade-Button und dem CTA-Overlay |
onFetch | function | Ja | Asynchroner Callback, der Felddaten oder null zum Abbrechen zurückgibt |
Der onFetch-Callback
Der onFetch-Callback erhält ein Kontextobjekt und muss entweder ein Datenobjekt oder null zurückgeben:
interface DataSourceFetchContext {
fieldValues: Record<string, unknown>;
blockId: string;
}
interface DataSourceConfig {
label: string;
onFetch: (context: DataSourceFetchContext) => Promise<Record<string, unknown> | null>;
}fieldValues— Eine Kopie der aktuellen Feldwerte des Blocks, nützlich wenn Ihre Auswahl-Oberfläche Kontext benötigtblockId— Die eindeutige Block-Instanz-ID- Rückgabewert — Ein Objekt, dessen Schlüssel mit den Feldschlüsseln übereinstimmen. Nur übereinstimmende Schlüssel werden zugeordnet; zusätzliche Schlüssel werden ignoriert
nullzurückgeben — Bricht den Ladevorgang ab. Keine Felder werden geändert und der Block bleibt unverändert
Die readOnly-Feldeigenschaft
Fügen Sie readOnly: true zu einer Felddefinition hinzu, um es nach einem Datenquellen-Ladevorgang zu sperren:
fields: [
{ key: 'name', type: 'text', label: 'Name', default: 'Produkt', readOnly: true },
{ key: 'price', type: 'text', label: 'Preis', default: '0,00 €', readOnly: true },
{ key: 'ctaText', type: 'text', label: 'Button-Text', default: 'Jetzt kaufen' },
]Schreibgeschütztes Verhalten:
- Wird nur durchgesetzt, wenn der Block eine
dataSourcehat und Daten geladen wurden - Vor dem ersten Laden sind alle Felder bearbeitbar, damit Benutzer Standardwerte setzen können
- Wenn aktiv, wird die Feldeingabe mit reduzierter Deckkraft, einem Schloss-Symbol und einem Tooltip deaktiviert
- Funktioniert mit allen Feldtypen einschließlich
repeatable(deaktiviert Hinzufügen/Entfernen und sperrt verschachtelte Felder)
INFO
Hinweis: Die Verwendung von readOnly bei einem Feld ohne dataSource in der Block-Definition hat keine Auswirkung. Das SDK protokolliert in diesem Fall eine Warnung in der Konsole, um Konfigurationsfehler zu erkennen.
Fehlerbehandlung
- Wenn
onFetchnullzurückgibt, wird der Vorgang als abgebrochen behandelt — keine Felder werden geändert - Wenn
onFetcheine Ausnahme auslöst, wird der Fehler abgefangen und mit einem[Templatical]-Präfix in der Konsole protokolliert. Der Block bleibt in seinem aktuellen Zustand
Vollständiges Beispiel
Eine Produktkarte, die Produktdaten aus Ihrer Anwendung lädt:
customBlocks: [
{
type: 'product-card',
name: 'Product Card',
fields: [
{ key: 'name', type: 'text', label: 'Name', default: 'Produkt', readOnly: true },
{ key: 'price', type: 'text', label: 'Preis', default: '0,00 €', readOnly: true },
{ key: 'image', type: 'image', label: 'Bild', readOnly: true },
{ key: 'ctaText', type: 'text', label: 'Button-Text', default: 'Jetzt kaufen' },
],
template: `
<div style="border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; font-family: Arial, sans-serif;">
<img src="{{ image }}" alt="{{ name }}" style="width: 100%; height: auto; display: block;" />
<div style="padding: 16px;">
<h3 style="margin: 0 0 8px; font-size: 18px; color: #333;">{{ name }}</h3>
<p style="font-size: 24px; font-weight: bold; margin: 0 0 16px; color: #111;">{{ price }}</p>
<a href="#" style="background: #007bff; color: #ffffff; padding: 12px 24px; border-radius: 4px; text-decoration: none; display: inline-block; font-weight: bold;">{{ ctaText }}</a>
</div>
</div>
`,
dataSource: {
label: 'Produkt auswählen',
onFetch: async ({ fieldValues, blockId }) => {
// Eigene Produktauswahl-Oberfläche öffnen
const product = await showProductPicker();
// null zurückgeben zum Abbrechen
if (!product) return null;
// Objekt mit Schlüsseln zurückgeben, die Ihren Feldschlüsseln entsprechen
return {
name: product.title,
price: `${product.price.toFixed(2)} €`,
image: product.imageUrl,
};
},
},
},
]In diesem Beispiel:
name,priceundimagesindreadOnly— sie werden nach dem Laden der Produktdaten gesperrtctaTextist nichtreadOnly— der Benutzer kann den Button-Text auch nach dem Laden anpassen- Der
onFetch-Callback öffnet eine Produktauswahl, und die zurückgegebenen Schlüssel (name,price,image) werden automatisch den passenden Feldern zugeordnet
Fehlerbehebung
Das SDK validiert Ihre Block-Definitionen bei der Initialisierung und protokolliert Warnungen in der Browser-Konsole, wenn etwas nicht stimmt — zum Beispiel doppelte type-Werte, Feldschlüssel-Konflikte oder ein leeres template. Ungültige Definitionen werden übersprungen, während gültige weiterhin normal registriert werden.
Wenn ein Block einen „Definition nicht gefunden"-Platzhalter anzeigt, wurde der Block mit einem type gespeichert, der nicht in Ihrer aktuellen customBlocks-Konfiguration vorhanden ist. Stellen Sie sicher, dass Sie bei jeder Initialisierung des Editors die gleichen Block-Definitionen übergeben.
Wenn ein Block einen roten Fehler anzeigt, liegt ein Syntaxfehler in Ihrer Liquid-Vorlage vor. Überprüfen Sie die Browser-Konsole auf die spezifische Fehlermeldung und korrigieren Sie den Template-String.