major
#29643
TL Views: Anpassung der React-Oberfläche an das Corporate Design eines Kunden mit eigener React-Komponentenbibliothek (Leitfaden und fehlende Erweiterungspunkte)
Kontext
Die React-Oberfläche (com.top_logic.layout.view, .view.xml) setzt Anwendungen deklarativ aus UIElement`s zusammen. Jedes Element erzeugt ein `ReactControl, das Zustand serverseitig hält, an das Modell bindet und ihn per SSE mit einer React-Komponente des Clients synchronisiert.
Ein Kunde bringt häufig eine eigene React-Komponentenbibliothek mit, die sein Corporate Design samt Stylesheets festlegt. Diese Bibliothek kennt TopLogic nicht: Sie synchronisiert keinen Zustand und bindet keine Daten. Das Ticket beschreibt, wie eine solche Bibliothek angebunden wird, damit Anwendungsoberflächen dem Corporate Design des Kunden entsprechen. Zusätzlich schließt es die Lücken in der Engine, die dem heute noch entgegenstehen. Der Beschreibungstext ist als dauerhafter Leitfaden gedacht.
Leitfaden
Grundsatz
Die Kundenbibliothek wird als zusätzliche Darstellungsschicht unterhalb des bestehenden Komponentenvertrags angebunden, nicht als zweiter Baukasten neben ihm. .view.xml, `UIElement`s, Controls, Datenbindung und Zustandssynchronisation bleiben unverändert. Ersetzt wird pro Komponentenname die React-Komponente, die den Zustand eines Controls darstellt. Außerdem werden die Design-Tokens des Kunden auf ein TL-Theme abgebildet. Das Corporate Design ist damit eine Frage der Auslieferung: Dieselben Views erscheinen in der Gestaltung des Kunden.
Die Nahtstelle ist bereits vorhanden: Server und Client verbindet ausschließlich der Komponentenname (drittes Konstruktorargument von ReactControl, z.B. "TLButton") zusammen mit den Zustandsschlüsseln, die das Control per putState veröffentlicht und die die Komponente über useTLState() liest. Gesten meldet die Komponente über useTLCommand() zurück.
Stufe 1: Theme aus den Design-Tokens des Kunden (immer)
Die Tokens des Kunden (Markenfarben, Schriften, Radien, Schatten) werden in einem eigenen Theme abgebildet (<theme name="acme" extends="default"> in einer Konfiguration des UIThemeService, vgl. tl-react-theme.config.xml und docs/faq/react-theme-tokens.md). Wo möglich, verweisen die CSS-Variablen des Kunden und die TL-Tokens auf dieselbe Quelle. Diese Stufe deckt bereits einen großen Teil des Erscheinungsbildes ab, gerade bei den komplexen Komponenten, die nicht ersetzt werden (Tabellen, Bäume, Flussdiagramme, Layout-Panels).
Stufe 2: Adapterkomponenten für Blatt-Widgets
Für jede TL-Komponente, zu der die Kundenbibliothek ein Gegenstück hat (Button, Text-/Zahl-/Datumseingabe, Auswahl, Checkbox/Schalter, Reiter, Dialog-/Fensterrahmen, Menüs, Snackbar), entsteht in einem Kundenmodul ein schlanker Adapter. Er bildet den TL-Zustand auf die Props der Bibliothek ab und die Callbacks der Bibliothek auf TL-Kommandos:
{{{#!js import { React, useTLState, useTLCommand, rootClassName, replace } from 'tl-react-bridge'; import type { ButtonState } from 'tl-react-bridge'; import { Button } from '@acme/ui';
const AcmeButton = ({ controlId }) => {
const s = useTLState<ButtonState>();
const send = useTLCommand();
return <Button id={controlId} className={rootClassName(s)}
disabled={s.disabled}
onClick={() => send('click')}>{s.label}</Button>;
};
replace('TLButton', AcmeButton); }}}
Das Bundle des Kundenmoduls wird über ClientResources als module-script mit requires="tl-react-bridge" angemeldet; das Stylesheet der Bibliothek als stylesheet (Aufbau des Moduls: docs/faq/new-react-module.md, docs/faq/new-ui-element.md, Abschnitt 3). Die Bibliothek muss dafür nichts über TopLogic wissen.
Stufe 3: Neue UIElements nur für Widgets ohne TL-Gegenstück
Hat der Kunde ein Widget, für das es in TL keine Entsprechung gibt (z.B. ein eigener Stepper, eine KPI-Kachel), entsteht ein UIElement + ReactControl + Adapterkomponente nach docs/faq/new-ui-element.md. Das Element wird generisch und über Konfiguration parametrisiert gebaut, nicht für eine einzelne Ansicht.
Was vermieden wird
- Kein paralleler Elementsatz (<acme-button>, <acme-field>, …): Er verdoppelt die Bindungs- und Synchronisationslogik, macht Views unportabel und zwingt dazu, jede Korrektur in TL doppelt nachzuziehen. Die View beschreibt, was angezeigt wird; das Kundenmodul entscheidet, wie es aussieht.
- Die komplexen Kompositionen bleiben die von TL: TableViewControl, der Fill-/Layout-Vertrag, Fenstermanager, Drag & Drop und Tastatur-Scopes werden über Tokens und css-class gestaltet und nicht auf ein Fremd-Grid umgebaut. Letzteres wäre ein eigenes Projekt.
Voraussetzungen an die Kundenbibliothek
- Eine React-Instanz. Die Bibliothek wird gegen das React aus tl-react-bridge gebündelt: Die Aliase auf die Shims für react, react-dom und react/jsx-runtime stehen in vite.config.ts, wie in com.top_logic.layout.react.wysiwyg bzw. …chartjs, und tl-react-bridge ist external. Die React-Peer-Version der Bibliothek muss zur Version der Bridge passen.
- Nur kontrollierte Komponenten. Der Zustand gehört dem Server. Komponenten, die intern eigenen Zustand halten (unkontrollierte Eingaben, selbstverwaltetes Offen/Ausgewählt), werden im kontrollierten Modus (value + onChange) verwendet, sonst weicht ihr Zustand vom Server ab.
- Globales CSS. Resets und generische Klassennamen des Kunden können mit den TL-Stylesheets kollidieren. Die Stylesheets werden über ClientResources geladen und frühzeitig auf Seiteneffekte geprüft.
- Portale, Fokus, Außenklick. Dialoge und Popover der Bibliothek, die in document.body portalen, müssen mit TL-Fokusfalle (focus-trap), Außenklick-Behandlung (outside-press) und Fenstermanager zusammenspielen. Hier sind die meisten Integrationsfehler zu erwarten.
Fehlende Erweiterungspunkte (unter diesem Ticket umzusetzen)
1. Kontext-Provider für Bibliotheken
Jedes Control wird als eigene React-Wurzel eingehängt (createRoot in tl-react-bridge.ts) und nur in TLControlContext.Provider eingebettet. Bibliotheken, die einen ThemeProvider, einen Intl-Provider oder einen CSS-in-JS-Cache benötigen, haben keinen Ort, ihn einzubringen. Benötigt wird ein registrierbarer Wurzel-Wrapper in der Bridge, den ein Kundenbundle anmeldet und der jede eingehängte Control-Wurzel umschließt.
2. Ausdrücklicher Ersetzungsmechanismus
Die Ersetzung einer Komponente funktioniert heute nur, weil registry.ts eine einfache Map ist (die letzte Registrierung gewinnt) und die Skripte in einer bestimmten Reihenfolge geladen werden. Das ist fragil und nicht sichtbar. Benötigt wird eine deklarierte Zuordnung (z.B. ein Komponentensatz in ClientResources: TLButton → AcmeButton) oder ein register, das eine stille Ersetzung meldet, sofern sie nicht ausdrücklich als Ersetzung gekennzeichnet ist.
3. Veröffentlichter Zustandsvertrag der ersetzbaren Komponenten
Die Zustandsschlüssel einer Komponente sind heute String-Konstanten, die über die putState-Aufrufe der Java-Controls verteilt sind. Ein Adapter-Autor braucht sie dokumentiert und stabil: ein typisiertes Zustandsinterface je ersetzbarer Komponente (idealerweise gemeinsam für Java und TypeScript definiert oder generiert) sowie eine Liste der Komponenten, die als ersetzbar vorgesehen sind.
Lösung
Der Leitfaden steht zusammen mit der umgesetzten API als FAQ in docs/faq/customer-component-library.md (verlinkt aus CLAUDE.md).
1. Wurzel-Wrapper
tl-react-bridge bietet registerRootWrapper(wrapper, { order? }) (bridge/root-wrapper.ts). Alle React-Wurzeln der Bridge – die per mount() eingehängten Controls und der Tooltip-Host – rendern ihren Inhalt innerhalb aller registrierten Wrapper; der Wrapper mit der kleinsten order liegt außen, bei gleicher order gilt die Registrierungsreihenfolge. Verschachtelte Controls werden über TLChild im Baum ihres Elternteils gerendert, Portale erben den Kontext ihres Baums; beide sind damit automatisch umschlossen. Ein nach dem Einhängen registrierter Wrapper wirkt sofort: die eingehängten Wurzeln rendern neu, ihr Teilbaum wird dabei neu aufgebaut (lokaler React-Zustand geht verloren, Serverzustand bleibt). Bibliotheken registrieren ihre Wrapper daher beim Laden ihres Skripts.
2. Ausdrückliche Ersetzung
registry.ts bietet replace(name, component). Eine Ersetzung gewinnt unabhängig von der Ladereihenfolge immer gegen register. Ein zweites register für einen belegten Namen und eine zweite Ersetzung desselben Namens werden als Konsolenwarnung gemeldet. Ein eingehängtes Control sucht seine Komponente beim Einhängen, ein verschachteltes bei jedem Rendern; die Kompositionen von TL beziehen ihre Blatt-Komponenten über die Registry, eine Ersetzung wirkt daher auch in Toolbars, Dialogen usw. Ausnahme: Schaltflächen, die eine Komponente selbst zeichnet (z.B. Hilfe/Schließen in der Titelleiste von TLWindow), folgen der Ersetzung von TLWindow, nicht der von TLButton.
3. Zustandsvertrag über msgbuf
Der Zustand aller ersetzbaren Komponenten ist in com.top_logic.layout.react/src/main/java/com/top_logic/layout/react/state/state.proto beschrieben; die Felddokumentation ist der veröffentlichte Vertrag, der Dateikopf listet die Zuordnung Komponentenname → Nachricht:
TLButton (ButtonState), TLToggleButton (ToggleButtonState), TLCheckbox (CheckboxState), TLTextInput (TextInputState), TLPasswordInput (PasswordInputState), TLNumberInput (NumberInputState), TLDatePicker (DatePickerState), TLSelect (SelectState), TLDropdownSelect/`TLOptionChips`/`TLSegmentedChoice` (DropdownSelectState), TLTabBar (TabBarState), TLWindow (WindowState), TLDialog (DialogState), TLMenu (MenuState), TLSnackbar (SnackbarState). Gemeinsame Basen: ControlState, FieldState, TypingFieldState; Kind-Controls im Zustand: ChildControl.
- Java: Die Controls verwenden die generierten Eigenschaftskonstanten (ButtonState.LABEL__PROP) in putState; die privaten Schlüsselkonstanten entfallen. Die Zustandsverwaltung von ReactControl (Map + Patches) und das übertragene JSON bleiben unverändert. ReactMenuControl.MenuEntry verwendet für den Eintragstyp das Enum MenuState.EntryType; die öffentlichen Zustandsschlüssel-Konstanten von ReactNumberInputControl (INPUT_MODE*), ReactWindowControl, ReactDialogControl und ReactSnackbarControl sind durch die generierten Konstanten ersetzt.
- TypeScript: msgbuf erzeugt aus derselben Datei reine Typdefinitionen (react-src/state/control-state.ts, per option TypeScript); Enums werden zu String-Unions mit den externen Namen (@Name). Die Typen werden aus tl-react-bridge exportiert, useTLState<T>() liefert den Zustand typisiert (useTLState<ButtonState>()).
- Absicherung: TestControlStateSchema prüft für jede ersetzbare Komponente, dass jeder Schlüssel im gesendeten Zustand deklariert ist, und dass die externen Namen der Java-Enums mit den Schema-Enums übereinstimmen.
- Grenze: Der Wert eines Feldes (FieldState.value) bleibt json; seine Form ist je Nachricht dokumentiert (msgbuf kann ein geerbtes Feld nicht verengen). Felder senden kein disabled, sondern editable.
Voraussetzung ist msgbuf mit TypeScript-Ziel (msgbuf.version 1.2.3).
Beispielmodul
com.top_logic.demo.react.corporate ist eine lauffähige Referenz: eine Stellvertreter-Bibliothek ohne TL-Bezug (BrandProvider, BrandButton, BrandCheckbox) und Adapter, die per registerRootWrapper den Provider und per replace die Komponenten TLButton und TLCheckbox einbringen. In tl-demo-react wird es über das Maven-Profil corporate-example zugeschaltet (Build und Start mit dem Profil, z.B. MAVEN_ARGS=-Pcorporate-example); ohne Profil bleibt die Demo unverändert.