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:

- 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:
_changedFieldist ein Transportfeld. KCForms entfernt es nach dem Hook wieder; es wird nie gespeichert.- Rechnen ohne Skript geht oft einfacher mit
Berechnungsfeldern.
onFieldChangelohnt 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:
onValidateundonBeforeSavelaufen normal,onBeforeSavekann abbrechen,onSubmitläuft — das ist der Zweck der Übung,onAfterSaveläuft nie, undctx.instanceIdbleibtnull.
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()nachpostpone(…)) 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')(oderreturn false) lässt den Vorgang auf der alten Stufe; der Bearbeiter liest den Grund im Workflow-Bereich.ctx.formzeigt 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, ohnectx.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/catchab. - Bibliotheken, Hilfsfunktionen,
ctx.paramsundctx.attachmentsstehen 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/errorlandet inscript.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ätzlichctx.addError(...)benutzt, stehen die Feldfehler mit drin. - Abbruch in
onBeforeDelete: HTTP 422 mitabortedBy: "onBeforeDelete". - Löschen ohne Lösch-Hooks: HTTP 204 ohne Inhalt.
- Fehler in einem Schaltflächen-Hook: HTTP 200 mit
ok: falseund 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>" }, dazudialogsundcloseRequested, wenn das Skriptview.*bzw.ctx.close()benutzt hat.
Fallen, die häufig zuschnappen¶
onLoadfür gespeicherte Sätze erwarten. Er läuft nur für den leeren Satz. Für Bestandssätze istcanEditder Einstiegspunkt.- In
onSubmitFelder setzen und sich wundern. Nach dem Speichern zählt nur, wasctx.persist(...)nachträgt. ctx.cancel()ohnereturn. Der Hook läuft weiter und macht womöglich noch Dinge, die zum Abbruch nicht passen.- Benachrichtigung in
onAfterSave, Formular erlaubt Entwürfe. Bei einem Entwurf mit Fehlern läuft der Hook nicht. - Eigener Hook heißt
saveForm. Der Name ist vergeben; der Hook läuft nie. - 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 |