Skip to content

feat(combobox-web): headless combobox, migration steps 1 and 2 (mobx store, view model) - #2504

Draft
r0b1n wants to merge 8 commits into
feat/combobox-selector-safety-netfrom
feat/combobox-mobx-store
Draft

r0b1n wants to merge 8 commits into
feat/combobox-selector-safety-netfrom
feat/combobox-mobx-store

Conversation

@r0b1n

@r0b1n r0b1n commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2499 (feat/combobox-selector-safety-net). Research branch for the headless combobox; see docs/design/headless-widgets.md and docs/design/headless-combobox.md, both added here. #2499 stays a plain test PR and carries nothing from this one.

Migration steps 1 and 2 of the combobox design doc, as commits:

  1. docs: add headless widgets design docs — the two design documents.
  2. build(combobox-web): add mobx, mobx-kit and brandi, run jest in strict action mode — dependencies, jest.config.js + jest.setup.ts with enforceActions: "always" and [MobX] warnings turned into errors.
  3. refactor(combobox-web): make the selector store observable with mobx — every selector and provider annotated with makeObservable; the setState({}) trigger kept so this commit is a pure store change.
  4. refactor(combobox-web): replace the forced re-render with a snapshot hook — useObservableSnapshot (useSyncExternalStore over a MobX reaction) subscribed to the search term.
  5. docs(combobox-web): record migration step 1 as an openspec change — openspec/changes/combobox-mobx-store.
  6. feat(combobox-web): add the combobox core, its container and view model — src/core/: the generic envelope, the combobox ViewModel contract, and the composition root in the style of datagrid-web and gallery-web: comboboxConfig (static properties read once, including the selector family), ComboboxGateProps (the Mendix values the Core derives from), CB_TOKENS, ComboboxContainer + createComboboxContainer, FilterInputEventService (the debounced filter-input action as a setup component, disposed on unmount), ComboboxCore (injected with selector, config, gate and the service; cached slices, bound commands) and useComboboxCore. SingleSelection and MultiSelection read the snapshot only; the downshift hooks take the Core. getSelector, useGetSelector and the hand-built preview core props are gone; the editor preview uses createPreviewContainer. Specs: 19 Core and hook tests, 12 container tests, 6 service tests.
  7. refactor(combobox-web): drop the deprecated search-term callback — onAfterSearchTermChange and its three tests.
  8. docs(combobox-web): record migration step 2 as an openspec change — openspec/changes/combobox-viewmodel, status lines and the composition-root decision in the design docs.

Deferred on purpose, listed in the step 2 change: open state, input value and highlight stay with downshift until step 3 (Behavior), which also replaces the selectors' updateProps with derivations over the gate; option content is still rendered by the captions providers until step 4 (classes and parts), so the editor preview overrides survive untouched. brandi-react is not added: views read a plain snapshot and never resolve from the container.

Verification: pnpm exec tsc --noEmit clean; pnpm run test 18 suites, 213 tests, 5 snapshots unchanged, no -u; lint 0 errors; pnpm run build OK. E2E has not run: the build workflow only triggers for PRs targeting main, so it runs once #2499 merges and this branch is rebased.

@r0b1n
r0b1n force-pushed the feat/combobox-selector-safety-net branch from d7cf817 to b6e38a2 Compare October 30, 2026 08:34
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch 2 times, most recently from e3126e8 to f281abc Compare October 30, 2026 08:50
@r0b1n
r0b1n force-pushed the feat/combobox-selector-safety-net branch 2 times, most recently from e251deb to ca117d9 Compare October 30, 2026 08:58
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch from f281abc to ed6c215 Compare October 30, 2026 08:58
@r0b1n
r0b1n force-pushed the feat/combobox-selector-safety-net branch from ca117d9 to 2a2a4ee Compare October 30, 2026 13:30
Design docs for the headless widget concept (generic) and the combobox
contract with its safety-net rules and the 8-step migration plan.
@r0b1n r0b1n changed the title refactor(combobox-web): convert the selector store to MobX (migration step 1) feat(combobox-web): headless combobox, migration steps 1 and 2 (mobx store, view model) Oct 30, 2026
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch from ed6c215 to 7095a5c Compare October 30, 2026 14:30
r0b1n added 7 commits October 30, 2026 16:57
…t action mode

Adds mobx 6.12.3, @mendix/widget-plugin-mobx-kit and brandi for the
headless combobox plan: the selector store becomes observable (step 1)
and the core is assembled from a brandi container like the data grid
and gallery widgets (step 2). The package now runs Jest through its own
jest.config.js so a setup file can enable enforceActions: always and turn
the MobX strict-mode warning into a test failure.
Every selector, options provider, captions provider and the lazy-load
provider now declares its state with makeObservable. Mendix values enter as
observable.ref only, props updates and setters are actions, derived getters
are computed. The Selector interface and the re-render trigger in
useGetSelector are unchanged so the characterisation tests pass as they are.
StaticOptionsProvider loses three field redeclarations that would have
shadowed the base class observables.
…hook

useGetSelector no longer registers onAfterSearchTermChange to call
setState. It subscribes to the observable search term through
useObservableSnapshot, a useSyncExternalStore wrapper around a MobX
reaction. The hook is the snapshot hook from the headless design docs and
will read the ViewModel in step 2.
Adds the combobox-mobx-store change (proposal, design, tasks) and a status
line under step 1 of the headless combobox migration plan.
Migration step 2 of docs/design/headless-combobox.md. The core derives
a plain ViewModel from the selector store as cached slices, so a View
can memoise on them, and the two selection views render from that
snapshot only. The core is assembled by a brandi container like the
data grid and gallery widgets: a frozen config for the static Studio
Pro properties, a props gate for the Mendix values, the selector family
bound per instance, and a setup host for the debounced filter-input
action. getSelector and useGetSelector are replaced by the container
and useComboboxCore; the editor preview uses the same container over
the preview selectors.
Migration step 2, third commit. onAfterSearchTermChange leaves the
OptionsProvider interface, the base provider and the two preview
providers. The view model subscription covers the search term, so the
side channel has no caller. Its three characterisation tests go with it;
the remaining defaults test keeps its file.
Adds openspec/changes/combobox-viewmodel with the container, config and
gate decisions, ticks task 3.2 of combobox-mobx-store, and records the
step 2 status and the composition-root decision in the design docs.
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch from 7095a5c to 08c22d9 Compare October 30, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant