Zum Inhalt

Datenelemente: Werte, Typen, Zeilen

Jedes Feld, das du im Formular-Editor anlegst, ist ein Datenelement. Im Skript begegnet es dir ständig: Du liest seinen Wert, rechnest damit, schreibst etwas zurück, prüfst ihn oder gehst über alle Felder eines Formulars.

Diese Seite zeigt das Handwerkszeug dafür — von „Wie komme ich an den Wert?" bis „Wie merke ich, dass sich etwas geändert hat?". Ohne Vorwissen, in kleinen Schritten. Die Sprache selbst steht nebenan unter JavaScript-Grundlagen, Objekte und Listen unter JSON und Objekte verstehen.

Zum Mitmachen

Alle Beispiele laufen in der Skript-Konsole: System-Administration → Werkzeuge & Diagnose → Skript-Konsole. Beispiel hineinkopieren, Ausführen klicken, unten die Ausgabe lesen. log.info(...) schreibt eine Zeile in diese Ausgabe.


Ein Datenelement hat zwei Seiten

Stell dir ein Feld wie ein beschriftetes Fach in einem Schrank vor. Es gibt

  • das Fach selbst: Es heißt betrag, trägt die Beschriftung „Rechnungsbetrag", nimmt nur Zahlen auf und ist vielleicht ein Pflichtfeld. Das ändert sich nicht, solange niemand das Formular umbaut.
  • den Inhalt: gerade steht 249.90 darin. Der wechselt bei jedem Datensatz.

Im Skript sind das zwei verschiedene Zugänge — und das ist die wichtigste Unterscheidung auf dieser Seite:

Ich möchte … … dafür nimmst du
den Wert lesen oder ändern ctx.form.<name>
wissen, was für ein Feld das ist (Typ, Beschriftung, Pflicht) ctx.model.primary.elements.<name>
über alle Felder laufen ctx.model.primary.elements
Zeilen einer Detail-Tabelle bearbeiten ctx.form.<tabellenfeld>
einen Fehler an ein Feld hängen field.<name>.error(...)

Merksatz: ctx.form ist der Inhalt, ctx.model ist die Beschriftung des Fachs. Geschrieben wird immer in ctx.form — ctx.model ist nur zum Nachschlagen da.


1. Den Wert lesen und schreiben

Der Wert steht in ctx.form, unter dem Namen des Feldes:

// lesen
var nachname = ctx.form.name;

// schreiben
ctx.form.ergebnis = 'Hallo ' + nachname;

log.info(ctx.form.ergebnis);      // Hallo Berg

Was du in ctx.form schreibst, übernimmt das Formular anschließend sichtbar — genau so baut man Berechnungen, Vorbelegungen und automatisch gefüllte Felder.

Drei Schreibweisen für dasselbe

KCForms bietet dir drei Wege zum selben Wert. Sie sind gleichwertig; nimm den, der sich für dich am besten liest:

ctx.form.name     = 'Berg';                   // ausführlich
$vorname          = 'Anna';                   // Kurzform mit $
field.email.value = 'a.berg@example.com';     // Feld-Handle

log.info($name + ', ' + ctx.form.vorname + ', ' + field.email.value);
Schreibweise Wann sie sich lohnt
ctx.form.betrag immer richtig
$betrag kürzeste Form für schnelle Rechnungen
field.betrag.value wenn du am selben Feld auch Fehler meldest (siehe Abschnitt 8)

Wenn der Feldname erst zur Laufzeit feststeht

Alle drei Schreibweisen können den Namen auch aus einer Variablen nehmen — eckige Klammern statt Punkt:

var summe = 0;
for (var i = 1; i <= 2; i++) {
  summe += $["wert" + i].num;            // Kurzform
}
log.info("Summe: " + kc.numFormat(summe, null, null, 0, 2));

var feld = "summe";
ctx.form[feld]  = summe;                 // ausführlich
field[feld].value = summe;               // Feld-Handle

Das ist der übliche Weg bei durchnummerierten Feldern — beitrag1 bis beitrag12, position1 bis position5 und so weiter.

Ein Name, den es nicht gibt, bricht ab

$["beitrga1"] meldet sofort $["beitrga1"]: Es gibt kein Datenelement mit diesem Namen. — genauso laut, wie sich $beitrga1 als ReferenceError meldet. Das ist Absicht: Eine Schleife, die einen Schritt zu weit läuft, würde sonst ab da still mit 0 weiterrechnen, und das fällt erst am Ergebnis auf.

Willst du bewusst prüfen, ob es ein Feld gibt, geht das ohne Abbruch:

log.info("wert1 gibt es: " + ("wert1" in $));
log.info("quatsch gibt es: " + ("quatsch" in $));

Oder mit ctx.form["quatsch"] — das liefert undefined statt eines Fehlers.


2. Die häufigste Stolperfalle: Text sieht aus wie eine Zahl

Alles, was aus dem Browser kommt, ist erst einmal Text — auch dann, wenn im Schema number steht. Rechnet man damit direkt, klebt JavaScript die Texte aneinander, statt zu addieren:

ctx.form.menge = '3';
ctx.form.preis = '4';

log.info('Falsch:  ' + ($menge + $preis));          // 34
log.info('Richtig: ' + ($menge.num + $preis.num));  // 7

Deshalb: vor jeder Rechnung .num anhängen. Das verkraftet auch leere Felder (0), deutsche Kommazahlen ("1.234,56") und Unsinn (0 statt Absturz).

ctx.form.number = '1.234,56';

log.info('Als Text: ' + $number);       // 1.234,56
log.info('Als Zahl: ' + $number.num);   // 1234.56

.num gibt es an jedem deklarierten Feld — als $feld.num und als field.feld.num. Details und die Begründung, warum der Wert überhaupt Text ist, unter kc-Helpers und im ctx.form-Abschnitt.

Wann kc.num(...)

.num gibt es nur an deklarierten Feldern und nur in Skripten. Überall sonst macht kc.num(...) dasselbe und ist dort die einzige Möglichkeit: für Zwischenergebnisse, Werte aus db.query(…), Zeilen einer Detail-Tabelle oder CSV-Datei — und in Bedingungen (kc:visibleIf, Regeln) sowie in kc:calc.

Nicht an ctx.form.<feld> hängen: Ein nie ausgefülltes Feld ist dort null, und null.num bricht ab. Ausgeschrieben also kc.num(ctx.form.betrag).

Und wieder zurück: die Zahl anzeigen

.num ist der Weg zum Rechnen. Für den Rückweg — Zahl zu Text, so wie sie ein Mensch lesen soll — gibt es .numFormat(…):

log.info($betrag.numFormat(null, "€", 12, 2, ",", ".", 3));   // 12.345,76 €
log.info($betrag.numFormat("EUR", null, 0, 2));               // EUR 12345,76

Die Reihenfolge der sieben Angaben ist prefix, suffix, stellenInsgesamt, anzNachkommastellen, dezimaltrenner, separatorZeichen, separatorBlockabstand; alle sind optional, null überspringt eine Position. Die vollständige Beschreibung samt Obergrenze und Rundung steht unter .numFormat.

Warum nicht toFixed(2)

toFixed ist JavaScript und kennt nur den Punkt als Dezimaltrenner — aus 1234.5 wird "1234.50", und für einen deutschen Beleg steht dann ein englisch geschriebener Betrag im Formular. Der übliche Ausweg .toFixed(2).replace(".", ",") bekommt zwar das Komma hin, aber keine Tausendergruppen, und er wirft bei einem Feldwert aus dem Browser einen Fehler: toFixed gibt es nur an einer Zahl, und dort steht Text.


3. Ist das Feld überhaupt ausgefüllt?

log.info('suchbegriff: ' + (kc.isEmpty(ctx.form.suchbegriff) ? 'leer' : 'gefüllt'));
log.info('land: '        + (kc.isEmpty(ctx.form.land)        ? 'leer' : 'gefüllt'));

kc.isEmpty ist wahr für „nichts eingetragen": null, leerer Text, nur Leerzeichen. Nicht leer sind dagegen 0 und false — eine Menge von 0 und ein bewusst abgewähltes Häkchen sind gültige Eingaben.

Für ein deklariertes Feld geht es wieder kürzer — direkt am Wert oder über das Handle, beide beantworten dieselbe Frage:

log.info('suchbegriff: ' + ($suchbegriff.isEmpty    ? 'leer' : 'gefüllt'));
log.info('land: '        + (field.land.isEmpty      ? 'leer' : 'gefüllt'));

Ein Feld, das noch nie ausgefüllt wurde, steht in der Datenbank auf NULL — $doktorgrad.isEmpty ist dann wahr. Das liegt daran, dass der $-Alias null als leeren Text liest; ctx.form.doktorgrad bleibt null und die Spalte damit ebenfalls.

Nicht mit if (ctx.form.menge) prüfen

if (ctx.form.menge) ist auch dann falsch, wenn 0 drinsteht — das ist ein Klassiker unter den schwer zu findenden Fehlern. Nimm $menge.isEmpty, field.menge.isEmpty oder kc.isEmpty(...).

isEmpty ohne Klammern

$menge.isEmpty und field.menge.isEmpty sind Eigenschaften, keine Methoden — also ohne (). Das ist Absicht: Schreibst du versehentlich field.menge.isEmpty(), bekommst du sofort einen Fehler. Wäre es umgekehrt eine Methode, wäre if (field.menge.isEmpty) immer wahr (eine Funktion ist truthy) — und das wäre genau derselbe stille Fehler, vor dem der Kasten darüber warnt.


4. Was für ein Feld ist das? Typ und Beschreibung auslesen

Für alles über das Feld selbst gibt es ctx.model. Die Felder des aktuellen Formulars stehen unter ctx.model.primary.elements:

var el = ctx.model.primary.elements.betrag;

log.info('Name:         ' + el.id);        // betrag
log.info('Beschriftung: ' + el.label);     // der Titel aus dem Editor — oder der Name, wenn keiner gesetzt ist
log.info('Typ: '          + el.type);      // number
log.info('Pflichtfeld:   ' + el.required); // false

Das sind die Angaben aus dem Formular-Editor. Typische Werte für type sind string, number, integer, date und boolean.

Noch genauer: def und column

Jedes Element hat zwei weitere Zugänge — die Schema-Sicht und die Datenbank-Sicht:

var el = ctx.model.primary.elements.betrag;

// .def — die Definition aus dem Formular-Editor
log.info('Schema-Typ:  ' + el.def.type);
log.info('Nachschlage-Liste: ' + el.def['kc:lookup']);   // undefined, wenn keine gesetzt ist

// .column — wie das Feld in der Datenbank steht
if (el.column) {
  log.info('Spalte:      ' + el.column.name);
  log.info('Spaltentyp: ' + el.column.ddl);        // decimal(18,4)
  log.info('darf leer sein: ' + el.column.nullable);
}

column kann null sein

Schaltflächen, Texte und Überschriften sind zwar Elemente, haben aber keine Spalte in der Datenbank. Bei ihnen ist column gleich null — deshalb steht oben das if (el.column) davor. Ohne diese Prüfung bricht das Skript mit „Cannot read property" ab, sobald ein solches Element dazukommt.

el.def lässt sich nicht als Ganzes ausgeben

log.info(JSON.stringify(el.def)) zeigt nur {}. Frag die Einträge einzeln ab (el.def.type, el.def['kc:lookup']) — das funktioniert.

Gibt es das Feld überhaupt?

Ein unbekannter Name liefert undefined statt eines Fehlers. Wenn der Name aus einer Variablen kommt, prüf das lieber:

var el = ctx.model.primary.elements['gibtsnicht'];

if (!el) {
  log.info('Das Feld kenne ich nicht.');
} else {
  log.info('Typ: ' + el.type);
}

5. Über alle Datenelemente laufen

elements ist gleichzeitig eine Liste und ein Verzeichnis. Für eine Schleife nimmst du die Listen-Seite: length sagt, wie viele es sind, [i] holt das i-te Element.

var p = ctx.model.primary;

log.info('Tabelle: ' + p.table);
log.info('Felder insgesamt: ' + p.elements.length);

for (var i = 0; i < p.elements.length; i++) {
  var el = p.elements[i];
  log.info(el.id + ' (' + el.type + ') = ' + ctx.form[el.id]);
}

Beachte die letzte Zeile: el.id ist der Name, und mit ctx.form[el.id] holst du dazu den Wert. So verbindest du beide Seiten — das ist das Muster hinter fast allen Skripten, die „über alles" laufen.

Die sichere Variante: get(...)

Neben der eckigen Klammer und dem Punkt gibt es get(...) — mit einer Zahl für die Position oder mit dem Namen:

ctx.model.primary.elements.get(0);          // nach Position
ctx.model.primary.elements.get('betrag');   // nach Namen

Das Ergebnis ist dasselbe. get(...) ist aber die sichere Wahl, wenn der Feldname aus einer Variablen kommt — und in einem Fall sogar die einzig richtige: An der Sammlung selbst sind list, length, get und ids belegt. Heißt ein Feld zufällig so, liefert elements.length die Anzahl statt des Feldes. elements.get('length') trifft das Feld.

Nur die Namen brauchst du? Dafür gibt es ids():

var namen = ctx.model.primary.elements.ids();
log.info(namen.join(', '));       // name, vorname, email, betrag, …

Beispiel: alle leeren Pflichtfelder melden

Ein Skript, das ohne Änderung in jedem Formular funktioniert — es fragt das Modell, statt Feldnamen fest einzutippen:

// onValidate
var p = ctx.model.primary;
var fehlend = [];

for (var i = 0; i < p.elements.length; i++) {
  var el = p.elements[i];
  if (el.required && kc.isEmpty(ctx.form[el.id])) {
    fehlend.push(el.label || el.id);
    ctx.addError(el.id, 'Bitte ausfüllen.');
  }
}

log.info('Fehlende Pflichtfelder: ' + (fehlend.length ? fehlend.join(', ') : 'keine'));

6. Änderungen erkennen

„Hat sich etwas geändert?" kann dreierlei heißen. Nimm den Abschnitt, der zu deiner Frage passt.

6.1 Welches Feld hat der Benutzer gerade verlassen?

Im Hook onFieldChange steht der Name des auslösenden Feldes in ctx.form._changedField:

// onFieldChange
var geaendert = ctx.form._changedField;

if (!geaendert) {
  log.info('Läuft gerade nicht wegen eines einzelnen Feldes.');
} else if (geaendert === 'land') {
  ctx.form.kundennr = '';                 // Land gewechselt → Kundennummer passt nicht mehr
  log.info('Land geändert, Kundennummer geleert.');
}

_changedField ist nur während des Hooks da; gespeichert wird es nie.

Lieber neu rechnen als vergleichen

Für Summen und abhängige Werte brauchst du _changedField meist gar nicht. Rechne einfach aus dem gesamten aktuellen Stand neu — dann stimmt das Ergebnis auch, wenn mehrere Felder auf einmal ankommen:

ctx.form.gesamtwert = kc.num(ctx.form.menge) * kc.num(ctx.form.preis);

6.2 Ist das ein neuer Datensatz?

if (ctx.instanceId === null) {
  log.info('Dieser Datensatz wurde noch nie gespeichert.');
} else {
  log.info('Bestehender Datensatz Nr. ' + ctx.instanceId);
}

In den Hooks rund ums Speichern (onSubmit, onAfterSave) gibt es zusätzlich ctx.isNew: true, wenn der Satz gerade neu angelegt wurde, false bei einer Änderung an einem bestehenden.

6.3 Hat sich ein Wert gegenüber dem gespeicherten Stand geändert?

Diese Frage beantwortet KCForms nicht von allein — den alten Wert kennt nur die Datenbank. Du liest ihn dir und vergleichst selbst:

// onBeforeSave — prüfen, ob der Betrag angefasst wurde
if (ctx.instanceId === null) {
  log.info('Neuer Datensatz — noch nichts zu vergleichen.');
} else {
  var alt = db.queryOne(
    'SELECT betrag FROM ' + ctx.model.primary.table + ' WHERE id = ${id}',
    { id: ctx.instanceId });

  if (alt && kc.num(alt.betrag) !== kc.num(ctx.form.betrag)) {
    log.info('Betrag geändert: ' + alt.betrag + ' → ' + ctx.form.betrag);
  }
}

Zwei Dinge sind hier absichtlich so gebaut:

  • Der Tabellenname kommt aus ctx.model.primary.table, nicht als getippter Text. Benennt jemand das Formular um, läuft das Skript weiter.
  • Verglichen wird mit kc.num(...), nicht direkt. Sonst gilt "100" als verschieden von 100, und du meldest eine Änderung, die keine war.

7. Zeilen: Datenelemente in einer Detail-Tabelle

Eine Detail-Tabelle ist ein Feld, in dem statt eines einzelnen Werts viele Zeilen stecken. Im Skript ist das eine Liste von Objekten: ein Objekt je Zeile, darin die Spalten als ganz normale Datenelemente.

var zeilen = ctx.form.positionen || [];      // || [] schützt vor "noch keine Zeilen"
var gesamt = 0;

for (var i = 0; i < zeilen.length; i++) {
  var zeile = zeilen[i];
  if (!zeile) continue;                      // Leerzeilen überspringen

  zeile.zeilensumme = kc.num(zeile.menge) * kc.num(zeile.preis);
  gesamt += zeile.zeilensumme;

  log.info('Zeile ' + (i + 1) + ': ' + zeile.menge + ' × ' + zeile.preis
           + ' = ' + zeile.zeilensumme);
}

ctx.form.gesamtwert = gesamt;
log.info('Gesamt: ' + gesamt);

Das Muster ist immer dasselbe:

  1. Liste aus ctx.form holen und mit || [] absichern.
  2. Mit einer Schleife durchgehen, leere Einträge überspringen.
  3. Innerhalb der Zeile ganz normal auf die Spalten zugreifen: zeile.menge.
  4. Ergebnisse in die Zeile oder ins Hauptformular zurückschreiben.

Zeilen ändern: ja. Zeilen anlegen: nein.

Bestehende Zeilen kannst du bedenkenlos ändern. Neue Zeilen per zeilen.push({...}) anzuhängen oder die Liste durch ein neues Array zu ersetzen, funktioniert derzeit nicht zuverlässig.

Für einfache Rechnungen brauchst du kein Skript

Zeilensumme und Gesamtsumme kann KCForms selbst — siehe Berechnungsfelder. Zum Skript greifst du, wenn Bedingungen, Nachschlagen oder Prüfungen dazukommen.

Welche Zeile war gemeint?

Steht in einer Zeile eine Schaltfläche, bekommt der Hook die Zeilennummer in der globalen Variablen event mit — 1 für die erste Zeile, 2 für die zweite und so weiter. Damit holst du dir genau die angeklickte Zeile:

// Hook einer Schaltfläche in der Spalte einer Detail-Tabelle
var zeilen = ctx.form.positionen || [];
var nr     = Number(event);              // 1-basiert!
var zeile  = zeilen[nr - 1];             // Listen zählen ab 0 → minus 1

if (zeile) {
  zeile.geprueft = 'ja';
  log.info('Zeile ' + nr + ' geprüft.');
}

Daneben steht in ctx.form._triggeredHook der Name des Hooks, der gerade läuft — praktisch, wenn mehrere Schaltflächen denselben Hook aufrufen.

Die Struktur einer Detail-Tabelle nachschlagen

Auch Detail-Tabellen haben eine Modell-Sicht. Über ctx.model.children kommst du an Tabellennamen und Spalten — zum Lesen, nicht zum Schreiben:

log.info('Detail-Tabellen: ' + ctx.model.children.length);

for (var i = 0; i < ctx.model.children.length; i++) {
  var ds = ctx.model.children[i];
  log.info(ds.id + ' → Tabelle ' + ds.table + ', ' + ds.elements.length + ' Spalten');

  for (var j = 0; j < ds.elements.length; j++) {
    log.info('   ' + ds.elements[j].id + ' (' + ds.elements[j].type + ')');
  }
}

ctx.model ist nur zum Lesen

Werte, die du über ctx.model…rows[…].elements.<x>.value setzt, landen nicht im Datensatz und werden nicht gespeichert. Geschrieben wird immer über ctx.form.


8. Einen Fehler an ein Datenelement hängen

Damit eine Meldung direkt am richtigen Feld erscheint, gibst du den Feldnamen mit. Das field-Handle ist dafür die kürzeste Schreibweise:

// onValidate
if (kc.num(ctx.form.betrag) < 0) {
  field.betrag.error('Der Betrag darf nicht negativ sein.');
  log.info('Meldung gesetzt.');
}
Aufruf Wirkung
field.betrag.error('…') weicher Fehler — Speichern kann erlaubt bleiben
field.betrag.hardError('…') harter Fehler — blockiert
ctx.addError('betrag', '…') dasselbe wie error, mit Namen als Text

Die Langform ctx.addError(name, text) brauchst du, wenn der Feldname in einer Variablen steht — so wie in der Pflichtfeld-Schleife in Abschnitt 5.


9. Kleine Helfer zum Mitnehmen

Diese vier Funktionen decken die meisten Alltagsfälle ab. Leg sie unter Hilfsfunktionen ab (im Skript-Editor neben den Hooks), dann kann jeder Hook dieses Formulars sie benutzen.

/** Wert eines Feldes als Zahl — egal ob Text, leer oder Kommazahl. */
function zahl(feldname) {
  return kc.num(ctx.form[feldname]);
}

/** Wahr, wenn das Feld nicht ausgefüllt ist. */
function leer(feldname) {
  return kc.isEmpty(ctx.form[feldname]);
}

/** Summe einer Spalte über alle Zeilen einer Detail-Tabelle. */
function spaltenSumme(tabellenfeld, spalte) {
  var zeilen = ctx.form[tabellenfeld] || [];
  var summe  = 0;
  for (var i = 0; i < zeilen.length; i++) {
    if (zeilen[i]) summe += kc.num(zeilen[i][spalte]);
  }
  return summe;
}

/** Alle Feldnamen und Werte ins Protokoll — zum Nachsehen bei der Fehlersuche. */
function felderProtokollieren() {
  var p = ctx.model.primary.elements;
  for (var i = 0; i < p.length; i++) {
    log.info('  ' + p[i].id + ' = ' + ctx.form[p[i].id]);
  }
}

// Anwendung
log.info('Summe: ' + (zahl('menge') * zahl('preis')));
log.info('Menge leer? ' + leer('menge'));
log.info('Positionen: ' + spaltenSumme('positionen', 'preis'));

10. Wo die Grenzen liegen

Das geht nicht Das nimmst du stattdessen
über ctx.model Werte speichern in ctx.form schreiben
neue Zeilen an eine Detail-Tabelle anhängen Zeilen im Formular anlegen lassen
JSON.stringify(el.def) ausgeben Einträge einzeln lesen: el.def.type
el.column ungeprüft benutzen vorher if (el.column) — Texte und Schaltflächen haben keine Spalte
ctx.form außerhalb eines Formulars benutzen in der Skript-Konsole gibt es die Testfelder $text, $number, $date, $flag

In der Skript-Konsole ist ctx.model null, weil dort kein Formular geöffnet ist. Skripte, die in beiden Welten laufen sollen, sichern sich ab:

if (ctx.model) {
  log.info('Modell vorhanden: ' + ctx.model.primary.table);
} else {
  log.info('Kein Formular-Kontext — Modell nicht verfügbar.');
}

Spickzettel

// Wert
ctx.form.feld                       // lesen / schreiben
$feld                               // Kurzform
field.feld.value                    // Feld-Handle
ctx.form['feld']                    // Name aus einer Variablen
$['beitrag' + i]                    // dasselbe als Kurzform (Schleifen!)
field['beitrag' + i].value          // und am Handle
'beitrag1' in $                     // gibt es das Feld? (ohne Abbruch)

// Umgang mit Werten
field.feld.num                      // sicher als Zahl
field.feld.numFormat(null,"€",0,2,",",".",3)   // als Anzeigetext: 12.345,76 €
$feld.text                          // als Text, getrimmt, nie null
$feld.bool                          // als Wahrheitswert
field.feld.text / field.feld.bool   // dasselbe am Handle
$feld.isEmpty                       // nicht ausgefüllt? (ohne Klammern!)
field.feld.isEmpty                  // dasselbe am Handle
kc.num(ctx.form.feld)               // dasselbe ohne Feld-Handle
kc.isEmpty(ctx.form.feld)           // (Zwischenwerte, Zeilen, Bedingungen)

// Über das Feld
var el = ctx.model.primary.elements.feld;
el.id  el.label  el.type  el.required
el.def.type                         // Schema-Sicht
el.column && el.column.ddl          // Datenbank-Sicht (kann null sein!)

// Alle Felder
ctx.model.primary.elements.length
ctx.model.primary.elements[i]        // auch .betrag / ['betrag']
ctx.model.primary.elements.get(i)    // sicher, wenn der Name variabel ist
ctx.model.primary.elements.ids()

// Änderungen
ctx.form._changedField              // auslösendes Feld (onFieldChange)
ctx.instanceId === null             // noch nie gespeichert
ctx.isNew                           // neu angelegt (onSubmit / onAfterSave)

// Zeilen
var zeilen = ctx.form.tabellenfeld || [];
zeilen[i].spalte                    // Wert einer Zelle
Number(event)                       // angeklickte Zeile (1-basiert)

// Meldungen
field.feld.error('…')  /  field.feld.hardError('…')
ctx.addError(name, '…')

Weiterlesen