Zum Inhalt

Lifecycle-Hooks

Ein Hook ist ein Skript mit festem Namen, das KCForms zu einem bestimmten Zeitpunkt von selbst ausführt. Du rufst ihn nie auf — du legst ihn an, und KCForms kümmert sich um den Rest.

Das Prinzip kennst du aus dem Alltag: „Wenn jemand klingelt, schau durch den Spion." Niemand ruft diese Regel auf; sie greift, wenn die Situation eintritt. Genauso hier: Du schreibst einmal auf, was beim Öffnen, beim Tippen, beim Speichern oder beim Löschen passieren soll — und KCForms führt es an genau dieser Stelle aus.

// onSubmit — läuft automatisch, nachdem ein Datensatz gespeichert wurde
log.info('Gespeichert wurde Datensatz Nummer ' + ctx.instanceId);

Jeder Hook bekommt dieselbe Werkzeugkiste: ctx mit den Feldwerten und Angaben zum Vorgang, dazu db, mail, fs, view, log und die übrigen Skript-Bausteine. Der Unterschied zwischen zwei Hooks ist nur: wann sie laufen und was sie zu diesem Zeitpunkt dürfen.

Die Landkarte

Vier Wege durch das Leben eines Datensatzes. Die meisten Formulare brauchen nur ein oder zwei Hooks daraus.

flowchart TD
    subgraph OEFFNEN["Formular öffnen"]
        direction LR
        N["neuer Satz"] --> OL["onLoad"] --> DEF["Vorbelegungen"]
        G["gespeicherter Satz"] --> CE["canEdit"]
    end
    subgraph TIPPEN["Ausfüllen"]
        direction LR
        FC["onFieldChange<br/>nach jedem Feld"]
    end
    subgraph SPEICHERN["Speichern"]
        direction LR
        VA["onValidate"] --> BS["onBeforeSave"] --> DB[("in die DB")] --> SU["onSubmit"] --> AS["onAfterSave"]
    end
    subgraph LOESCHEN["Löschen"]
        direction LR
        BD["onBeforeDelete"] --> DEL[("aus der DB")] --> AD["onAfterDelete"]
    end
    OEFFNEN --> TIPPEN --> SPEICHERN
    TIPPEN -.-> LOESCHEN

Daneben gibt es Hooks, die nicht in dieser Kette hängen, sondern auf ein Ereignis warten: Schaltflächen (eigene Hooks), die Workflow-Skripte vor und nach einem Stufenwechsel, onCopy beim Kopieren eines Datensatzes und onRetention, wenn die Aufbewahrungsfrist eines Datensatzes abgelaufen ist.

Wo Hooks entstehen

Im Formular-Editor, Reiter Skripte: links die Liste der vorhandenen Hooks, rechts der Editor. Am Fuß der Liste steht + Hook hinzufügen — ein Klick darauf klappt die Auswahl auf:

Die Hook-Auswahl im Reiter „Skripte"

  • Oben stehen die Standard-Hooks, die dieses Formular noch nicht hat. Ein Klick legt den Hook mit einer kommentierten Vorlage an. Bereits angelegte Hooks fehlen in der Liste — im Bild zum Beispiel onFieldChange, den das Formular schon besitzt.
  • Darunter das Feld Custom-Hook (kc:button) für einen eigenen Hook mit freiem Namen. Den ruft später eine Schaltfläche auf (siehe Schaltflächen).
  • Hilfsfunktionen ganz oben in der Liste ist kein Hook. Der Code dort wird jedem Skript dieses Formulars vorangestellt — gemeinsame Funktionen für alle Hooks. Details: Hilfsfunktionen.

Was vor deinem Hook noch läuft

Bevor dein Hook-Code startet, stellt KCForms ihm Code voran — in dieser Reihenfolge: zugewiesene Bibliotheken aus der Datenbank, dann Bibliotheken aus dem Dateisystem, zuletzt die Hilfsfunktionen des Formulars. Das Letzte gewinnt: Eine Funktion in den Hilfsfunktionen überschreibt eine gleichnamige aus einer Bibliothek — das Speziellere schlägt das Allgemeinere.

Öffnen: onLoad und canEdit

onLoad — nur für den leeren Satz

onLoad läuft, wenn das Formular für einen neuen, leeren Datensatz geöffnet wird. ctx.form ist dabei leer, ctx.instanceId ist immer null. Typischer Zweck: vorbelegen.

// onLoad
ctx.form.erfasstAm  = new Date().format('yyyy-MM-dd');
ctx.form.bearbeiter = ctx.user.name;
log.info('Vorbelegt für ' + ctx.user.name);

Beim Öffnen eines gespeicherten Satzes läuft onLoad nicht

Das ist die häufigste Fehlannahme zu diesem Hook. Wer einen vorhandenen Datensatz aus der Liste öffnet, löst keinen onLoad aus — die Werte kommen aus der Datenbank, nicht aus einem Skript.

Nachgemessen am laufenden System: Ein Formular, dessen onLoad drei Felder füllt, liefert diese drei Felder beim neuen Satz gefüllt — beim Öffnen eines gespeicherten Satzes kommen sie leer zurück, weil dort nie etwas hineingeschrieben wurde.

Wenn beim Öffnen eines gespeicherten Satzes etwas passieren soll, ist canEdit der Ort dafür.

Nach onLoad füllt KCForms noch die deklarativen Vorbelegungen (kc:defaultLookup, kc:defaultExpression) ein — aber nur in Felder, die dann noch leer sind. Dein Skript hat also Vorrang. Danach werden leere Detail-Tabellen mit kc:childTable.initialRows auf die gewünschte Zeilenzahl gebracht.

canEdit — darf dieser Satz bearbeitet werden?

Läuft beim Öffnen eines gespeicherten Satzes und entscheidet, ob das Formular bearbeitbar oder schreibgeschützt erscheint. return false oder ctx.cancel('Grund') sperren; ohne Hook gilt die Standardregel (ein Satz einer älteren Formularversion ist gesperrt).

// canEdit
if (ctx.form.status === 'abgeschlossen') {
  ctx.cancel('Abgeschlossene Anträge sind schreibgeschützt.');
}

Der Hook sieht zusätzlich ctx.recordFormVersion (Version des Satzes) und ctx.currentFormVersion (aktuelle Version des Formulars) — siehe Formularversionen. Stürzt er ab, greift die Standardregel; gesperrt wird also nie aus Versehen durch einen Programmfehler.

Ausfüllen: onFieldChange

Läuft, sobald der Anwender ein Feld verlässt, dessen Wert sich geändert hat. Der Name des auslösenden Feldes steht in ctx.form._changedField.

// onFieldChange
ctx.form.summe = kc.num(ctx.form.wert1) + kc.num(ctx.form.wert2);
log.info('Summe ist ' + ctx.form.summe);

Gezielt auf ein Feld reagieren:

// onFieldChange
if (ctx.form._changedField === 'land') {
  ctx.form.plz = '';            // andere Land, andere PLZ
  log.info('PLZ geleert');
}

Zwei Dinge, die man wissen sollte:

  • _changedField ist ein Transportfeld. KCForms entfernt es nach dem Hook wieder; es wird nie gespeichert.
  • Rechnen ohne Skript geht oft einfacher mit Berechnungsfeldern. onFieldChange lohnt sich, wenn Daten geholt oder Felder geleert werden müssen.

Prüfen: onValidate und Feld-Validatoren

Beim Knopf Prüfen und noch einmal zu Beginn jedes Speicherns läuft diese Kette:

flowchart LR
    S["Schema-Prüfung<br/>Pflicht, Typ, Muster"] --> V["onValidate"] --> F["Feld-Validatoren<br/>kc:validator"]

Fehler meldest du mit ctx.addError(feld, text) oder — wenn gar nichts mehr gehen darf — mit ctx.addHardError(feld, text).

// onValidate
if (kc.num(ctx.form.betrag) > 1000 && !ctx.form.freigabe) {
  ctx.addError('freigabe', 'Bei Beträgen über 1000 € ist eine Freigabe nötig.');
}
weicher Fehler (addError) harter Fehler (addHardError)
Meldung am Feld ja ja
Speichern als Entwurf möglich, wenn das Formular Speichern mit Fehlern erlaubt nie
gedacht für fachliche Regeln Daten, die physisch nicht in die Spalte passen

Details zu beiden Wegen und zu kc:validator: Validierung.

Speichern: die Kette im Detail

flowchart TD
    A["Schema-Prüfung"] --> B["onValidate"] --> C["Feld-Validatoren"]
    C --> D{"Fehler?"}
    D -- "ja, und Entwürfe verboten" --> STOP["Abbruch<br/>nichts gespeichert"]
    D -- "nein / Entwurf erlaubt" --> E["onBeforeSave"]
    E --> F{"abgebrochen?"}
    F -- ja --> STOP2["Abbruch<br/>nichts gespeichert"]
    F -- nein --> G[("INSERT / UPDATE<br/>samt Detail-Zeilen")]
    G --> H["onSubmit"] --> I["onAfterSave"]
# Schritt Abbruch möglich ctx.instanceId Wofür
1 Schema-Prüfung – wie geladen Pflicht, Typ, Muster
2 onValidate – wie geladen eigene Regeln
3 Feld-Validatoren – wie geladen kc:validator
4 onBeforeSave ja wie geladen letzte Prüfung, letzte Ergänzung
– Speichern INSERT oder UPDATE, Detail-Zeilen, Anhänge
5 onSubmit – gesetzt Mail, Schnittstellen, PDF
6 onAfterSave – gesetzt Abschluss, ctx.isNew trennt Anlegen/Ändern

Was du wo noch ändern darfst

In onBeforeSave geschriebene Werte wandern direkt mit in die Datenbank — der Hook sieht ctx.form in seiner endgültigen Form und darf ihn anfassen.

// onBeforeSave
ctx.form.zuletztGeaendert = new Date().toISOString();   // wird mitgespeichert
log.info('Zeitstempel ergänzt');

In onSubmit und onAfterSave ist der Datensatz bereits geschrieben. Wer dort noch etwas in die Datenbank bringen will, muss es ausdrücklich nachtragen:

// onSubmit
ctx.form.aktenzeichen = 'AZ-' + ctx.instanceId;
ctx.persist(['aktenzeichen']);     // ohne diese Zeile bleibt es nur im Browser

Abbrechen

Nur onBeforeSave kann das Speichern verhindern — mit ctx.cancel('Grund') oder return false. Das Formular zeigt dann die Meldung, und es wurde nichts geschrieben.

// onBeforeSave
if (kc.num(ctx.form.betrag) > 5000 && !ctx.form.managerFreigabe) {
  ctx.cancel('Beträge über 5.000 € brauchen eine Manager-Freigabe.');
  return;
}

ctx.cancel() beendet das Skript nicht

Es merkt den Abbruch nur vor. Ohne return läuft der Rest des Hooks weiter — deshalb steht in fast jedem Beispiel return; direkt darunter.

In onSubmit und onAfterSave ist ein ctx.cancel(...) wirkungslos: Der Satz steht schon in der Datenbank. KCForms ignoriert es und schreibt eine Warnung ins Protokoll, damit du den vertauschten Hook findest.

Wenn es Fehler gab: zwei Schalter

Zwei Formular-Eigenschaften entscheiden, wie weit die Kette bei Prüffehlern läuft (Metadaten):

Prüffehler? Speichern mit Fehlern Absenden mit Fehlern Ergebnis
nein – – alles läuft
ja aus – Abbruch, nichts gespeichert
ja an aus gespeichert, aber weder onSubmit noch onAfterSave
ja an an gespeichert, Kette läuft vollständig

Ein harter Fehler sticht beide Schalter: Er blockiert immer.

onAfterSave hängt an onSubmit

Beide laufen unter derselben Bedingung. Ein Entwurf mit Fehlern, bei dem nur Speichern mit Fehlern erlaubt ist, löst also auch keine onAfterSave-Logik aus — ein häufiger Stolperstein bei Benachrichtigungen.

Formulare ohne eigene Tabelle

Ein Formular im Memory-Modus speichert nichts in einer eigenen Tabelle (typisch, wenn die Daten per Skript an ein anderes System gehen). Dort gilt:

  • onValidate und onBeforeSave laufen normal, onBeforeSave kann abbrechen,
  • onSubmit läuft — das ist der Zweck der Übung,
  • onAfterSave läuft nie, und ctx.instanceId bleibt null.

Löschen

flowchart LR
    BD{"onBeforeDelete<br/>Abbruch?"} -- nein --> D[("löschen")] --> AD["onAfterDelete"]
    BD -- ja --> X["Satz bleibt"]

onBeforeDelete sieht den Satz noch vollständig in ctx.form und kann das Löschen verhindern:

// onBeforeDelete
if (ctx.form.status === 'freigegeben') {
  ctx.cancel('Freigegebene Anträge dürfen nicht gelöscht werden.');
}

onAfterDelete läuft danach. Die ID des gelöschten Satzes steht in ctx.deletedId (ctx.instanceId ist dann null), die letzten Werte stehen noch in ctx.form:

// onAfterDelete
log.info('Antrag #' + ctx.deletedId + ' von ' + ctx.form.antragsteller + ' gelöscht');

Nach dem Löschen gibt es kein Zurück

Ein Fehler in onAfterDelete wird protokolliert und verschluckt — der Datensatz bleibt gelöscht. Was unbedingt klappen muss, gehört in onBeforeDelete.

Ohne Lösch-Hook ist es schneller

Hat ein Formular weder onBeforeDelete noch onAfterDelete, lädt KCForms den Satz gar nicht erst in einen Skript-Kontext — das Löschen antwortet schlank mit 204 No Content.

Aufbewahrung: onRetention

Läuft im nächtlichen Gallring-Lauf für jeden Datensatz, dessen Aufbewahrungsfrist abgelaufen ist, sofern das Formular unter Aufbewahrung die Aktion Eigenes Skript gewählt hat. Der Datensatz ist vollständig geladen: ctx.form und die $feld-Kurzformen samt Detail-Tabellen, ctx.attachments und ctx.params. Angemeldet ist der Benutzer gallring.

Das Skript entscheidet, statt selbst zu löschen, und zwar über das Objekt retention:

Aufruf Wirkung
retention.deleteRecord() Satz, Detail-Tabellen, Anlagen, Workflow-Verlauf und Änderungsprotokoll löschen
retention.anonymize(['name', 'email']) Felder leeren, der Satz bleibt. Eine Detail-Tabelle in der Liste wird geleert; Anlagen und Änderungsprotokoll fallen immer weg.
retention.postpone(90, 'Grund') in 90 Tagen erneut vorlegen
retention.hold('Grund') sperren, bis jemand die Sperre im Admin-Bereich aufhebt
retention.log('…') Zeile ins Gallring-Protokoll
retention.ref('…') Referenz ins Protokoll, z. B. die Quittung eines Archivs
retention.info formId, instanceId, startDate, retainUntil, rule, legalBasis, approvedBy, runId

Drei Regeln, die das Verhalten erklären:

  • Ausgeführt wird erst am Ende. deleteRecord() merkt die Löschung nur vor. Wirft das Skript danach noch einen Fehler, bleibt der Satz stehen. So wird nichts gelöscht, dessen Abgabe an ein Archiv gescheitert ist.
  • Genau eine Entscheidung. Eine zweite (deleteRecord() nach postpone(…)) bricht mit Fehler ab.
  • Keine Entscheidung heißt: nichts passiert. Der Satz bleibt, und der nächste Lauf legt ihn wieder vor. Im Protokoll steht „Keine Entscheidung“.
// onRetention — erst ablegen, dann löschen
var archiv = fs['/archiv/' + ctx.formId + '/' + ctx.instanceId];
archiv.mkdir(true);
archiv['/datensatz.pdf'].writeBytes(pdf.render().bytes);
retention.ref('Archiv: /archiv/' + ctx.formId + '/' + ctx.instanceId);

retention.deleteRecord();

Keine Inhalte ins Protokoll

retention.log(…), retention.ref(…) und die Gründe von postpone/hold stehen im Gallring-Protokoll, und das bleibt jahrelang. Aktenzeichen und technische Nummern sind dort richtig, Namen und Adressen nicht.

Die Hooks onBeforeDelete und onAfterDelete laufen beim Gallring nicht. Sie gehören zum Löschen durch einen Anwender. Ein Veto dort würde eine Pflicht-Löschung sonst stillschweigend dauerhaft blockieren.

Kopieren: onCopy

Läuft bei Als neuen Satz kopieren, bevor die Kopie im Formular erscheint. Der Hook bekommt ein globales copy-Objekt mit sourceInstanceId und räumt typischerweise auf:

// onCopy
ctx.form.status       = 'neu';
ctx.form.aktenzeichen = '';
ctx.form.bemerkung    = 'Kopie von #' + copy.sourceInstanceId;

Die Kopie ist noch nicht gespeichert — sie landet als Entwurf im Formular, und beim Speichern läuft die normale Speicher-Kette.

Workflow: Skripte am Stufenwechsel

Bei jedem Stufenwechsel eines Workflows laufen bis zu fünf Skripte — zwei vor dem Wechsel, drei danach. Angelegt werden sie im Workflow-Block der Formular-Eigenschaften, direkt an der Stufe oder am Übergang; der Name trägt die Zuordnung.

Skript Läuft Kann verhindern
onWorkflowCheck vor jedem Wechsel ✅
onWorkflowCheck_<von>_<nach> vor genau diesem Übergang ✅
onWorkflowStep nach jedem Wechsel nimmt zurück
onWorkflowStep_<von>_<nach> nach genau diesem Übergang nimmt zurück
onWorkflowEnter_<stufe> beim Erreichen der Stufe, egal woher nimmt zurück

Sie laufen in der Reihenfolge der Tabelle, danach geht die Benachrichtigung raus. Ein globales wf-Objekt beschreibt den Schritt:

wf-Feld Inhalt
fromStage / toStage Nummer der alten und der neuen Stufe
fromLabel / toLabel deren Beschriftung
action "approve", "return" (zurückgeben) oder "decline" (endgültig ablehnen)
approve / returned / declined dasselbe als true/false
decision "approve" oder "reject" — ältere Form, trennt Zurückgeben und Ablehnen nicht
terminal true, wenn der Vorgang damit abgeschlossen ist
comment Kommentar des Bearbeiters
recipient Empfänger der neuen Stufe: .email, .login, .firstName, .lastName, .personalnr
initiatorEmail Adresse des Antragstellers
actingLogin wer entschieden hat
now Zeitpunkt der Entscheidung
url Link zum Vorgang für den Empfänger (nur nach dem Wechsel)
mail Standard-Benachrichtigung steuern: .skip = true, .subject, .text
phase, hook "check" oder "action", und der Name des laufenden Skripts
// onWorkflowCheck_3_4 — vor dem Wechsel
if (wf.approve && !$begruendung) {
  ctx.cancel('Bitte zuerst eine Begründung eintragen.');
  return;
}
// onWorkflowEnter_99 — nach dem Wechsel
if (wf.approve) {
  ctx.form.abgeschlossenAm = wf.now;      // wird automatisch gespeichert
}

Was diese Skripte von den übrigen Hooks unterscheidet:

  • Vor dem Wechsel ist noch nichts geschrieben. ctx.cancel('Grund') (oder return false) lässt den Vorgang auf der alten Stufe; der Bearbeiter liest den Grund im Workflow-Bereich. ctx.form zeigt seine aktuellen Eingaben; Änderungen daran werden in der Prüfung nicht gespeichert.
  • Nach dem Wechsel werden geänderte ctx.form-Felder automatisch nachgespeichert — nur die geänderten, ohne ctx.persist(...).
  • Ein Fehler nach dem Wechsel nimmt ihn zurück. Status, Verlauf und Feldwerte stehen danach wie vor dem Klick, und der Bearbeiter sieht die Meldung. Nicht zurückholen lässt sich, was das Skript außerhalb des Vorgangs angestoßen hat (Mail, Datei, gebuchter Aufruf) — deshalb gehört nach vorn, was scheitern kann, und ans Ende, was sich nicht zurückholen lässt. Wer eine Aktion für entbehrlich hält, fängt ihren Fehler mit try/catch ab.
  • Bibliotheken, Hilfsfunktionen, ctx.params und ctx.attachments stehen wie in jedem anderen Hook bereit.

Geändert gegenüber älteren Fassungen

Früher gab es nur onWorkflowStep; ein Fehler darin wurde protokolliert und verschluckt, und Bibliotheken, Hilfsfunktionen, ctx.params und ctx.attachments fehlten. Bestehende onWorkflowStep-Skripte laufen unverändert weiter — mit einem Unterschied: Wirft eines einen Fehler, wird der Wechsel jetzt zurückgenommen.

Schaltflächen: eigene Hooks

Jede Schaltfläche kann über scriptHook einen frei benannten Hook auslösen. Er läuft nur auf Klick, sonst wie jeder andere Hook.

"_btnReisetage": {
  "type": "string",
  "kc:button": { "label": "Reisetage berechnen",
                 "scriptHook": "berechneReisetage",
                 "variant": "primary" }
}

Im Hook stehen zusätzlich bereit:

Feld Inhalt
ctx.form._triggeredHook Name des ausgelösten Hooks
ctx.form._answerId / _dialogId die Antwort, wenn der Klick aus einem view.Question-Dialog zurückkommt

Alle drei sind Transportfelder und werden nicht gespeichert. Was der Hook in ctx.form schreibt, übernimmt das Formular sofort in die Maske — gespeichert wird es erst beim nächsten Speichern.

Sieben Namen sind vergeben

saveForm, printForm, validateForm, closeForm, newRecord, newForm, prevPage und nextPage sind Standard-Aktionen. Eine Schaltfläche mit einem dieser Namen löst die gleichnamige Toolbar-Aktion aus (Speichern, Drucken, Prüfen, Schließen, neuer Satz, Seite vor/zurück) und erreicht den Server gar nicht. Ein eigener Hook mit einem dieser Namen würde also nie laufen — such dir einen anderen Namen.

Was ein Hook sieht

onLoad onFieldChange onValidate onBeforeSave onSubmit onAfterSave onBeforeDelete onAfterDelete
ctx.form lesen ✅ (leer) ✅ ✅ ✅ ✅ ✅ ✅ ✅
ctx.form ändern wirkt ✅ ✅ ✅ ✅ nur mit ctx.persist nur mit ctx.persist – –
ctx.instanceId immer null ✅ ✅ ✅ ✅ ✅ ✅ null
ctx.isNew – – – – – ✅ – –
ctx.deletedId – – – – – – – ✅
abbrechen – – Fehler melden ✅ – – ✅ –

In allen Hooks dieser Tabelle verfügbar: ctx.user, ctx.formId, ctx.params (Properties), ctx.attachments, view.* für Dialoge und die Bausteine db, mail, http, fs, log, kc. Für Schaltflächen-Hooks gilt dasselbe. Die Workflow-Skripte bekommen dasselbe — und zusätzlich das wf-Objekt.

Wenn etwas schiefgeht

  • Zeitlimit: Ein Hook darf standardmäßig 6 Sekunden laufen. Danach bricht KCForms ihn ab — Schutz gegen Endlosschleifen.
  • Protokoll: Alles aus log.info/warn/error landet in script.log, nicht in der Browser-Konsole. Pro Zeile steht dabei, aus welchem Formular sie kam.
  • Fehlerverhalten: unterschiedlich je Hook —
Hook Fehler im Skript bedeutet
onLoad, onFieldChange, onValidate Aufruf schlägt fehl, das Formular meldet es
onBeforeSave, onBeforeDelete Vorgang wird abgebrochen, nichts geändert
onSubmit Satz ist gespeichert, der Fehler kommt zurück
onAfterSave, onAfterDelete wird protokolliert und verschluckt
onWorkflowCheck… der Stufenwechsel findet nicht statt, der Bearbeiter sieht die Meldung
onWorkflowStep…, onWorkflowEnter… der Stufenwechsel wird zurückgenommen, der Bearbeiter sieht die Meldung
eigener Hook (Schaltfläche) Schaltfläche meldet den Fehler, Formular bleibt offen
Für Entwickler: was der Server antwortet
  • Abbruch in onBeforeSave: HTTP 200 mit { ok: false, abortedBy: "onBeforeSave", message: "…" }. Hat der Hook zusätzlich ctx.addError(...) benutzt, stehen die Feldfehler mit drin.
  • Abbruch in onBeforeDelete: HTTP 422 mit abortedBy: "onBeforeDelete".
  • Löschen ohne Lösch-Hooks: HTTP 204 ohne Inhalt.
  • Fehler in einem Schaltflächen-Hook: HTTP 200 mit ok: false und einem Fehler am Pseudo-Feld _hook. Bei einem Mail-Zustellfehler steht dort die Klartext-Meldung des Mailservers.
  • Erfolgreicher Schaltflächen-Hook: { ok: true, values: …, errors: …, hook: "<name>" }, dazu dialogs und closeRequested, wenn das Skript view.* bzw. ctx.close() benutzt hat.

Fallen, die häufig zuschnappen

  1. onLoad für gespeicherte Sätze erwarten. Er läuft nur für den leeren Satz. Für Bestandssätze ist canEdit der Einstiegspunkt.
  2. In onSubmit Felder setzen und sich wundern. Nach dem Speichern zählt nur, was ctx.persist(...) nachträgt.
  3. ctx.cancel() ohne return. Der Hook läuft weiter und macht womöglich noch Dinge, die zum Abbruch nicht passen.
  4. Benachrichtigung in onAfterSave, Formular erlaubt Entwürfe. Bei einem Entwurf mit Fehlern läuft der Hook nicht.
  5. Eigener Hook heißt saveForm. Der Name ist vergeben; der Hook läuft nie.
  6. In einem Workflow-Skript erst die Mail, dann den Fremdaufruf. Scheitert der Aufruf, wird der Wechsel zurückgenommen — die Mail ist aber schon raus. Erst, was scheitern kann.

Spickzettel

Hook Läuft Kann abbrechen Merkmal
onLoad neuer, leerer Satz wird geöffnet – instanceId immer null
canEdit gespeicherter Satz wird geöffnet ✅ (sperrt) entscheidet über Schreibschutz
onFieldChange Feld verlassen und geändert – _changedField
onValidate Prüfen und vor jedem Speichern Fehler melden ctx.addError
onBeforeSave direkt vor dem Schreiben ✅ Änderungen werden mitgespeichert
onSubmit nach dem Schreiben – Mail, Schnittstellen, PDF
onAfterSave ganz am Ende – ctx.isNew
onBeforeDelete vor dem Löschen ✅ Satz noch in ctx.form
onAfterDelete nach dem Löschen – ctx.deletedId
onCopy beim Kopieren – copy.sourceInstanceId
onWorkflowCheck… vor einem Workflow-Stufenwechsel ✅ wf-Objekt, speichert nichts
onWorkflowStep…, onWorkflowEnter… nach einem Workflow-Stufenwechsel nimmt zurück wf-Objekt, speichert selbst
eigener Name Klick auf eine Schaltfläche – kc:button.scriptHook