ctx — der Skript-Kontext¶
Wenn ein Skript läuft, steht schon alles bereit, was zum aktuellen Vorgang
gehört: die Werte im Formular, wer gerade angemeldet ist, ob der Datensatz
bereits gespeichert wurde. Das alles liegt in ctx — einem einzigen
Wort, das du in jedem Skript benutzen kannst, ohne es vorher anzulegen.
Diese Zeile liest zwei Feldwerte und schreibt das Ergebnis in ein drittes Feld. Mehr braucht es nicht — kein Laden, kein Speichern, keine Verbindung öffnen.
Wenn du die anderen Werkzeuge im Skripting schon gesehen hast (db, mail,
fs, view …): Die tun etwas — eine Mail verschicken, eine Datei lesen.
ctx tut nichts, es ist einfach da und beschreibt die Lage. Deshalb steht
es in fast jedem Skript.
Wo solche Zeilen stehen¶
Skripte gehören zum Formular. Du findest sie im Formular-Editor im Reiter Skripte: links die Liste der Anlässe — beim Öffnen, bei jeder Änderung, beim Prüfen, beim Speichern —, rechts der Code, der dann läuft. Welcher Anlass wann dran ist, steht im Lebenszyklus.

Zum Ausprobieren gibt es außerdem die Skript-Konsole (Zahnrad-Menü oben rechts). Dort tippst du ein paar Zeilen ein, drückst Ausführen und siehst sofort das Ergebnis, ohne ein Formular anzufassen. Zwei Dinge sind dort nützlich zu wissen:
- Die Konsole öffnet mit dem zuletzt gespeicherten Skript, nicht leer — Zurücksetzen gibt dir ein leeres Blatt.
- Rechts lassen sich Testwerte für
ctx.formeintragen. Damit probierst du eine Berechnung aus, ohne ein Formular auszufüllen.
Was du wo findest¶
Die häufigsten Fragen und ihre Antwort — die Details stehen weiter unten:
| Du möchtest … | … dafür nimmst du |
|---|---|
| einen Feldwert lesen oder setzen | ctx.form — oder die Kurzform $feldname |
| einen Feldnamen erst zur Laufzeit bilden | $["beitrag" + i], field["beitrag" + i] oder ctx.form["beitrag" + i] — Beschreibung |
| mit einem Feldwert rechnen | field.<name>.num — Text wird sonst aneinandergeklebt |
| einen Betrag anzeigen (Mail, PDF, Meldung) | .numFormat(…) — "12.345,76 €" in einem Aufruf |
| prüfen, ob ein Feld leer ist | $feld.isEmpty — nicht if (ctx.form.feld), das ist bei 0 falsch |
| wissen, wer das Formular ausfüllt | ctx.user |
| eine Fehlermeldung an ein Feld hängen | field.<name>.error(…) |
| prüfen, ob der Datensatz fehlerfrei ist | ctx.isValid() |
| wissen, ob der Datensatz schon gespeichert ist | ctx.instanceId |
| einen Wert nachträglich in die Datenbank schreiben | ctx.persist() |
| einen konfigurierbaren Wert statt einer festen Zahl | ctx.params |
| unterscheiden, ob du auf Test- oder Produktivsystem bist | ctx.system |
| das Formular nach dem Skript schließen | ctx.requestClose() |
Was bei einem Skript-Lauf passiert¶
Jedes Mal, wenn ein Hook startet, stellt KCForms ein frisches ctx
zusammen: aktuelle Feldwerte, angemeldete Person, Datensatz-ID. Das Skript
läuft, ändert vielleicht etwas daran — und danach schaut der Server nach, was
sich verändert hat, und schickt genau das an das Formular im Browser zurück.
Praktisch heißt das: Was du in ctx.form schreibst, siehst du danach im
Formular. Du musst nichts „abschicken"; das Einsammeln übernimmt KCForms.
flowchart LR
A["Hook startet<br/>(z. B. onFieldChange)"] --> B["ctx ist gefüllt:<br/>ctx.form, ctx.user …"]
B --> C["Dein Skript<br/>liest und ändert"]
C --> D["Formular übernimmt<br/>geänderte Werte,<br/>zeigt Fehler und Dialoge"]
Drei typische Fälle:
| Hook | Was das Skript typischerweise tut | Was davon ankommt |
|---|---|---|
onFieldChange |
Werte in ctx.form berechnen |
die neuen Feldwerte |
onValidate |
field.betrag.error(…) melden |
die Fehlermeldungen |
onSubmit |
ctx.requestClose() aufrufen |
das Formular schließt sich |
Änderungen wirken nur für diesen einen Lauf. Beim nächsten Hook beginnt alles
von vorn — ctx ist keine Ablage, in der etwas zwischen zwei Aufrufen liegen
bleibt. Was dauerhaft bleiben soll, gehört in ein Feld des Formulars.
Felder von ctx im Detail¶
ctx.form¶
Alle Feldwerte des aktuellen Datensatzes, zum Lesen und zum Ändern. Hinter dem Punkt steht der Feldname aus dem Formular:
Die $feld-Aliase (siehe kc-Helpers) sind eine
äquivalente Kurzform:
Was du hier hineinschreibst, übernimmt das Formular anschließend — so entstehen
Berechnungen, Vorbelegungen und voneinander abhängige Auswahllisten. Bei einer
Detail-Tabelle (kc:childTable) erwartet das Feld eine Liste, in der jede
Zeile ein eigener Eintrag mit ihren Spalten ist.
Zum Rechnen: field.<name>.num
Rechnest du direkt mit einem Feldwert, klebt JavaScript die Ziffern
aneinander, statt zu addieren — aus "3" und "4" wird "34" statt 7.
Zwei Schreibweisen lösen das; beide ergeben dasselbe:
$summe = $wert1.num + $wert2.num; // kurz
field.summe.num = field.wert1.num + field.wert2.num; // gleichwertig
.num gibt es zu jedem deklarierten Feld. Es liest den Wert als echte Zahl
— auch aus "1.234,56" — und liefert 0, wenn nichts Lesbares drinsteht.
Der Schreibzugriff (field.summe.num = …) legt die Zahl wie $summe = … ab.
Nur in Skripten: In Bedingungen (kc:visibleIf, Regeln) und in
kc:calc gibt es field.<name> nicht — dort bleibt kc.num(…) die
Schreibweise.
Der Alias bleibt dabei der Wert selbst — Vergleiche wie $land === 'DE'
funktionieren unverändert. Nicht an ctx.form.<feld> hängen: dort ist ein
leeres Feld null, und null.num bricht ab; ausgeschrieben also
kc.num(ctx.form.wert1). Siehe kc-Helpers.
Warum der Wert überhaupt Text ist — und warum das Absicht ist
Es sieht nach einer überflüssigen Hürde aus: Im Schema steht type: number,
warum kommt dann kein number an? Dahinter stecken zwei Gründe, und einer
davon ist wichtiger, als er aussieht.
Aus dem Browser kommt ohnehin alles als Text — und zwar formatiert.
Ein Betragsfeld liefert "1.234,56" mit Tausenderpunkt und Dezimalkomma.
parseFloat macht daraus 1.234; es braucht also eine Umwandlung, die das
deutsche wie das englische Format erkennt. Genau das tun .num und
kc.num.
Aus der Datenbank kommt ein number absichtlich als Text. JavaScript
kennt nur IEEE-754-Gleitkommazahlen; ein DECIMAL aus der Spalte lässt sich
darin nicht verlustfrei ablegen. 0.1 + 0.2 ergibt dort
0.30000000000000004, und ab etwa 16 Stellen fallen Nachkommastellen weg.
Bei Geldbeträgen in einem Antrag wäre das ein stiller Rechenfehler — die
unangenehmste Sorte, weil niemand ihn bemerkt. Als Text bleibt der Wert
exakt, und die Umwandlung geschieht dort, wo du sie siehst.
Ein integer kommt dagegen als echte Zahl an. Ganze Zahlen passen
verlustfrei in einen Double (bis 2⁵³). Praktische Folge war bisher, dass
$menge + $menge2 rechnet und $preis + $preis2 verkettet — dieselbe
Schreibweise, zwei Ergebnisse, abhängig von einem Schema-Detail, das man
beim Skriptschreiben nicht vor Augen hat. .num ist die eine Schreibweise,
die in beiden Fällen rechnet; deshalb die Empfehlung oben.
Detailzeilen per Skript setzen¶
Eine Detail-Tabelle lässt sich direkt mit einem JavaScript-Array belegen — auch
verschachtelt (1:n:m) und auch aus JSON.parse(…):
// ganze Tabelle neu setzen
ctx.form.positionen = [
{ artikel: 'A-100', menge: '2' },
{ artikel: 'B-200', menge: '1' }
];
// eine Zeile anhängen
ctx.form.positionen.push({ artikel: 'C-300', menge: '5' });
// Sub-Liste innerhalb einer Zeile — die Zeile muss es geben
ctx.form.kostenstellen = [{ nummer: 'K1' }];
ctx.form.kostenstellen[0].kostenpositionen = [{ wert: '10' }];
// Ergebnis eines Web-Service o. Ä.
var antwortText = '[{"artikel":"D-400","menge":"3"}]'; // z. B. von http.get(…)
ctx.form.positionen = JSON.parse(antwortText);
Nach dem Skript-Lauf werden die Zeilen automatisch in normale
Server-Datenstrukturen übernommen. Übernommen werden Texte, Zahlen,
Wahrheitswerte, Listen und Objekte; ein Date wird zu einem ISO-Text,
Funktionen entfallen.
Anhängen geht nur an eine Liste, die es schon gibt
Zum Anhängen heißt es push — es verhält sich überall gleich
(siehe den Kasten darunter zu add). Und push braucht eine Liste: In einem
onFieldChange-Hook schickt das Formular nur einen Teil der Werte mit, eine
unangetastete Detail-Tabelle fehlt dann ganz, und der Aufruf bricht mit
„undefined" ab. Wer nicht weiß, ob schon Zeilen da sind, schreibt deshalb:
add gibt es — es tut nur etwas anderes, als man denkt
add ist nicht weg, und das ist die unangenehmere Lage. Was passiert,
hängt davon ab, was gerade in der Tabelle steht:
| Untergrund | add(zeile) |
add() |
|---|---|---|
| Liste aus dem Formular | hängt an, liefert true |
Fehler (Arity) |
| Tabelle im Skript neu zugewiesen | undefined |
undefined |
| Skript aus einem importierten Formularpaket | — | hängt an, liefert die Zeile |
Die Falle ist der Rückgabewert, nicht die Verfügbarkeit:
var neu = ctx.form.positionen.add({}); // neu === true, nicht die Zeile
neu.artikel = 'C-300'; // läuft ins Leere, ohne Fehler
Auf einer Liste aus dem Formular ist add schlicht Javas List.add —
es hängt an und meldet true. Nur in Skripten aus einem importierten
Formularpaket gibt add() die neue Zeile zurück; dort blendet KCForms eine
eigene Oberfläche ein. Deshalb push: es funktioniert auf jedem der
drei Untergründe gleich.
Eine Zeile ist eine Java-Map — JSON.stringify zeigt sie nicht
Lesen über Index und Spaltenname funktioniert wie erwartet:
ctx.form.kostenstellen[0].nummer liefert den Wert, …positionen.length
die Zeilenzahl. Die JavaScript-Reflexion trägt dagegen nicht — eine Zeile
ist serverseitig eine Java-Map, kein JS-Objekt:
var zeile = ctx.form.kostenstellen[0];
zeile.nummer // "K1" ← funktioniert
JSON.stringify(zeile) // "{}" ← NICHT der Inhalt
Object.keys(zeile) // "get,values,clear,entrySet,…" ← Java-Methoden
Zum Nachsehen beim Entwickeln also nicht JSON.stringify nehmen, sondern
die Spalten einzeln lesen (oder log.info(zeile) — das Server-Log gibt die
Map lesbar aus). Dieselbe Falle wie bei el.def im
ctx.model-Kapitel.
ctx.user¶
Wer das Formular gerade ausfüllt. Diese Angaben kannst du nur lesen — ändern lässt sich hier nichts:
| Schlüssel | Beispiel |
|---|---|
name |
"oliver" (Login-Name; Aliase idUser, id) |
firstName |
"Oliver" |
lastName |
"Kehrmann" |
email |
"oliver.kehrmann@kehrmann-consulting.com" |
locale |
"de-DE" |
roles |
Gruppen ohne ROLE_-Präfix, z. B. ["ADMIN", "Entwickler"] |
permissions |
Berechtigungen ohne PERM_-Präfix, z. B. ["HR_DATA_EDIT", "FORM_STUDIO"] |
isAnonymous / anonymous |
true bei Zugriff über einen Ad-hoc-Link ohne Anmeldung |
Gruppen heißen hier roles
In Server-Skripten stehen die Gruppen unter ctx.user.roles. In
Bedingungen und Regeln im Browser
gibt es zusätzlich den Alias ctx.user.groups.
Beispiel: Formular nur mit Berechtigung bearbeiten lassen:
if (ctx.user.permissions.indexOf('HR_DATA_EDIT') < 0) {
view.add(new view.Message('Keine Berechtigung', 'Hinweis'));
ctx.requestClose();
}
ctx.formId¶
Der Kurzname des Formulars, zu dem das Skript gehört — etwa
"contact-request". In der Skript-Konsole steht hier null, solange du
dort kein Formular ausgewählt hast: Ein Skript, das frei in der Konsole läuft,
gehört zu keinem Formular.
Das ist kein Schönheitsfehler, sondern hat Folgen: Alles, was ein Formular
voraussetzt — ein PDF erzeugen (pdf.render), einen Ausdruck an eine Mail
hängen (mail.addOutput), einen Direktlink bauen
(mail.newAdhocInvocation) — braucht diesen Namen und meldet sonst, dass er
fehlt.
ctx.instanceId¶
Die Nummer des Datensatzes, an dem gerade gearbeitet wird. Solange das Formular
neu und noch nicht gespeichert ist, steht hier null — daran erkennst du
im Skript zuverlässig, ob es sich um einen neuen oder einen bestehenden
Datensatz handelt:
onSubmit läuft erst nach dem Speichern und sieht deshalb immer eine Nummer.
ctx.persist([feld, …])¶
Schreibt Werte aus ctx.form nachträglich in den schon gespeicherten
Datensatz. Das braucht man, wenn ein Wert erst feststeht, nachdem gespeichert
wurde — etwa ein Link, der die Nummer des Datensatzes enthält.
Du musst dafür weder die Tabelle kennen noch SQL schreiben: persist() findet
die richtige Tabelle selbst und behandelt die Werte genau so, wie es ein
normales Speichern täte (inklusive Längenbegrenzungen aus dem Schema).
// onSubmit — der Deep-Link steht erst NACH dem Speichern fest
ctx.form.datensatzLink = mail.newAdhocInvocation('any').url;
ctx.persist('datensatzLink'); // nur dieses Feld nachtragen
// ctx.persist(); // alle skalaren Top-Level-Felder schreiben
Ohne Argumente werden alle skalaren Top-Level-Felder geschrieben,
mit Argumenten nur die genannten — einzeln (ctx.persist('a', 'b')) oder
als Liste (ctx.persist(['a', 'b'])). Rückgabewert ist die Anzahl
betroffener Zeilen (1 = ok, 0 = Datensatz weg/fremd).
Ein Feldname, den es nicht gibt, meldet trotzdem 1
Nennst du ein Feld, das im Schema nicht steht — Tippfehler im Namen, Feld
später umbenannt —, dann schreibt persist() nichts und gibt dennoch
1 zurück. Der Rückgabewert taugt also nicht als Beweis, dass der Wert
angekommen ist. Wer sichergehen will, liest die Spalte danach einmal zurück
oder prüft den Namen gegen ctx.form.keySet().
Nur in Post-Persist-Hooks
ctx.persist() funktioniert in onSubmit / onAfterSave — dort
existiert die Instanz (ctx.instanceId gesetzt). In onBeforeSave
braucht man es nicht: dort einfach ctx.form.<feld> setzen, die
Pipeline speichert das mit. In Kontexten ohne gespeicherten Datensatz
(onLoad/onValidate/onBeforeSave, Skript-Konsole) wirft der Aufruf eine
sprechende Fehlermeldung.
persist() löst keine Hooks erneut aus (kein onSubmit-Rerun) und
deckt keine Detail-Tabellen (kc:childTable) ab — nur die
Primärzeile.
ctx.errors / ctx.addError(field, msg)¶
Hier sammelt KCForms die Beanstandungen eines Skript-Laufs. Du legst eine ab, indem du sagst, an welchem Feld sie hängen soll und was dort steht:
// onValidate
if (kc.num(ctx.form.betrag) > 1000 && !ctx.form.freigabe) {
ctx.addError('freigabe', 'Bei Beträgen über 1000 € ist eine Freigabe nötig.');
}
Die Meldung erscheint im Formular als roter Hinweis direkt unter dem genannten Feld — dort, wo der Anwender sie braucht. Mehrere Meldungen am selben Feld stehen untereinander.
Daneben gibt es ctx.addHardError(field, message). Der Unterschied zeigt sich
nur bei Formularen, die ein Speichern trotz Fehler erlauben
(allowSaveWithErrors): Eine gewöhnliche Meldung lässt sich dort übergehen,
eine harte nicht. Für Regeln, die unter keinen Umständen umgangen werden
dürfen, nimmst du die harte Variante.
field.<name> — das Feld-Handle¶
Statt den Feldnamen in Anführungszeichen zu tippen, kannst du ihn direkt
ansprechen: Zu jedem Feld des Formulars gibt es field.<name> — mit dem Wert
(.value, .num), der Frage nach dem Inhalt (.isEmpty) und den Meldungen
(.error(…), .hardError(…)). Das liest sich kürzer, und ein Tippfehler im
Feldnamen fällt sofort auf, statt still ins Leere zu laufen:
// onValidate — rollenabhängige Freigabe, produktabhängige Menge
if (kc.num($betrag) > 1000 && !ctx.user.roles.includes('MANAGER')) {
field.betrag.hardError('Ihre Rolle darf maximal 1.000 € freigeben.');
}
if ($produkt === 'bleistift' && kc.num($menge) > 100) {
field.menge.error('Sie sind nicht berechtigt, so viele Bleistifte zu bestellen.');
}
| Aufruf | Entspricht |
|---|---|
field.menge.error('…') |
ctx.addError('menge', '…') (weich) |
field.menge.hardError('…') |
ctx.addHardError('menge', '…') (hart) |
field.menge.value |
Lesen/Schreiben von ctx.form.menge (wie $menge) |
field.menge.num |
derselbe Wert als Zahl (wie kc.num($menge)) |
field.betrag.numFormat(…) |
derselbe Wert als Anzeigetext — .numFormat |
field.menge.isEmpty |
ist nichts eingetragen? — dasselbe als $menge.isEmpty (Beschreibung) |
field.ort.text |
derselbe Wert als Text, getrimmt, nie null — kurz $ort.text |
field.haken.bool |
derselbe Wert als Wahrheitswert — kurz $haken.bool |
.num ist die kurze Form fürs Rechnen — sie nimmt auch "1.234,56" und liefert
0, wenn nichts Lesbares drinsteht:
.value liefert den Wert so, wie er angekommen ist — und ist damit
identisch zu $feld. Die übrigen Zugriffe sagen im Namen, was sie liefern, und
wandeln erst beim Lesen um. Das ist Absicht: Der rohe Wert bleibt einsehbar
(wichtig, wenn du ihn unverändert an eine Schnittstelle weiterreichst), und für
number bleibt die Genauigkeit erhalten.
.isEmpty beantwortet „nichts eingetragen?" — wahr bei null, leerem Text und
reinen Leerzeichen, nicht bei 0 oder false:
Beides sind Eigenschaften, keine Methoden — also ohne (). Bei .isEmpty
ist das bewusst so: Als Methode wäre if (field.menge.isEmpty) immer wahr
(Funktionen sind truthy) und damit ein stiller Fehler; als Eigenschaft fällt ein
versehentliches isEmpty() sofort mit einem Fehler auf.
Warum es .num braucht und wann kc.num(…) die richtige Wahl bleibt, steht oben
bei ctx.form.
Die Liste entsteht bei jedem Skript-Lauf neu aus den Feldern des Formulars.
Namen mit Leer- oder Sonderzeichen sprichst du in Klammern an:
field['feld name'].error('…'). Ein eigenes Feld darf nicht field heißen —
dieser Name ist vergeben.
Beispiel-Formular
Das Demo-Formular „Skript-Validierung" (skript-validierung, Katalog
„Demo") führt beide Ebenen zusammen: field.menge.error (produktabhängig),
field.betrag.hardError (rollenabhängig) und ctx.addError('', …) +
ctx.isValid() (datensatzweit). Installierbar unter
System-Administration → Formular-Bausteine → Beispiele.
ctx.isValid() / ctx.hasErrors() / ctx.hasHardErrors()¶
Komfort-Abfragen auf ctx.errors — praktisch, um eine Aktion nur bei
sauberem Datensatz auszulösen:
// onSubmit — Mail nur senden, wenn keine Validierungsfehler vorliegen
if (ctx.isValid()) {
mail.send({ to: ctx.form.email, subject: 'Antrag ok', body: '…' });
} else {
log.info('Versand übersprungen — Datensatz hat Fehler.');
}
| Aufruf | Ergebnis |
|---|---|
ctx.isValid() |
true, solange kein Fehler gesammelt wurde |
ctx.hasErrors() |
Gegenstück — true, sobald mind. ein Fehler vorliegt |
ctx.hasHardErrors() |
true, wenn ein harter Fehler (addHardError) vorliegt |
Die Prüfung ist besonders in onSubmit / onAfterSave nützlich: Erlaubt
ein Formular allowSaveWithErrors, läuft onSubmit auch mit (weichen) Fehlern
— mit ctx.isValid() triggerst du die Folge-Aktion nur im sauberen Fall. Zu
diesem Zeitpunkt spiegelt ctx.errors die vollständige Validierung
(Schema-Pflichtfelder, onValidate, kc:validator).
Momentaufnahme, keine neue Prüfung
ctx.isValid() schaut nur nach, was bis zu diesem Punkt an Meldungen
aufgelaufen ist. Änderst du danach noch Werte in ctx.form, wird deswegen
nicht neu geprüft — eigene Prüfungen ergänzt du selbst mit
ctx.addError(…).
Achte auf die Klammern: Es heißt ctx.isValid(), nicht ctx.valid. Ohne
Klammern bekommst du die Funktion selbst statt ihres Ergebnisses — und die
gilt immer als „wahr", weshalb die Abfrage stillschweigend falsch läuft.
Wer lieber direkt in die Liste schaut: ctx.errors.length === 0.
ctx.effects¶
Ein Protokoll dessen, was das Skript ausgelöst hat — etwa ein mail.send(…).
KCForms führt es von selbst; im Skript liest man hier normalerweise nichts. Es
ist zum Nachschauen gedacht, wenn im Nachhinein die Frage aufkommt, ob eine
Mail wirklich rausging.
ctx.dialogs¶
Die Dialoge, die das Skript über view.add(…) anzeigen lässt — Meldungen,
Rückfragen, Auswahlen. Das Formular zeigt sie nach dem Skript-Lauf der Reihe
nach an. Direkt in diese Liste zu schreiben ist möglich, aber unnötig:
view.add(…) nimmt dir das ab.
ctx.attachments¶
Die Dateien, die am aktuellen Datensatz hängen:
| Pfad | Beschreibung |
|---|---|
ctx.attachments.length |
Anzahl Anlagen |
ctx.attachments[0] |
erste Anlage als {id, filename, sizeBytes, contentType, …} |
ctx.attachments.get(i) |
wie [i], klassischer JS-Stil |
ctx.attachments.maxNumber |
Limit Anzahl Anlagen für dieses Formular |
ctx.attachments.maxSize |
Limit Dateigröße in MB |
ctx.attachments ist schreibbar — Anlagen lassen
sich per Skript hinzufügen und entfernen (add/remove), und die Bytes
eines Eintrags sind direkt lesbar (bytes). Details und die vollständige
API auf der Seite Dateianlagen. Wer die Bytes
nur an eine Mail hängen will, gibt einen Eintrag weiterhin bequem an
multipart.addAttachment(att) weiter.
ctx.system¶
Map mit System-Markern aus den App-Settings. Aktuell nur ein Schlüssel:
| Pfad | Inhalt |
|---|---|
ctx.system.id |
"Prod" / "Dev" / "QS" (oder null) |
Klassische Anwendung — Mail-Versand abschalten, solange man nicht in Prod ist:
if (ctx.system.id === 'Prod') {
mail.send(msg);
} else {
log.info('DEV/QS: Mail-Versand übersprungen');
}
ctx.lang / ctx.locale — aktive Sprache¶
Die Sprache der aktuellen Anfrage, aufgelöst aus der Profil-Sprache des
Anwenders (der Accept-Language-Header, den das Frontend mitschickt).
| Feld | Inhalt |
|---|---|
ctx.lang |
nur die Sprache: "de", "sv" oder "en" — das brauchst du meistens |
ctx.locale |
Sprache samt Land, z.B. "de-DE" |
ctx.docLang |
schreibbar — Sprache für den PDF-Ausdruck (mp.addOutput(...) / pdf.render()) |
In Läufen ohne HTTP-Anfrage (Zeitplaner-Jobs) fällt die Sprache auf die
Server-Default-Sprache (de) zurück.
ctx.docLang — Ausdruck-Sprache setzen. ctx.lang ist die Sprache des
Aufrufers (der abschickenden Person). Soll der PDF-Ausdruck stattdessen in
einer anderen Sprache erzeugt werden — typisch der Empfänger-Sprache einer
Mail —, setzt das Skript ctx.docLang vor mp.addOutput(...)/pdf.render():
var empfaengerSprache = 'sv'; // z.B. aus dem Datensatz des Empfängers
var mp = new mail.MessageMultipart();
ctx.docLang = empfaengerSprache; // Ausdruck folgt dieser Sprache
mp.addOutput("Ausdruck.pdf"); // Titel, ${i18n:…} und ${date|…} in ctx.docLang
Ohne gesetztes ctx.docLang gilt ctx.lang.
Alternativ nimmt der Ausdruck die Sprache direkt als Argument entgegen —
mp.addOutput(name, format, type, cid, lang) bzw. pdf.render(lang) (Stage
27.BF). Das ist kürzer, wenn die Sprache nur für diesen einen Ausdruck gilt,
und hat Vorrang: lang-Argument → ctx.docLang → ctx.lang.
var empfaengerSprache = 'sv';
var mp = new mail.MessageMultipart();
mp.addOutput("Ausdruck.pdf", "pdf", null, null, empfaengerSprache);
var url = pdf.render(empfaengerSprache).deliver("Ausdruck.pdf");
Damit kann ein Skript sprachabhängig verzweigen — etwa eine Bestätigungs-Mail in der Sprache des Anwenders formulieren:
const betreff = ctx.lang === 'sv' ? 'Din beställning'
: ctx.lang === 'en' ? 'Your order'
: 'Ihre Bestellung';
mail.send({ to: ctx.user.email, subject: betreff, body: '…' });
ctx.params — Properties¶
Die Properties des Formulars (und die
globalen) stehen als ctx.params.<name> bereit, ihre Zusatzwerte als
ctx.paramsExtra.<name>. So bleiben Grenzwerte, Adressen oder Textbausteine
änderbar, ohne das Skript anzufassen:
if (kc.num(ctx.form.betrag) > kc.num(ctx.params.freigabegrenze)) {
ctx.addError('betrag', 'Betrag über der Freigabegrenze.');
}
i18n(key, …args) — Server-Textkatalog¶
Löst einen Schlüssel aus dem Server-Textkatalog (messages_de/sv/en.properties)
gegen ctx.lang auf. Platzhalter {0}, {1} … werden aus den zusätzlichen
Argumenten gefüllt. Ein unbekannter Schlüssel liefert den Schlüssel selbst
zurück (kein Fehler).
// messages_*.properties: validate.required = Pflichtfeld / Required field / …
field.email.error(i18n('validate.required'));
log.info(i18n('form.hook.undefined', 'onFoo')); // {0} → 'onFoo'
Für eigene, formular-spezifische Texte ist meist ctx.lang + Inline-Strings
(siehe oben) der pragmatischere Weg — der i18n()-Helfer ist dafür gedacht,
die vorhandenen System-Meldungen wiederzuverwenden bzw. zentral gepflegte
Zusatz-Keys zu nutzen.
Makros folgen ebenfalls der Sprache
In Leittexten/Druckvorlagen liefert ${session:locale} jetzt die aktive
Sprache, und die Datums-Makros (${date|EEEE, d. MMMM yyyy}) formatieren
Monats-/Wochentagsnamen in dieser Sprache. Die Währungs-/Zahlenformatierung
bleibt bewusst an das Formular gebunden (Euro-Gruppierung + €-Symbol).
ctx.requestClose()¶
Setzt einen Flag, der dem Frontend nach dem Skript-Lauf signalisiert „schließe das Formular und navigiere zurück". Idiomatisch in einer Frage-Antwort:
var q = new view.Question("Wirklich schließen?");
var yes = q.YES; yes.callback = function() { ctx.requestClose(); };
var no = q.NO;
q.add(yes); q.add(no);
view.add(q);
Kein Hook nach requestClose
requestClose() setzt nur einen Flag. Was im Skript noch
folgt, läuft normal weiter — auch mail.send(), db.query()
oder weitere view.add()-Calls. Erst nach Skript-Ende guckt
das Frontend in den Flag und schließt.
Was ctx nicht hat¶
Der Vollständigkeit halber, weil das immer wieder gefragt wird:
- Kein
ctx.changedField— welches FeldonFieldChangeausgelöst hat, steht während dieses Hooks inctx.form._changedField. Robuster ist es trotzdem, abhängige Werte immer vollständig ausctx.formzu berechnen — dann ist die Reihenfolge der Eingaben egal. - Keine Aufruf-Argumente — ein Hook bekommt keine zusätzlichen
Parameter vom Aufrufer. Für unterschiedliche Modi hängt man verschiedene
Hooks an verschiedene
kc:button-Elemente oder wertet Feldwerte aus. - Keinen Zugriff auf die HTTP-Anfrage — es gibt kein
ctx.requestoderctx.response. Ein Skript soll sich nicht darauf verlassen, wie jemand gerade auf das Formular zugreift (Browser, Cookie, Kopfzeile), sondern nur darauf, was im Formular steht und wer angemeldet ist. So läuft dasselbe Skript auch in einem geplanten Job oder über eine Schnittstelle.
Nachsehen, was gerade in ctx steht¶
Wenn ein Skript nicht tut, was es soll, ist die erste Frage meistens: Was steht überhaupt drin? Diese Zeilen schreiben es auf — einfach vorne ins Skript setzen oder in der Skript-Konsole ausführen:
log.info('formId = ' + ctx.formId);
log.info('instanceId= ' + ctx.instanceId);
log.info('user.name = ' + (ctx.user ? ctx.user.name : '<no user>'));
log.info('user.perm = ' + JSON.stringify(ctx.user && ctx.user.permissions));
log.info('system.id = ' + (ctx.system && ctx.system.id));
log.info('form keys = ' + String(ctx.form.keySet()));
log.info('attach = ' + (ctx.attachments && ctx.attachments.length) + ' Anlagen');
Object.keys(ctx.form) liefert keine Feldnamen
Man würde es erwarten, aber Object.keys(ctx.form) zeigt nicht die Felder
des Formulars — es liefert eine Liste technischer Begriffe wie get und
put. Grund ist, dass ctx.form aus der Java-Welt des Servers kommt und
sich nur wie ein JavaScript-Objekt anfühlt.
Die Feldnamen bekommst du mit ctx.form.keySet().
Die Ausgabe erscheint in der Skript-Konsole und landet zusätzlich in der Datei
logs/script.log — dort findest du sie also auch dann, wenn das Skript im
Hintergrund lief, etwa in einem geplanten Job.
