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 ​