1
0
Files
breadcrumb-the-shire/docs/reference-frontend-javascript.md
fs c7b8fd516a feat: extend module platform with UI slots, runtime components, CLI tooling and {{userId}} search support
Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.

New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering

New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
  module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
  FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 22:19:56 +01:00

370 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend JavaScript
Letzte Aktualisierung: 2026-03-18
## Ordnerstruktur
- `/web/js/core`
- technische Basismodule (DOM/Factories)
- `/web/js/components`
- wiederverwendbare UI-Features
- `/web/js/pages`
- seitenspezifische Hilfen (z. B. Listeninitialisierung)
## Initialisierung
- `/web/js/app-boot.js`
- wird im `<head>` geladen
- setzt früh UI-States (no-js/js, Sidebar-/Contrast-LocalStorage)
- `/web/js/app-init.js`
- Modul-Entrypoint für Standardseiten
- initialisiert Runtime + globale Komponenten zentral
- `/web/js/app-login-init.js`
- reduzierter Entrypoint für Loginseiten
## Modulkonvention (components/pages)
Empfohlenes Muster:
1. `init...()` Funktion exportieren
2. Früher Guard auf fehlende Elemente
3. Keine stillen Fehler verschlucken
4. Nur DOM/UI-Verhalten, keine Fachlogik
5. Lifecycle-Contract für Runtime-Komponenten: `init(root, config)` gibt API mit `destroy()` zurück
6. Runtime-Komponenten dürfen keinen eigenen Auto-Init via `DOMContentLoaded`/Sideeffect enthalten
## Component Runtime (Restmigration)
- Zentrale Runtime: `web/js/core/app-component-runtime.js`.
- Runtime-Page-Configs:
- Standardseiten: `<script type="application/json" id="page-config-app-components">...</script>`
- Loginseiten: `<script type="application/json" id="page-config-login-components">...</script>`
- Runtime liest Config über `readPageConfig(...)` und mountet registrierte Komponenten pro Entrypoint:
- `web/js/app-init.js` (default)
- `web/js/app-login-init.js` (login)
- Komponenteninterner Contract:
- Init-Signatur: `initX(root, config)`
- Rueckgabe: `{ destroy() }`
- `destroy()` muss alle Listener/Timer/Observer abbauen.
- Hybrid Host-Policy:
- Host-gebundene UI-Komponenten nutzen explizite `data-app-component`-Hosts im Markup.
- Tabs sind host-gebunden und laufen ausschliesslich ueber `data-app-component="tabs"`.
- Globale Utilities ohne natuerliches Host-Element werden runtime-global (`scope: 'global'`) registriert, aber mit demselben Lifecycle-Contract.
- Root-Strict gilt fuer host-gebundene Komponenten: keine `document.querySelector`-Fallback-Pfade.
- Projektseitige Custom-Globals auf `window.*` sind verboten; Komponenten kommunizieren ueber Core-Channels.
## UI Storage Contract
- Persistente UI-States laufen fuer Runtime-Komponenten ueber `web/js/core/app-ui-storage.js`.
- Storage-Keys sind namespaced (`namespace:version:scope:*`) statt ungeordnet verteilt.
- Hard-Cut-Policy: keine Legacy-Key-Fallbacks und kein Legacy-Key-Sync in migrierten Runtime-Komponenten/Core-Modulen.
Template für neue Komponenten:
- bestehende Komponenten als Referenz verwenden (`/web/js/components/*.js`)
## Page Module Contract (Hard Cut)
Für seitenbezogene Initialisierung gilt verbindlich:
- Kein inline `<script type="module">` in `pages/**` oder `templates/**`.
- Pro Seite genau ein externes Modul unter `web/js/pages/*` laden.
- Serverseitige Daten für dieses Modul über
`<script type="application/json" id="page-config-...">...</script>` bereitstellen.
- Page-Module lesen Config zentral über `readPageConfig(...)` aus `web/js/core/app-page-config.js`.
- JS-`src` in `pages/**` und `templates/**` nur über `assetVersion('...js...')`.
## Defensive DOM-Nutzung
Nicht jede Seite hat jedes Element.
- Vor Zugriff immer Element-Existenz prüfen.
- Optional zentrale Helper (`app-dom.js`) verwenden.
- Module sollen auf nicht-passenden Seiten sauber no-op sein.
## Grid.js-Integration
- Gemeinsame Initialisierung über Core/Page-Utilities.
- Vendor-spezifische CSS-Anpassungen in:
- `/web/css/vendor-overrides/gridjs.css`
- Bei dynamischen Rows mit Lightbox/Tooltips: nach Render Refresh triggern.
### Grid URL State Sync
`createServerGrid(...)` synchronisiert bei `urlSync: true` den Grid-Zustand zentral in die URL.
- Search + Filter (bestehendes Verhalten)
- Sortierung über `order` + `dir`
- Pagination über `page` (1-basiert)
Optionale Konfiguration:
```js
createServerGrid({
// ...
urlSync: true,
urlState: {
pageParam: 'page',
sortOrderParam: 'order',
sortDirParam: 'dir',
cleanFirstPage: true,
syncPage: true,
syncSort: true
}
});
```
Hinweise:
- `page=1` wird bei `cleanFirstPage: true` nicht in der URL gehalten.
- Übernommene URL-Werte werden validiert:
- `page >= 1`
- `dir` nur `asc|desc`
- `order` nur bekannte Sort-Key-Spalten
- Bei Search-/Filter-Änderung wird auf Seite 1 zurückgesetzt.
### Row Interaction (Globaler Standard)
Listen mit `rowDblClick.getUrl(...)` nutzen standardmäßig eine zugängliche Row-Interaktion:
- sichtbare Action-Spalte rechts (`Open`/`Edit`)
- Doppelklick bleibt als Fallback aktiv
- `Enter` auf fokussierter Zeile navigiert zur selben URL
- Action-Header/Tooltip zeigt den Enter-Hinweis
Optionale Konfiguration über `createServerGrid(...)`:
```js
createServerGrid({
// ...
rowDblClick: {
getUrl: (rowData) => new URL(`admin/users/edit/${rowData?.cells?.[1]?.data}`, appBase).toString(),
},
rowInteraction: {
enabled: true,
showActionButton: true,
keepDblClick: true,
enableEnterOnRow: true,
actionColumnLabel: 'Actions',
resolveActionLabel: (url) => (url.includes('/edit/') ? 'Edit' : 'Open'),
}
});
```
Defaults:
- `rowInteraction.enabled`: `true`, wenn `rowDblClick.getUrl` vorhanden ist
- `showActionButton`: `true`
- `keepDblClick`: `true`
- `enableEnterOnRow`: `true`
- `actionColumnLabel`: `language.actions.column` oder `'Actions'`
Hinweis:
- Falls eine Seite `actions.enabled: true` explizit setzt, wird keine zusätzliche automatische Row-Action-Spalte injiziert.
### Grid Error Recovery
Bei Grid-Fehlern rendert die Factory automatisch einen Retry-CTA im Error-State.
- Label kommt aus `language.errorRetry` (Fallback: `Retry`).
- Klick auf Retry lädt den Grid-Server-Request erneut.
### Filter UX 2.0 (Globaler Standard)
Für Listen gilt standardmäßig:
1. Quick Search sichtbar außerhalb des Drawers.
2. Restliche Filter im rechten Drawer (`data-filter-drawer-*`).
3. Drawer arbeitet `apply-first` (Draft bis `Apply`).
4. Aktive Filter werden als Chips angezeigt (`data-active-filter-chips`).
5. Filter-Reset erfolgt über Chips (`Clear all filters`), nicht im Drawer.
JS-Verkabelung:
- Standard-Entrypoint ist `initStandardListPage(...)` aus `web/js/core/app-grid-factory.js`.
- Der Wrapper kapselt intern `createServerGrid(...)` + (bei `mode: 'drawer'`) `initListFilterExperience(...)`.
- Bei Apply/Clear wird URL mit `pushState` fortgeschrieben.
- Save-Filter-Usecases lesen den angewandten Zustand über `gridConfig.baseUrl()`.
- Chip-State-Logik ist zentral im Wrapper verdrahtet (Default: `app-list-filter-state.js`).
Search-only-Ausnahme:
- Listen ohne zusätzliche Filter initialisieren über denselben Wrapper mit `mode: 'search-only'` (ohne Drawer/Chips).
### Standard-List-API
Für Listen-Templates gilt:
- `initStandardListPage({ grid, filters, hooks })` verwenden.
- `grid.filterSchema` serverseitig aus `filter-schema.php` (`gridSchemaClientFilters(...)`) übergeben.
- Optional zusätzliche Spezialfilter über `grid.extraFilters`.
- Bei Drawer-Listen `filters.mode: 'drawer'`, bei Search-only `filters.mode: 'search-only'`.
Regel:
- keine direkten Aufrufe von `gridFiltersFromSchema(...)` in Listen-Templates
- keine direkten Aufrufe von `initListFilterExperience(...)` in Listen-Templates
- keine inline `filters: [...]`-Arrays in Listen-Templates
- MultiSelect-Filter immer als CSV-Key (`*_ids`, `actions`, `event_types` etc.)
Serverseitig passendes Template-Markup:
- normale Select-Filter über `selectFilter(...)`
- MultiSelect-Filter über `multiSelectFilter(...)`
### List Titlebar Standard
Für Listen-Titlebars gilt:
- Titlebar über `templates/partials/app-list-titlebar.phtml` rendern.
- Titel über `$listTitle` setzen.
- Aktionen über `$listTitleActionsHtml` als Slot (captured HTML) übergeben.
Beispiel:
```php
<?php
$listTitle = t('Users');
ob_start();
?>
<a role="button" class="primary" href="admin/users/create"><?php e(t('Create user')); ?></a>
<?php
$listTitleActionsHtml = ob_get_clean();
require templatePath('partials/app-list-titlebar.phtml');
?>
```
### List Tabs Standard
Für Tenant- oder Kontext-Tabs in Listen gilt:
- Tabs über `templates/partials/app-list-tabs.phtml` rendern.
- Tab-Items über `$listTabsItems` mit `href`, `label`, `active` übergeben.
- Optional `id` über `$listTabsId` setzen.
### List Purge Action Standard
Für Purge-Buttons in Titlebar-Actions gilt:
- Purge-Markup über `templates/partials/app-list-purge-action.phtml` rendern.
- Formular/CSRF/Button/Confirm werden zentral übergeben via:
- `$listPurgeEnabled`
- `$listPurgeFormId`
- `$listPurgeAction`
- `$listPurgeConfirmMessage`
- `$listPurgeButtonLabel`
### Aside Grouped Navigation + Persisted Details State
Für gruppierte Sidebar-Navigation mit einklappbaren `details` gilt:
- Zentraler Storage-Helper: `web/js/core/app-details-open-state.js` (`initPersistedDetailsGroup(...)`).
- Standardgruppen über `details[data-details-key]`.
- Persistenz-Aktivierung über:
- `data-details-storage="..."` (allgemein, z. B. Detailseiten)
- `data-aside-details-storage="..."` (Aside-Panels)
- Optionales Aktiv-Verhalten im Aside über `data-aside-details-open-active="1"`:
- aktive Gruppe bleibt immer offen
- effektive Open-Keys = `persistedOpenKeys activeOpenKeys`
Admin-Aside-Standard:
- Storage-Key: `aside-admin-sections-v1`
- Gruppenkeys:
- `admin-organization`
- `admin-roles-permissions`
- `admin-automation`
- `admin-monitoring`
- `admin-logs`
- `admin-system`
### Detail Page Standard
Für Create/Edit-Detailseiten gilt:
- Hauptformular über `data-standard-detail-form="1"` explizit opt-in markieren.
- Initialisierung läuft zentral über `initStandardDetailPage(...)` aus `web/js/core/app-detail-page-factory.js`.
- Runtime-Init erfolgt über `web/js/app-init.js` (Komponente `standard-detail-page`).
Standardverhalten:
- Dirty-State über serialisierte `FormData`-Diffs (initial vs. aktuell).
- `beforeunload`-Warnung nur bei ungespeicherten Änderungen.
- Back/Cancel-Link in `.app-details-titlebar h1 a` fragt bei Dirty-State per Confirm.
- `Cmd/Ctrl+S` triggert primären Save/Create-Button (`data-detail-save-primary="1"`).
- Submit-Lock/Confirm laufen zentral über die Detail-Action-Policy.
- Bei Dirty-State wird in der Titlebar automatisch ein kleiner Hinweis mit Feldanzahl gezeigt.
### Detail Action Policy
Für standardisierte Detailseiten gilt zusätzlich:
- `initStandardDetailPage(...)` bindet intern `initDetailActionPolicy(...)` aus `web/js/core/app-details-action-policy.js`.
- Aktivierung pro Seite über `data-detail-action-policy="1"` auf `.app-details-titlebar` (Default im Shared-Partial aktiv).
- Confirm nicht mehr inline per `onclick/onsubmit`, sondern über `data-detail-confirm-message`.
- Aktionssemantik über `data-detail-action-kind` (z. B. `save`, `save-close`, `delete`, `danger`).
- Optionaler Lock-Override über `data-detail-submit-lock="none"` (Default: `form`).
### Global Confirm Actions
Für allgemeine Form-/Button-Confirms außerhalb der Detail-Policy gilt:
- zentral über `web/js/components/app-confirm-actions.js`
- Dialog-Engine zentral über `web/js/core/app-confirm-dialog.js` (`confirmDialog.confirm(...)`)
- Confirm-Text über `data-confirm-message` bzw. auf Detail-Aktionen `data-detail-confirm-message`
- optionale Dialog-Meta über
- `data-confirm-title`, `data-confirm-confirm-label`, `data-confirm-cancel-label`, `data-confirm-variant`, `data-confirm-focus`
- analog für Detail-Aktionen: `data-detail-confirm-*`
- kein direktes `window.confirm(...)` in App-JS
- keine inline `onclick="return confirm(...)"` / `onsubmit="return confirm(...)"` verwenden
### Frontend Telemetry Standard
Für UX-/Fehler-Telemetrie im Frontend gilt:
- zentral über `web/js/core/app-telemetry.js` (`telemetry.capture(...)`)
- `warnOnce(...)` sendet standardisiert `frontend.warn_once`
- AJAX-/Fetch-Fehler laufen zentral über den globalen Fetch-Hook als `frontend.ajax_error`
- keine neuen Console-only-Warnpfade für relevante UX-Probleme ohne Telemetrie
- Payloads nur mit Whitelist-Feldern und ohne sensitive Rohdaten (keine Tokens/Query-Parameter/Stacktraces)
### Detail Validation Summary Standard
Für serverseitige Formularfehler in Create/Edit-Templates gilt:
- Fehlerausgabe über `templates/partials/app-details-validation-summary.phtml`.
- Keine inline `<div class="notice" data-variant="error">`-Blöcke mit `foreach ($errors as $error)` mehr.
- Standardtitel kommt aus `t('Please review the following errors')`.
- In Action-Handlern strukturierte Feld-Errors als `$validationSummaryErrors = $errorBag->toArray();` bereitstellen.
Template-Beispiel:
```php
<?php
$validationSummaryErrors = $validationSummaryErrors ?? ($errors ?? []);
require templatePath('partials/app-details-validation-summary.phtml');
?>
```
Optional strukturierte Einträge:
- `['message' => '...', 'fieldName' => 'email']`
- `['message' => '...', 'fieldId' => 'email']`
- `['email' => ['Required']]` (FormErrors-ähnliche Map)
JS-Verhalten über `initStandardDetailPage(...)`:
- Klick auf Validation-Link fokussiert das passende Feld.
- Bei vorhandener Summary wird initial das erste betroffene Feld (oder Summary) fokussiert.
- Betroffene Felder werden zentral mit `aria-invalid="true"` und `aria-describedby` (Summary-ID) markiert.
## Laufzeit-Check (ohne Node)
Prüfen im Browser:
1. DevTools öffnen (Console, optional Preserve log aktivieren).
2. Kernseiten laden (`/admin/users`, `/address-book`, Audit-Listen).
3. Filter setzen/zurücksetzen, Row-Action-Button + Enter + Doppelklick sowie Bulk-Aktionen testen.
4. Erwartung: keine JavaScript-Errors und keine Unhandled Promise Rejections.