@bimetal/react
v0.40.0
Published
Generic React bindings for the BIMETAL component family: the useController primitive + one hook per component controller. No calendar specifics — the calendar React adapter lives in @bimetal/calendar-react-components.
Maintainers
Readme
@bimetal/react
Die generische React-Bindung für die BIMETAL-Komponenten-Familie. Dieses Paket bindet einen
kanonischen Controller (aus einem der @bimetal/*-headless-Pakete) via dem React-eigenen Primitiv
useSyncExternalStore in React ein — es enthält keine eigene Interaktionslogik und keine
Kalender-Spezifika. Jedes if lebt im Controller; React rendert nur den identitätsstabilen Snapshot
und übersetzt DOM-Events in Intent-Aufrufe.
Die fertigen Komponenten (<Calendar>, <Kanban>, <Table>, <Gantt>, <Whiteboard>,
<Tree>, <List>, <Picker>, <FormBuilder>/<FormRenderer>) liegen in den
@bimetal/<familie>-react-components-Paketen. Dieses Paket liefert die Bausteine darunter (den
generischen Hook, per-Komponenten-Hooks, die Fokus-Trap, Test-Utilities) für eigene React-UIs.
Installation
npm install @bimetal/react @bimetal/<familie>-headless @bimetal/<familie>-data
# z. B. @bimetal/react @bimetal/kanban-headless @bimetal/kanban-dataWas das Paket liefert
| Export | Zweck |
|--------|-------|
| useController(factory, options) | Das generische Primitiv: erzeugt einen Controller (stabil über die store-Identität) und abonniert seinen Snapshot via useSyncExternalStore. Gibt [snapshot, controller] zurück. |
| useKanbanController, useTableController, useGanttController, useWhiteboardController, useTreeController, useListController, usePickerController, useToastController, useOverlayController, useFormBuilderController, useFormRendererController | Vorkonfigurierte Wrapper um useController je Familie — elf Hooks in zehn Dateien (useFormController.ts liefert Builder und Renderer). |
| useLiveOptions(options) | Hält die Callbacks eines einmal erzeugten Controllers lebendig. Steckt bereits in allen Controller-Hooks und in useFocusTrap; direkt gebraucht wird er nur für ein eigenes Binding. Siehe Options an einen Controller durchreichen. |
| useConfigPush(ctrl, config) / useReactivePush(push, value, identity) | Reaktive Durchreichung einer Options-Leitung (setConfig, setRules, …) in einen laufenden Controller. |
| useFocusTrap(ref, options) | Fokus-Trap-Hook (Dialog-Lifecycle: enter → trap → escape → restore), gewrappt um @bimetal/a11y-dom. onEscape ist lebendig (bimetal-179). |
| renderProbe / probeRender | Test-Utilities für Re-Render-Zählung. |
Die Controller-Factories selbst (createKanbanController, createTableController,
createGanttController, createWhiteboardController, createTreeController, createListController,
createPickerController, createFormBuilderController/createFormRendererController) + ihre Typen
werden aus den jeweiligen @bimetal/<familie>-headless-Paketen re-exportiert.
Der Calendar-Hook (
useCalendarController) liegt aus historischen Gründen direkt in@bimetal/calendar-react-components, nicht hier — Calendar war die Referenz-Komponente vor dieser generischen Bindung. Alle anderen Familien laufen über dieses Paket. Zusammen sind das zwölf Controller-Hooks: elf hier, einer im Kalender-Paket.
Architektur-Hintergrund: Controller-Konzept (Headless und Bindings).
Re-exports
Zur Bequemlichkeit re-exportiert @bimetal/react alles aus @bimetal/temporal, sodass die Zeit-/
Layout-/Recurrence-Typen aus einem einzigen Paket importiert werden können.
Options an einen Controller durchreichen
setConfig (und setRules/setCustomViews) mit strukturell identischer
Config ist ein No-op — das ist seit bimetal-157 ein Vertrag des Controllers,
nicht des React-Bindings. Ein inline erzeugtes Objekt ist deshalb in jedem
Render-Layer unbedenklich:
// Genau so gedacht: das Inline-Objekt ist bei jedem Render neu; der Controller
// erkennt die strukturelle Gleichheit und löst keinen Zustandswechsel aus.
const [snapshot, ctrl] = useTableController({
store,
config: { columns: COLUMNS, rowHeight: 36 },
});Für eigene Bindings (ein Controller, den BIMETAL nicht selbst anbietet) gibt es die reaktive Durchreichung einzeln:
import { useConfigPush, useReactivePush } from '@bimetal/react';
useConfigPush(ctrl, config); // ctrl.setConfig, reaktiv
useReactivePush(v => ctrl.setRules(v), rules, ctrl); // beliebiger SetterEine Grenze, die man kennen muss: Werte, die sich nur per Identität
vergleichen lassen — eine inline definierte Funktion (nodeMeta), ein frisches
Date, ein per Factory gebautes Regel-Objekt — sind bei jedem Render neu und
gelten damit als Änderung. Memoisiere sie (useMemo, Modul-Konstante). Der
Controller meldet sich in der Entwicklung von selbst, wenn er eine solche
Instabilitäts-Signatur sieht — mit dem Namen der betroffenen Leitung.
Callbacks: die Anwesenheit ist am Mount festgelegt, die Identität lebt
Ein Controller wird einmal erzeugt (useController memoisiert auf
[store, id]) und destrukturiert seine Callbacks im Konstruktor. Ohne
Gegenmaßnahme wäre damit die Closure des ersten Renders für immer
festgenagelt — jeder Host, dessen onSubmit frischen State liest (der Normalfall
in React), schickte gegen den Stand vom Mount ab. Still, plausibel und falsch.
useLiveOptions löst das, und alle Controller-Hooks dieses Pakets (sowie
useFocusTrap und useCalendarController) fahren bereits darüber. Der Vertrag:
// Jeder Render darf einen frischen Handler übergeben — die Geste erreicht IMMER
// den zuletzt übergebenen. Kein useCallback nötig, kein Controller-Neubau.
const [snapshot, ctrl] = useFormRendererController({
store, formId,
onSubmit: values => save(values, draftFromState), // liest frischen State
});| | Verhalten |
|---|---|
| Callback beim Mount vorhanden, später getauscht | Der Aufruf erreicht die neueste Funktion. |
| Callback beim Mount vorhanden, später weggenommen (undefined) | Der Aufruf erreicht die zuletzt bekannte Funktion. Der Controller hat die Anwesenheit beim Mount destrukturiert: er kann einen Loader nicht nachträglich lernen, also darf er auch keinen verlieren — sonst stünde undefined dort, wo der Kern eine Funktion erwartet (ein weggenommenes loadChildren hinterließ einen Baum mit ewigem Lade-Chevron, ohne Fehlermeldung). |
| Callback beim Mount nicht vorhanden, später gesetzt | Wirkungslos — der Controller hat bereits undefined destrukturiert. Im Entwicklungsmodus wird das gemeldet, einmal je Feld je Hook-Instanz. Übergib den Callback ab dem ersten Render; ein stabiles No-op genügt. |
| Nicht-Funktions-Felder (options, initialValues, debounceMs, …) | Weiterhin auf den Mount-Wert festgelegt. Ihr Wert wird intern aufgefrischt, damit ein nach einem <Activity>/Suspense-Reveal neu erzeugter Controller nicht auf dem Mount-Stand einfriert — ein Reaktivitäts-Versprechen ist das ausdrücklich nicht. Wer sie zur Laufzeit ändern will, nimmt die imperative Setter des Controllers (setOptions(), setConfig()). |
Grenzen der Weiterleitung (heute nutzt kein Options-Typ eines davon; ein
künftiger darf daran nicht still brechen): die Weiterleitung ist eine gewöhnliche
Pfeilfunktion, es überlebt nur das Aufruf-Verhalten. Eigenschaften an der
Funktion, length/name, new-Barkeit, die this-Bindung, Symbol-Schlüssel
und die Prototyp-Kette gehen verloren. Ein Options-Feld, das als Methode auf
this zugreift, muss gebunden übergeben werden.
Für ein eigenes Binding (ein Controller, den BIMETAL nicht selbst anbietet) gilt dieselbe Reihenfolge wie in den Hausbindings — erst lebendig machen, dann erzeugen:
import { useController, useLiveOptions, useConfigPush } from '@bimetal/react';
const live = useLiveOptions(options); // Callbacks lebendig
const [snapshot, ctrl] = useController(() => createMyController(live), [options.store]);
useConfigPush(ctrl, options.config);