kc, log, Date-Erweiterungen, $feld-Aliase¶
Das Sammelbecken für Kleinkram, der überall gebraucht wird: robuste Zahlenkonvertierung, Logging, Datumsfunktionen und die Feld-Aliase. Klein im Umfang, groß im Alltagsnutzen.
Zahlen aus Feldern: .num¶
Eingabefelder kommen aus dem Browser immer als Text, auch wenn das Schema
type: number sagt. Rechnest du direkt damit, klebt JavaScript die Ziffern
aneinander:
.num löst das — hänge es an den Feldwert:
Für ein deklariertes Feld gibt es zwei Schreibweisen — nimm die, die zum Rest deines Skripts passt:
$summe = $wert1.num + $wert2.num; // Alias, kurz
field.summe.num = field.wert1.num + field.wert2.num; // Handle
.num liest den Wert als echte Zahl — auch aus "1.234,56" — und liefert 0,
wenn nichts Lesbares drinsteht. Auch bei einem leeren Feld ist das Ergebnis
0, nicht etwa ein Fehler.
Nicht an ctx.form.<feld> hängen
ctx.form.betrag.num funktioniert nur, solange etwas im Feld steht. Ein nie
ausgefülltes Feld ist dort null, und null.num bricht mit
„Cannot read property 'num' of null" ab — ausgerechnet im häufigsten Fall.
Der $-Alias und das Handle fangen das ab (beide liefern 0). Wenn du
ausgeschrieben arbeiten willst, nimm kc.num(ctx.form.betrag).
Warum nicht einfach rechnen
Ein integer kommt im Skript schon als Zahl an, ein number bewusst als
Text (Begründung im ctx.form-Abschnitt). $menge +
$menge2 rechnet deshalb, $preis + $preis2 verkettet — dieselbe
Schreibweise, zwei Ergebnisse, abhängig von einem Schema-Detail, das man
beim Skriptschreiben nicht vor Augen hat. .num macht beide Fälle gleich.
kc.num(v) — dieselbe Umwandlung als Funktion¶
.num gibt es nur an Feldwerten. Überall sonst macht kc.num(…) genau
dasselbe — und dort ist es die einzige Möglichkeit:
| Wo | Beispiel |
|---|---|
| Werte aus der Datenbank | kc.num(db.scalar("SELECT SUM(…)")) |
| Zeilen einer Detail-Tabelle | kc.num(zeile.menge) |
| Zeilen aus CSV, XML, Web-Services | kc.num(k.Umsatz) |
| Formular-Properties | kc.num(ctx.params.grenze) |
Bedingungen (kc:visibleIf, Regeln) und kc:calc |
kc.num(betrag) > 1000 |
Die letzte Zeile ist wichtig: Dort gibt es weder $feld noch field.<name> —
in Bedingungen und Berechnungsfeldern bleibt kc.num(…) die Schreibweise.
.num delegiert intern an kc.num; gleiche Heuristik, gleiches Ergebnis.
Was kc.num akzeptiert:
| Eingabe | Ergebnis |
|---|---|
null, undefined, "", " " |
0 |
42 (Number) |
42 |
"42" |
42 |
"42.5" |
42.5 (englischer Stil) |
"42,5" |
42.5 (deutscher Stil) |
"1.234,56" |
1234.56 (deutsch mit Tausender) |
"1,234.56" |
1234.56 (englisch mit Tausender) |
"abc" |
0 (kein Parse möglich) |
Infinity, NaN |
0 |
Die Heuristik hinter Tausender-Trenner: der zuletzt auftretende Trenner (Komma oder Punkt) ist der Dezimaltrenner; alles davor wird als Tausender weggestrippt. Damit funktionieren beide Konventionen gleichzeitig — ohne dass das Skript wissen muss, welche Locale der User gerade hat.
kc.num gibt nie eine Exception
Auch bei völlig kaputter Eingabe kommt 0 zurück. Skripte, die
zwischen „leerer Eingabe" und „Eingabefehler" unterscheiden
müssen, prüfen vorher mit kc.isEmpty(...) oder mit einem
Format-Validator.
Nachschlagen: field.<name>.num¶
Das Handle-Schreibweise im Detail. field.summe.num = … legt die Zahl ab wie
$summe = …; gelesen wird sie wie oben beschrieben.
Der $-Alias bleibt dabei der Wert selbst (typeof $preis ist string
bzw. number) — .num hängt an Text und Zahl, nicht an einem Hilfsobjekt.
Vergleiche wie $land === 'DE' funktionieren deshalb unverändert.
Zahlen anzeigen: .numFormat(…)¶
.num liefert die Zahl zum Rechnen. .numFormat(…) liefert sie als
fertigen Text zum Anzeigen — Vorzeichen, Rundung, Tausendergruppen,
Dezimaltrenner, Vor- und Nachsatz in einem Aufruf:
log.info($wert1.numFormat(null, "€", 12, 2, ",", ".", 3)); // 12.345,76 €
log.info($wert2.numFormat("kg", null, 4, 0)); // kg 1234
Alle sieben Angaben sind optional; null überspringt eine Position:
| Nr. | Parameter | Vorgabe | Bedeutung |
|---|---|---|---|
| 1 | prefix |
keiner | Vorsatz, mit einem Leerzeichen abgesetzt |
| 2 | suffix |
keiner | Nachsatz, mit einem Leerzeichen abgesetzt |
| 3 | stellenInsgesamt |
keine Grenze | Obergrenze, siehe unten |
| 4 | anzNachkommastellen |
was der Wert mitbringt | wird kaufmännisch gerundet |
| 5 | dezimaltrenner |
, |
|
| 6 | separatorZeichen |
keins | ohne Angabe wird nicht gruppiert |
| 7 | separatorBlockabstand |
3 |
Ziffern je Gruppe |
stellenInsgesamt ist eine Obergrenze¶
Gemeint sind die Ziffern vor und nach dem Komma zusammen, ohne
Trennzeichen und ohne Vorzeichen — dieselbe Zählweise wie NUMERIC(12,2) in
der Datenbank. Passt der Wert nicht hinein, kommen Rauten zurück:
log.info((12345.76).numFormat(null, "€", 4, 2)); // "#### €"
log.info((0.5).numFormat(null, null, 2, 2)); // "0,50" — führende Null zählt nicht
Das ist bewusst keine stille Kürzung: eine gekappte Summe sieht richtig aus
und fällt niemandem auf. 0 oder weglassen heißt „keine Grenze".
Gerechnet wird genau¶
Gerundet wird kaufmännisch und auf dem Dezimalwert, nicht auf einer Gleitkommazahl:
Als double wäre 12345.755 in Wirklichkeit 12345.7549…, und dasselbe
Runden ergäbe 12345,75. Genau deshalb transportiert KCForms ein
number-Feld als Text — .numFormat wirft diese Genauigkeit nicht weg.
Auch als Funktion: kc.numFormat(wert, …)¶
Gleiche Parameter, ein Argument davor. Nötig überall dort, wo der Wert nicht
aus einem deklarierten Feld kommt — Datenbank-Zeilen, CSV-Zeilen,
Detail-Zeilen, ctx.params:
var zeilen = [{ kunde: "Meyer", betrag: "1234.5" },
{ kunde: "Schulz", betrag: "89012.345" }];
zeilen.forEach(function (r) {
log.info(r.kunde + ": " + kc.numFormat(r.betrag, null, "€", 0, 2, ",", ".", 3));
});
// Meyer: 1.234,50 €
// Schulz: 89.012,35 €
Leere Werte ergeben 0 — und Bedingungen kennen numFormat nicht
Ein leeres Feld liefert "0,00 €", nicht "" — dieselbe Lesart wie bei
.num. Wer stattdessen nichts anzeigen will, fragt vorher mit
kc.isEmpty(...).
Und: numFormat gibt es im Skript, nicht in Bedingungen
(kc:visibleIf, Regeln) und nicht in kc:calc — dort stehen kc.num,
kc.text, kc.bool und kc.isEmpty zur Verfügung. Die Anzeige eines
Feldes stellt man über die Feld-Eigenschaften ein
(kc:numberPrefix/-Suffix, Nachkommastellen, Trennzeichen), nicht über
eine Bedingung.
Im Formularfeld klebt das €-Zeichen, hier nicht
Die Feldanzeige setzt €, %, ° und $ bewusst ohne Leerzeichen an den Wert.
.numFormat setzt immer eines. Der Text landet in Mails, PDFs und
Protokollen, wo kein Feld danebensteht — dort ist Vorhersagbarkeit mehr
wert als die Übereinstimmung mit der Feldanzeige. Wer es anders will,
übergibt den Nachsatz ohne Leerzeichenwunsch und baut ihn selbst an:
$betrag.numFormat(null, null, 0, 2, ",", ".", 3) + "€".
Steht da nichts? .isEmpty¶
Die Frage „ist das Feld ausgefüllt?" beantwortet .isEmpty — direkt am Wert,
ohne Klammern:
log.info("leer: " + $suchbegriff.isEmpty + " / gefüllt: " + $wert1.isEmpty);
// leer: true / gefüllt: false
Wahr ist es bei null, bei "" und bei reinen Leerzeichen — also genau dann,
wenn in der Formular-Welt „nichts eingetragen" gilt. Ein Feld, das noch nie
ausgefüllt wurde, steht in der Datenbank auf NULL; auch dafür ist .isEmpty
wahr — ein optionales Feld wie doktorgrad ist der typische Fall:
0 und false sind nicht leer — das ist der eigentliche Zweck der
ganzen Familie. Eine eingetragene Menge von 0 ist ein gültiger Wert, und
false ist eine bewusste Auswahl; if ($menge) wäre bei beiden falsch,
$menge.isEmpty ist es nicht.
Ohne Klammern — und warum das wichtig ist
.isEmpty ist eine Eigenschaft, keine Methode. Schreibst du
$email.isEmpty(), bekommst du sofort einen Fehler — das ist gewollt.
Wäre es umgekehrt eine Methode, liefe if ($email.isEmpty) still ins
Falsche: Ein Funktionsobjekt ist immer truthy, die Bedingung also immer
wahr. Genau die Sorte Fehler, die .isEmpty vermeiden soll.
Am Handle: field.<name>.isEmpty¶
Dieselbe Prüfung, dieselbe Antwort — nur über das Feld-Handle. Praktisch, wenn direkt danach eine Meldung an dasselbe Feld geht:
Überall: kc.isEmpty(v)¶
Die Kurzformen gibt es nur an deklarierten Feldern und nur in Skripten.
Für alles andere ist kc.isEmpty(...) die Schreibweise, und dort ist sie die
einzige Möglichkeit: Zwischenergebnisse, Werte aus db.query(…), Zeilen einer
Detail-Tabelle oder CSV-Datei — und Bedingungen (kc:visibleIf, Regeln) sowie
kc:calc.
Dieselbe Grenze wie bei .num — und derselbe Grund, .isEmpty nicht
an ctx.form.<feld> zu hängen: dort ist ein nie ausgefülltes Feld null, und
null.isEmpty bricht ab. Ausgeschrieben also kc.isEmpty(ctx.form.doktorgrad).
kc.text(v) und kc.bool(v)¶
kc.text gibt den Wert als Text zurück — außen getrimmt und nie null,
also sicher für Verkettung und Vergleich. kc.bool liest die üblichen
Schreibweisen als Wahrheitswert: true, "true", "1", "yes", "ja", "on"
und Zahlen ungleich 0 sind wahr, alles andere falsch — dieselbe Lesart, die auch
beim Schreiben in die Datenbank gilt.
Kürzer: $feld.text und $feld.bool¶
An einem deklarierten Feld hängen beide direkt am Wert — und ebenso am Handle und mit berechnetem Namen:
log.info($land.text + " / " + $flag.bool); // Schweden / true
log.info(field.land.text + " / " + $["flag"].bool);
$land.text ist außen getrimmt und nie null — ein nie ausgefülltes Feld
ergibt "", kein undefined. Damit sind alle fünf Zugriffe des
Feld-Handles auch an der Kurzschreibweise vorhanden:
| Frage | Kurzform | Am Handle |
|---|---|---|
| der Wert | $betrag |
field.betrag.value |
| als Zahl | $betrag.num |
field.betrag.num |
| als Anzeigetext | $betrag.numFormat(…) |
field.betrag.numFormat(…) |
| steht da nichts? | $betrag.isEmpty |
field.betrag.isEmpty |
| als Text | $ort.text |
field.ort.text |
| als Wahrheitswert | $haken.bool |
field.haken.bool |
Außerhalb deklarierter Felder — Zwischenwerte, Datenbankzeilen, Bedingungen —
bleiben kc.text(...) und kc.bool(...) die Schreibweise.
kc.text fehlte im Skript
Vor dem Stand vom 26.09.2026 gab es kc.text nur in Bedingungen — der Bedingungs-
Auswerter bringt sein eigenes kc-Objekt mit. Im Skript warf derselbe
Aufruf Unknown identifier: text, obwohl die Doku beide als „die gleichen
Helper" führte. Jetzt sind beide Seiten gleich ausgestattet, kc.bool
eingeschlossen; ein Test hält das fest.
log.debug / log.info / log.warn / log.error¶
Schreibt ins Skript-Protokoll:
log.info("Kunde " + ctx.form.kundennr + " gespeichert");
log.warn("Veraltetes Skript-Pattern in onSubmit");
log.error("Mahnung-Service nicht erreichbar — fallback");
Jeder Wert darf direkt hinein¶
Zahl, Wahrheitswert, Objekt, Array, Datum — es braucht weder + "" noch
.toString():
log.info($wert1.num); // 1234.56
log.info(0.0); // 0 — nicht "0.0"
log.info(ctx.form.einverstanden) // true
log.info([1, 2, 3]); // [1,2,3]
log.info({ nr: 4711, ort: "Kiel" });
// {"nr":4711,"ort":"Kiel"}
Die Regeln dahinter sind die von JavaScript, nicht die von Java:
| Wert | Im Protokoll |
|---|---|
| Zahl | wie in JavaScript — 5, nicht 5.0; 1234.56 bleibt 1234.56 |
| Text | unverändert, ohne Anführungszeichen |
| Wahrheitswert | true / false |
| Objekt, Array | als JSON — kopierbar, ab 8000 Zeichen gekürzt |
| Datum | ISO-Zeitstempel (2026-09-29T21:04:07.940Z) |
null, undefined |
bleiben unterscheidbar |
Zusammensetzen mit + bleibt selbstverständlich richtig und ist oft
lesbarer — log.info(zeilen.length + " Zeilen") sagt mehr als eine nackte
Zahl. Neu ist nur, dass es kein Muss mehr ist.
Vor dem Stand vom 29.09.2026 ging ausschließlich Text
log.info(5) brach vorher mit
TypeError: … Cannot convert '5' … to Java type 'java.lang.String' ab —
GraalVM wandelt einen Skript-Wert nicht von sich aus in Text um. Betroffen
war alles außer dem String, also auch Objekte und Wahrheitswerte.
Aufgefallen ist es an der Kurzschreibweise $feld.num aus
vom 29.09.2026: die liefert eine Zahl, und die ging nicht durch.
Deshalb steht in älteren Beispielen dieses Handbuchs durchgängig
+ " Zeilen" oder .toString() — das waren Umgehungen dieser Grenze.
Sie funktionieren unverändert weiter.
Dasselbe gilt für job.log(…) und job.fail(…) im
Zeitplaner: auch dort landet eine Zahl
jetzt als 5 in der Lauf-Historie, nicht als 5.0.
Die Ausgabe landet in logs/script.log und je Benutzer in
logs/by-user/<login>.log, jede Zeile mit [form:<id>] davor — sowie in
der Skript-Konsole, wenn das Skript dort gestartet wurde. Ansehen lässt sie
sich unter System-Administration → Betrieb & Überwachung → System-Logs
(auch live). In der Docker-Konsole (docker compose logs backend) erscheint
sie bewusst nicht.
| Methode | Sichtbarkeit Default |
|---|---|
debug(msg) |
nur sichtbar bei aktiviertem DEBUG-Level |
info(msg) |
immer sichtbar |
warn(msg) |
immer sichtbar, gelb in der Konsole |
error(msg) |
immer sichtbar, rot in der Konsole |
Unter System-Administration → Betrieb & Überwachung → Log-Konfiguration
lässt sich der Logger kcforms.script zur Laufzeit auf DEBUG schalten —
ohne Neustart.
Date.prototype.format(pattern[, locale[, zone]])¶
Erweiterung am Date-Prototyp — formatiert ein
JavaScript-Date mit einem Java-SimpleDateFormat-Pattern:
new Date().format("dd.MM.yyyy HH:mm:ss")
// "27.04.2026 14:23:11"
new Date().format("d. MMMM yyyy", "de_DE")
// "27. April 2026"
new Date().format("EEEE, d. MMMM yyyy", "de_DE")
// "Montag, 27. April 2026"
| Pattern-Token | Bedeutung |
|---|---|
dd |
Tag, 2-stellig |
MM |
Monat numerisch, 2-stellig |
MMMM |
Monatsname ausgeschrieben (lokalisiert) |
yyyy |
Jahr 4-stellig |
HH |
Stunde 24h, 2-stellig |
mm |
Minute, 2-stellig |
ss |
Sekunde, 2-stellig |
EEEE |
Wochentag ausgeschrieben |
EEE |
Wochentag-Kurzform |
zzz |
Zeitzone-Kurzform |
Weitere Tokens: Date.format läuft serverseitig durch
java.text.SimpleDateFormat; alle dort definierten
Pattern-Buchstaben funktionieren.
Welche Uhr gilt?¶
Ein Skript läuft auf dem Server, nicht im Browser. Welche Uhrzeit
format schreibt, hängt deshalb von einer Einstellung ab:
- System-Administration → Betrieb & Überwachung → System-Einstellungen →
Zeitzone für Datums- und Zeitangaben. Ist dort eine Zeitzone gewählt
(z. B.
Europe/Berlin), rechnenformat,Date.fromStringund die eingebauten Funktionen wiegetHours()in dieser Zone. - Ohne Einstellung gilt die Uhr des Servers. Ein Server läuft oft in der Weltzeit (UTC) — dann steht im Formular „19:38", wenn es bei dir 21:38 ist.
Für einen einzelnen Aufruf nennst du die Zone als drittes Argument:
var t = new Date(Date.UTC(2026, 6, 15, 19, 38));
log.info(t.format("HH:mm zzz", null, "UTC")); // "19:38 UTC"
log.info(t.format("dd.MM.yyyy HH:mm", "de", "Asia/Tokyo")); // "16.07.2026 04:38"
Ein unbekannter Zonenname ist ein Fehler — das Skript bricht mit einer Meldung ab, statt still eine falsche Uhrzeit zu schreiben.
Eine geschriebene Uhrzeit bleibt, wie sie ist
Schreibt ein Skript eine Uhrzeit als Text in ein Feld, steht sie dort
fest. Stellst du die Zeitzone später um, ändern sich nur neue Einträge.
Wer sichergehen will, schreibt die Zone dazu: format("dd.MM.yyyy HH:mm zzz").
Locales akzeptiert beide Schreibweisen — "de_DE" und
"de-DE" (IETF-Tag). Ohne Locale-Argument gilt das Server-Default.
Date.fromString(s[, pattern[, locale[, zone]]])¶
Parst einen Datums-String zurück in ein JS-Date:
Date.fromString("27.04.2026", "dd.MM.yyyy");
// Date object für 2026-04-27 00:00:00 lokal
Date.fromString("27. April 2026", "d. MMMM yyyy", "de_DE");
// Date object — funktioniert nur mit passender Locale
Ohne pattern erwartet Date.fromString das Format
"EEE MMM dd HH:mm:ss zzz yyyy" (so wie Date.toString() ausgibt). Für
eigenen Code immer ausdrücklich ein Muster angeben.
Falsches Format → klare Fehlermeldung:
$feld — Form-Field-Aliase¶
Vor jedem Skript-Lauf installiert KCForms automatisch
Getter/Setter-Aliase auf globalThis, damit man ctx.form.feld
als $feld schreiben kann:
// Lang
ctx.form.summe = kc.num(ctx.form.wert1) + kc.num(ctx.form.wert2);
// Kurz — funktional identisch
$summe = kc.num($wert1) + kc.num($wert2);
Was bekommt einen Alias?
- Alle Felder des Formulars (aus dem Schema) — auch solche, die gerade
leer sind — sowie alle Schlüssel, die beim Start in
ctx.formstehen. - Vier Testwerte für die Skript-Konsole:
$text,$number,$date,$flag.
Was nicht aliased ist:
- Schlüssel, die weder im Schema stehen noch beim Start in
ctx.formwaren (etwa ein Hilfswert, den das Skript selbst anlegt): Dafür gibt es keinen Alias —ctx.form.hilfswertverwenden. - Feldnamen mit Leerzeichen, Punkten, Bindestrichen — die sind keine
gültigen JS-Identifier. Du erreichst sie über
$['weird name'](siehe gleich) oderctx.form['weird name'].
Berechneter Name: $["feld"]¶
$beitrag1 ist ein fest getippter Bezeichner — aus einer Schleifenvariablen
lässt sich so ein Name nicht zusammensetzen. Dafür gibt es die eckigen
Klammern:
var summe = 0;
for (var i = 1; i <= 2; i++) {
summe += $["wert" + i].num;
}
$["summe"] = summe;
log.info($["summe"].numFormat(null, "€", 0, 2, ",", ".", 3)); // 300,50 €
Gelesen und geschrieben wird dasselbe wie bei $feld: $["wert1"] und
$wert1 sind derselbe Wert, null kommt als leerer Text an, und .num,
.numFormat und .isEmpty hängen genauso daran. Dieselbe Schreibweise gibt
es am Handle — field["wert" + i].value — und ausgeschrieben als
ctx.form["wert" + i].
Ein Name, den es nicht gibt, bricht ab
$["beitrga1"] meldet
$["beitrga1"]: Es gibt kein Datenelement mit diesem Namen. Das ist
Absicht und dasselbe Verhalten wie bei der festen Schreibweise, wo
$beitrga1 ein ReferenceError ist. Die dynamische Form darf nicht stiller
sein als die feste: Eine Schleife, die einen Schritt zu weit läuft, würde
sonst ab da mit 0 weiterrechnen.
Tolerant fragen geht trotzdem — "beitrag1" in $ ist wahr oder falsch,
ohne Abbruch, und ctx.form["beitrga1"] liefert undefined.
Komfort, kein Pflichtgebrauch
Die $feld-Schreibweise ist eine Kurzform. Wer alles über
ctx.form.x schreibt, macht sofort klar, woher der Wert kommt. Bei
kurzen Berechnungen sind die Aliase aber angenehm.
field.<name> — Fehler an ein Feld hängen¶
Für jedes Feld gibt es außerdem ein Handle field.<name>:
field.menge.error('…') (wie ctx.addError('menge', '…')),
field.menge.hardError('…') und field.menge.value. Details in der
ctx-Referenz.
Wann ist welcher Helper richtig?¶
| Situation | Helper |
|---|---|
| Eingabewerte rechnen | kc.num |
| Pflicht-Check schreiben | kc.isEmpty |
| Text fürs PDF erzeugen | Date.format |
| Datum aus User-Input parsen | Date.fromString |
| Diagnose/Audit | log.info |
Kurzform für ctx.form.x |
$x |
Diese Vier sind die Bausteine, mit denen 90 % aller Form-Hooks
auskommen. Alles darüber hinaus ist Spezial-API (mail, view,
pdf, delivery, fs, xml).