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

14 KiB
Raw Blame History

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:

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

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