Zum Inhalt

Verschachtelte Datensätze über Sections

Neben der Detail-Tabelle (kc:childTable) gibt es in KCForms eine zweite, gleichwertige Art, verschachtelte Datensätze (1:n, 1:n:m) abzubilden: Section-Datasets. Beide nutzen im Hintergrund dieselbe Datenbank-Struktur und dasselbe Datenformat — sie unterscheiden sich in der Darstellung und in der Schreibweise im Schema.

Worum geht es?

Eine Detail-Tabelle zeigt eine kompakte Tabelle mit einer Zeile pro Datensatz — ideal für kurze, tabellarische Daten (Bestellpositionen, Belege).

Hat ein Unter-Datensatz viele Felder oder selbst wieder Unter-Datensätze, wird eine Tabelle unübersichtlich. Dann helfen Section-Datasets: Jeder Datensatz erscheint als eigener Akkordeon-Block, in dem die Felder wie ein eingebettetes Formular stehen. Unter-Datensätze erscheinen rekursiv im jeweiligen Block.

Wann nutzt man welches?

Kriterium kc:childTable Section-Dataset
Darstellung tabellarisch (Spalten) Formular (Felder im Block)
Datensätze je Kopf viele, kompakt wenige bis mittlere Anzahl
Felder je Datensatz wenige (3–6 Spalten) beliebig viele
Unter-Datensätze möglich, eng rekursiv, gut lesbar
Schreibweise im Schema type: array + kc:childTable flach, Abschnitte mit kc:dataset

Faustregel: viele Datensätze mit wenigen Spalten → Detail-Tabelle. Wenige Datensätze mit vielen Feldern oder tiefer Verschachtelung → Section-Datasets.

Detail-Tabelle als Blöcke

Auch eine Detail-Tabelle kann jede Zeile als Block zeigen (layout: "block", siehe Detail-Tabellen). Der Unterschied: Bei Section-Datasets stehen die Felder flach im Schema und lassen sich mit Abschnitten, Leittexten und eigenem Raster frei gestalten.

Schema-Beispiel

Das Beispielformular Kosten im Wirtschaftsjahr (Section-Datasets) zeigt eine 1:n:m-Struktur:

{
  "type": "object",
  "title": "Kosten im Wirtschaftsjahr (Section-Datasets)",
  "properties": {
    "_secKopf": {
      "type": "null", "title": "Kopfdaten",
      "kc:section": true, "kc:sectionLevel": 2, "kc:sectionStyle": "spacing"
    },
    "wirtschaftsjahr": { "type": "integer", "title": "Wirtschaftsjahr",
                         "kc:defaultValue": 2026 },
    "gesamt": { "type": "number", "title": "Gesamt im Wirtschaftsjahr",
                "kc:readonly": true, "kc:decimals": 2,
                "kc:numberSuffix": " EUR", "kc:align": "right" },

    "_secStellen": {
      "type": "null", "title": "Kostenstellen",
      "kc:section": true, "kc:sectionLevel": 2, "kc:sectionStyle": "line",
      "kc:dataset": {
        "name": "kostenstellen",
        "initialRows": 3,
        "titleField": "titel"
      }
    },
    "titel": { "type": "string", "title": "Titel", "kc:maxLength": 40 },
    "summe": { "type": "number", "title": "Summe", "kc:readonly": true,
               "kc:decimals": 2, "kc:numberSuffix": " EUR", "kc:align": "right" },

    "_secPositionen": {
      "type": "null", "title": "Kostenpositionen",
      "kc:section": true, "kc:sectionLevel": 3, "kc:sectionStyle": "spacing",
      "kc:dataset": {
        "name": "kostenpositionen",
        "parent": "kostenstellen",
        "initialRows": 2
      }
    },
    "wert": { "type": "number", "title": "Wert", "kc:decimals": 2,
              "kc:numberSuffix": " EUR", "kc:align": "right" }
  }
}

Stück für Stück

Element Bedeutung
_secKopf Abschnitt ohne Dataset — gruppiert nur optisch. type: null zeigt, dass der Eintrag keinen Wert trägt.
wirtschaftsjahr, gesamt Felder des Kopf-Datensatzes, direkt in ctx.form.
_secStellen mit kc:dataset Beginn eines Datasets: Alle folgenden Felder bis zum nächsten Abschnitt gehören zu kostenstellen.
wert Feld des Unter-Datasets, zu finden unter ctx.form.kostenstellen[i].kostenpositionen[j].wert.

Die Schlüssel in kc:dataset:

Schlüssel Bedeutung
name Name des Datasets — Pfad in ctx.form und Teil des Tabellennamens
parent Name des übergeordneten Datasets; macht daraus einen Unter-Datensatz je Elternzeile
initialRows so viele leere Datensätze erhält ein neuer Kopf-Datensatz
keepEmptyRows true = komplett leere Datensätze beim Speichern behalten (Standard: verwerfen)
titleField Feld, dessen Wert als Überschrift des Blocks erscheint
rowHeaderMode hidden = keine Überschrift (nur Auf-/Zu-Pfeil), custom = eigener Text
rowHeaderTemplate eigener Überschriftstext mit Platzhaltern, z. B. ${itemLabel} #${index}: ${row.kostenposition}
addButtonLabel Beschriftung des Knopfs „+ Eintrag hinzufügen"

Das Raster der Felder im Block steuert kc:sectionLayout am Abschnitt (vertical, horizontal-2, horizontal-3; leer = vom Formular erben).

Im Formular-Editor

In den Eigenschaften eines Abschnitts wählst du, ob er nur optisch trennt (Master — nur optische Trenn-Section) oder ein eigenes Dataset beginnt (Sub-Dataset — eigene 1:n-Tabelle). Für ein Dataset stellst du dort Name, übergeordnetes Dataset, Überschrift der Blöcke, Anfangs-Datensätze, „Leere Zeilen beim Speichern beibehalten", die Knopfbeschriftung und das Spaltenraster der Sub-Felder ein.

Im Ausfüllmodus

  • Jeder Datensatz erscheint als Akkordeon-Block; ein Klick auf die Überschrift klappt ihn auf oder zu.
  • Unter den Blöcken steht + Eintrag hinzufügen (oder der eigene Text).
  • Unter-Datensätze erscheinen im jeweiligen Block, eine Ebene eingerückt.
  • Wird ein Datensatz gelöscht, fallen seine Unter-Datensätze mit weg (in der Datenbank per ON DELETE CASCADE, wie bei Detail-Tabellen).

Skripte

Section-Datasets verhalten sich im Skript wie Detail-Tabellen.

Schreiben über ctx.form

Werte ändert ein Skript immer direkt im verschachtelten ctx.form-Baum:

// Summe je Kostenstelle und Gesamtsumme
var stellen = ctx.form.kostenstellen || [];
var gesamt = 0;
for (var i = 0; i < stellen.length; i++) {
  var stelle = stellen[i];
  if (!stelle) continue;
  var sub = 0;
  var positionen = stelle.kostenpositionen || [];
  for (var j = 0; j < positionen.length; j++) {
    if (positionen[j]) sub += kc.num(positionen[j].wert);
  }
  stelle.summe = sub;
  gesamt += sub;
}
ctx.form.gesamt = gesamt;

Pfade: ctx.form.<dataset>[i].<feld> bzw. ctx.form.<dataset>[i].<unterDataset>[j].<feld>.

Lesen über ctx.model

Zusätzlich baut KCForms ctx.model.children auf: je Dataset ein DataSet, je Datensatz eine Row mit elements (Werte) und subordinates (Unter-Datasets):

var ds = ctx.model.children.kostenstellen;
if (ds && ds.rows.length > 0) {
  var titel = ds.rows[0].elements.titel.value;
  // Unter-Datasets: ds.rows[0].subordinates.kostenpositionen.rows[k].elements.wert.value
}

ctx.model ist eine Lese-Sicht

Schreibvorgänge über ctx.model landen nicht in ctx.form und werden nicht gespeichert. Geschrieben wird immer über ctx.form.

initialRows auch ohne Oberfläche

Die leeren Anfangs-Datensätze legt auch das Backend an — also auch dann, wenn ein neuer Datensatz ohne Web-Formular entsteht. Hat onLoad bereits eine Liste geschrieben, bleibt sie unverändert.

Hinweise

  • Reihenfolge zählt. Ein Abschnitt gilt ab seiner Position bis zum nächsten Abschnitt. parent muss auf ein weiter oben deklariertes Dataset zeigen.
  • Nicht mischen. Ein Formular sollte entweder Detail-Tabellen oder Section-Datasets für seine verschachtelten Daten verwenden, nicht beides.
  • Datenbank: Je Dataset eine Tabelle form_<formid>_<datasetname>, verknüpft über parent_id mit ON DELETE CASCADE — wie bei Detail-Tabellen.
  • Listen-Export (CSV/XLSX) funktioniert unverändert, weil das Datenformat gleich ist.

Beispiel zum Anschauen

Im Katalog Service, Ordner Demos/1zunzum, gibt es zwei Varianten desselben Formulars (installierbar unter System-Administration → Formular-Bausteine → Beispiele):

Formular-ID Darstellung Schema
kosten-im-wirtschaftsjahr Tabelle verschachteltes kc:childTable
kosten-im-wirtschaftsjahr-sections Akkordeon flach mit kc:dataset

Beide rechnen mit demselben Skript — sie unterscheiden sich nur in der Darstellung.