Skip to content

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 CaseItems?
USP-Liste (Icon + Titel + Text, n×)ja
FAQ-Eintraegeja
Testimonialsja
Image-Galerieja
Single-Hero (Titel + Bild)nein — Top-Level-Felder reichen
Blog-Listing aus DBnein — 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

json
{
  "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"}
      ]
    }
  ]
}
FeldPflichtZweck
typejaimmer "items"
placeholderneinLabel der Items-Section im Properties-Panel
menujaSub-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:

sql
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.

php
$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):

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 — die position-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:

vue
<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:

AktionHTTP-MethodeAction
Item anlegen/speichernPOSTsave_widget_item (gegen /api/backend/pagebuilder)
Items umsortierenPOSTsort_widget_items
Item loeschenDELETEwidget_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 = id oder base_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