feat(isdirty): Navigations-Guard gegen ungespeichertes Verlassen von Bearbeitungsmasken

Zentraler DirtyGuard (dirtyguard.ts) mit DirtyMaskHelper-Klasse und
3-Button-Bestätigungs-Modal (Speichern/Verwerden/Abbrechen). Form meldet
Dirty-State via onDirtyChange nach oben; input-Event ergänzt change für
frühzeitiges Dirty-Marking. Alle App-Navigationen (Breadcrumb, Menü,
In-Masken-Buttons, Abbrechen) laufen über guardedNavigate. Auto-Redraw
bei dirty per e.redraw=false unterbunden, damit DOM-Eingaben erhalten
bleiben (Input-Widget hat kein Live-Binding).

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-19 18:21:58 +02:00
1 parent fd30049a32
commit a67921ec74
10 files changed
+462 -120

No files matched your search

+32 -9
View File
@@ -12,6 +12,13 @@ export interface iForm {
stickyButtons?: boolean
/** Die Funktion gibt das HTMLFormElement zurück, welches in der Form-Componente angelegt wird */
onformcreate?: (form:HTMLFormElement) => void
/** Callback bei Änderung des Dirty-State. Wird mit true aufgerufen,
* sobald der Nutzer Eingaben vornimmt, und mit false, nachdem das
* Formular erfolgreich gespeichert oder zurückgesetzt wurde. Die
* umgebende Maske nutzt dies, um ihren eigenen isDirty-State mit dem
* der Form synchron zu halten und so den Navigations-Guard
* (dirtyguard.ts) zu speisen. */
onDirtyChange?: (dirty: boolean) => void
}
export const Form:m.ClosureComponent<iForm> = (vnode) => {
@@ -29,6 +36,9 @@ export const Form:m.ClosureComponent<iForm> = (vnode) => {
}
function onchanged (e:Event) {
isDirty = true
if (vnode.attrs.onDirtyChange != undefined) {
vnode.attrs.onDirtyChange(true)
}
if (vnode.attrs.onchanged != undefined) {
vnode.attrs.onchanged(e)
}
@@ -37,6 +47,9 @@ export const Form:m.ClosureComponent<iForm> = (vnode) => {
e.preventDefault()
if (vnode.attrs.onsubmit(form) == true) {
isDirty = false
if (vnode.attrs.onDirtyChange != undefined) {
vnode.attrs.onDirtyChange(false)
}
}
}
function registerFormElement (f:HTMLFormElement) {
@@ -46,22 +59,24 @@ export const Form:m.ClosureComponent<iForm> = (vnode) => {
}
function cancel(e:Event) {
e.preventDefault()
if (canCloseForm() == true) {
isDirty = false
if (vnode.attrs.oncancel != undefined) {
vnode.attrs.oncancel(e)
}
else {
history.back()
}
// Der Dirty-Check beim Abbrechen erfolgt jetzt durch den
// Navigations-Guard der Maske (dirtyguard.ts), da oncancel die
// Navigation auslöst. Die Form selbst fragt hier nicht mehr nach,
// um eine doppelte Bestätigung zu vermeiden. isDirty wird nicht
// zurückgesetzt, damit der Guard den aktuellen State sieht.
if (vnode.attrs.oncancel != undefined) {
vnode.attrs.oncancel(e)
}
else {
e['redraw'] = false
history.back()
}
}
function reset(e:Event) {
if (canCloseForm("reset")) {
isDirty = false
if (vnode.attrs.onDirtyChange != undefined) {
vnode.attrs.onDirtyChange(false)
}
// Hier passiert nichts.
// Da mithril nach jedem User-Event (wie z.B. einem 'Click') ein redraw() ausführt, wird hier die Form
// neu gezeichnet und zwar mit den ursprünglichen Daten aus den zu grundeliegenden Datenmodel, das ja
@@ -109,6 +124,10 @@ export const Form:m.ClosureComponent<iForm> = (vnode) => {
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
element.addEventListener("change", onchanged)
// 'input' feuert schon während des Tippens (vor
// Blur), so dass die Maske dirty wird, auch wenn
// der Nutzer sofort eine Breadcrumb klickt.
element.addEventListener("input", onchanged)
}
registerFormElement(form)
}
@@ -131,6 +150,10 @@ export const Form:m.ClosureComponent<iForm> = (vnode) => {
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
element.addEventListener("change", onchanged)
// 'input' feuert schon während des Tippens (vor
// Blur), so dass die Maske dirty wird, auch wenn
// der Nutzer sofort eine Breadcrumb klickt.
element.addEventListener("input", onchanged)
}
registerFormElement(form)
}