Widget Items / Repeater
Widgets koennen beliebig viele Sub-Items tragen — Listen gleichartiger Eintraege, die der Redakteur im Properties-Panel hinzufuegt, sortiert und loescht. Das Pattern nutzt den Feldtyp items im widget_menu und speichert die Daten in der separaten Tabelle page_widget_item. Diese Seite erklaert Definition, Persistierung und Rendering.
Wofuer Items verwenden?
| Use Case | Items? |
|---|---|
| USP-Liste (Icon + Titel + Text, n×) | ja |
| FAQ-Eintraege | ja |
| Testimonials | ja |
| Image-Galerie | ja |
| Single-Hero (Titel + Bild) | nein — Top-Level-Felder reichen |
| Blog-Listing aus DB | nein — ueber getWidgetContent() laden, keine Editor-Items |
Items sind fuer Inhalte, die der Redakteur im Pagebuilder selbst pflegt. Wenn Inhalte aus einer externen Tabelle kommen (Blog, Shop), ist getWidgetContent() der richtige Weg — siehe getWidgetContent().
Items-Definition im widget_menu
{
"widget_title": "USP Icon List",
"widget_icon": "icon.png",
"widget_template": "uspIconList",
"widget_menu": [
{
"type": "items",
"placeholder": "Icon-Boxes",
"menu": [
{"row": "title", "type": "text", "placeholder": "Titel"},
{"row": "subtitle", "type": "text", "placeholder": "Icon-Klasse (fa-...)"},
{"row": "text", "type": "textarea", "placeholder": "Beschreibung"}
]
}
]
}| Feld | Pflicht | Zweck |
|---|---|---|
type | ja | immer "items" |
placeholder | nein | Label der Items-Section im Properties-Panel |
menu | ja | Sub-Feldtypen — dieselbe Struktur wie widget_menu oben |
items selbst hat kein row — die Sub-Feld-Werte landen in der separaten page_widget_item-Tabelle, nicht in einer Spalte von page_row_col_widget.
Das Sub-menu-Array akzeptiert dieselben Feldtypen wie das aeussere widget_menu (text, textarea, filemanager, select statisch/DB). Nur ein Item kann nicht wieder Sub-Items haben — Items sind nicht verschachtelbar.
Die Tabelle page_widget_item
Items werden pro Widget-Instanz gespeichert, mit Multi-Language- und Sortier-Support:
CREATE TABLE page_widget_item (
id INT AUTO_INCREMENT PRIMARY KEY,
widget_id INT NOT NULL, -- FK → page_row_col_widget.base_id
base_id INT, -- fuer Sprachvarianten (base = Hauptsprache)
language_short VARCHAR(5),
position INT DEFAULT 0, -- fuer Drag & Drop-Sortierung
title VARCHAR(255),
subtitle VARCHAR(255),
text MEDIUMTEXT,
link VARCHAR(500),
image VARCHAR(500), -- JSON mit Media-ID
image2 VARCHAR(500), -- nur in page_widget_item!
logo VARCHAR(500),
pname VARCHAR(255), -- nur in page_widget_item!
dateStart DATETIME, -- nur in page_widget_item!
dateEnd DATETIME -- nur in page_widget_item!
);Spalten nur in page_widget_item
image2, pname, dateStart, dateEnd gibt es ausschliesslich hier, nicht im Top-Level-page_row_col_widget. Verwendung in der aeusseren widget_menu-Liste (ohne items-Wrapper) wirft keine Fehler, aber der Wert wird nicht gespeichert.
Widget-ID vs. Base-ID
Fuer das Laden der Items braucht das PHP-Widget die Base-ID des Widgets (bei Multi-Language Instanzen ist das die ID der Hauptsprach-Instanz). Sie liegt in $widget_array['item_id'] oder $widget_array['base_id'] — beide sind Aliase.
$widgetBaseId = (int) ($widget_array['item_id'] ?? $widget_array['base_id'] ?? 0);Items in getWidgetContent() laden
Standard-Pattern mit Multi-Language-Fallback (aus uspIconList/bootstrap.php):
public static function getWidgetContent($widget_array, $backend = false)
{
$widget_content_array = [];
if ($backend) return $widget_content_array;
$baseLanguage = $GLOBALS['baseLanguage'];
$currentLanguage = $GLOBALS['current_language'];
$widgetBaseId = (int) $widget_array['item_id'];
// Aktuelle Sprache
$q = query("SELECT * FROM page_widget_item
WHERE widget_id = $widgetBaseId
AND language_short LIKE '$currentLanguage'
ORDER BY position ASC");
// Fallback auf Hauptsprache, wenn aktuelle Sprache leer
if (is_current_lang_main($currentLanguage) != 1 && num_rows($q) == 0) {
$q = query("SELECT * FROM page_widget_item
WHERE widget_id = $widgetBaseId
AND language_short LIKE '$baseLanguage'
ORDER BY position ASC");
}
$items = [];
while ($row = fetch_assoc($q)) {
$items[] = [
'title' => rawurldecode($row['title']),
'icon' => rawurldecode($row['subtitle']),
'text' => rawurldecode($row['text']),
];
}
$widget_content_array['items'] = $items;
return $widget_content_array;
}Echte Quelle: _public/extensions/core/backend/uspIconList/widgets/uspIconList/bootstrap.php.
Wichtige Punkte
ORDER BY position ASC— dieposition-Spalte wird durch Drag & Drop im Editor gesetzt. Ohne ORDER erscheinen Items in Insert-Reihenfolge (meistens falsch).rawurldecode()auf Text-Feldern — der Pagebuilder speichert Strings URL-encoded, um HTML-Entities sauber durch die Layer zu bringen. Ohne Decode bekommt das Vue-Template z. B.Hello%20World.files::loadURL($row['image'])fuer Bild-Felder — aus der Media-ID wird eine absolute URL.- Multi-Language: erst aktuelle Sprache, dann Fallback auf
baseLanguage. Das vermeidet leere Widgets in Unter-Sprachen, wenn nur die Hauptsprache gepflegt ist.
Items im Vue-Template rendern
Das Widget-Template bekommt die Items unter widget.content.items:
<template>
<div class="usp-icon-list">
<div v-for="item in widget.content?.items" :key="item.title" class="usp-box">
<i :class="['fa', item.icon]"></i>
<h3>{{ item.title }}</h3>
<div v-html="item.text"></div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
widget: {
data?: Record<string, any>
content?: { items?: Array<{ title: string; icon: string; text: string }> }
}
}>()
</script>Key fuer v-for: Konventionell item.id oder item.title. Wenn mehrere Items denselben Titel tragen koennen, besser eine eindeutige Kombination oder im PHP vor Rueckgabe $item['_key'] = 'item_' . $row['id'] setzen.
CRUD-API fuer Items
Item-Management laeuft ueber das Pagebuilder-Backend:
| Aktion | HTTP-Methode | Action |
|---|---|---|
| Item anlegen/speichern | POST | save_widget_item (gegen /api/backend/pagebuilder) |
| Items umsortieren | POST | sort_widget_items |
| Item loeschen | DELETE | widget_item |
Details zu den Pagebuilder-API-Actions liegen in der API Reference — das Pagebuilder-Properties-Panel ruft diese Actions automatisch auf, wenn der Redakteur Items hinzufuegt, sortiert oder loescht.
Multi-Language-Handling
Items mit mehreren Sprachen folgen dem gleichen base_id/language_short-Pattern wie andere Core-Tabellen:
- Hauptsprache:
base_id = idoderbase_id IS NULL,language_short = 'de'(oder eingestellte Basissprache) - Sprachvariante:
base_id = {Hauptsprach-Item-ID},language_short = 'en', …
Der Pagebuilder erzeugt beim Speichern einer Uebersetzung automatisch einen Varianten-Record mit gesetztem base_id. Beim Laden im getWidgetContent() einfach die passende language_short-Zeile holen (siehe Code-Beispiel oben) — keine manuelle Merge-Logik noetig.
Items in Hauptsprache strukturieren, Varianten nur uebersetzen
position, image, image2 und andere strukturelle Felder werden oft nur in der Hauptsprache gesetzt — Sprachvarianten erben die Struktur. Pragma: das Widget-PHP liest position direkt aus der Sprachvarianten-Zeile und erwartet dort die korrekte Sortierung. Ein gut gepflegter Redakteur kopiert die Items in die Sprachvariante mit identischer Reihenfolge.
Haeufige Fehler
ORDER BY position ASC vergessen
Ohne explizite Sortierung ist die Reihenfolge von SELECT * FROM page_widget_item nicht garantiert. In der Praxis kommen Items in Insert-Reihenfolge — Drag & Drop-Umsortierung wirkt sich dann nur teilweise aus.
$widget_array['widget_id'] statt item_id
widget_id ist die Row-ID der Widget-Instanz (eine pro Platzierung auf einer Seite) — nicht die Base-ID, die fuer Items gebraucht wird. Fuer Item-Queries item_id oder base_id nutzen.
rawurldecode vergessen
Title/Text/Subtitle sind URL-encoded gespeichert. Ohne Decode landen Entities wie %20, %C3%A4 ungefiltert im Vue-Template.
Items im falschen Table-Namen
Repeater-Items landen immer in page_widget_item — nicht in einer plugin-eigenen Tabelle. Wer eine eigene Tabelle will (z. B. weil der Repeater 20 Felder braucht): Plugin-eigenes table-Construct bauen und per backend/item-API verwalten. Siehe Plugins › Content Constructs.
Items nicht verschachtelbar
Ein items-Feld kann nicht wieder ein items-Unterfeld im menu haben. Fuer 2-stufige Repeater muss man entweder ein zweites Widget daneben stellen oder die Unter-Struktur als JSON in einem text-Feld ablegen (und clientseitig parsen).
Siehe auch
- widget_menu-Feldtypen —
itemsals Feldtyp - getWidgetContent() — Items laden + Multi-Language-Fallback
- Widget-Anatomie — wo
$widget_array['item_id']herkommt - Plugins › Content Constructs — Alternative mit eigener Tabelle
- Pagebuilder-API fuer Items: API Reference Sektion
backend/pagebuilder(save_widget_item,sort_widget_items,widget_item)