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.
parentmuss 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 überparent_idmitON 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.