Zum Inhalt

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.

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

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.

Der Reiter „Skripte" im Formular-Editor: links die Liste der Hooks, rechts der Code mit ctx-Zugriffen

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.form eintragen. 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:

var name = ctx.form.kontaktName;
ctx.form.summe = kc.num(ctx.form.wert1) + kc.num(ctx.form.wert2);

Die $feld-Aliase (siehe kc-Helpers) sind eine äquivalente Kurzform:

$summe = $wert1.num + $wert2.num;

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:

ctx.form.positionen = ctx.form.positionen || [];
ctx.form.positionen.push({ artikel: 'C-300', menge: '5' });

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:

if (ctx.instanceId === null) {
  // wird gerade zum ersten Mal ausgefüllt
}

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:

field.summe.num = field.wert1.num + field.wert2.num;

.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:

if (field.begruendung.isEmpty) field.begruendung.error('Bitte ausfüllen.');

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 Feld onFieldChange ausgelöst hat, steht während dieses Hooks in ctx.form._changedField. Robuster ist es trotzdem, abhängige Werte immer vollständig aus ctx.form zu 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.request oder ctx.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.

Die Skript-Konsole: oben die eingetippten log.info-Zeilen, darunter die Ausgabe mit formId, Benutzername und System-Kennung