feat(ui): token-select primitive for large multi-selects
Adds tokenSelectForm() in core/Support/helpers/ui.php: an inline typeahead combobox + flat alphabetical removable list, designed for selections that outgrow the chip-header of the vendor MultiSelect (roles, permissions, and similar admin pickers). Contract: - Hidden <select multiple name="<name>[]"> is the form submission source — consumers read via $request->body() verbatim, no parsing - Items are ['id' => int, <labelKey> => string, 'key' => string?] where labelKeys default to ['description', 'label', 'name']; 'key' is an invisible fuzzy-match hint - $labelOverrides lets callers swap emptyState / removeTooltip / noMatches / clearAll / countSuffix / errorMessage with domain copy - $disabled renders pure-presentation list (no combobox, no remove) Runtime: - initTokenSelect in web/js/components/app-token-select.js is registered as 'token-select' in app-init.js; destroy()/cleanupFns contract - Syncs the hidden <select> on every mutation and dispatches 'change' so dependent UI can react Wired up: pages/admin/permissions/_form.phtml (assigned roles), pages/admin/roles/_form.phtml (permissions + assignable roles). Helper contract lives in tests/Support/Helpers/TokenSelectFormHelperTest.php; runtime contract + entrypoint registration + host usage are enforced by FrontendRuntime*ContractTest.php. Coexists with multiSelectForm() — pick tokenSelectForm() when the selected set can grow beyond ~10 items or typeahead is expected. Docs in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,15 @@ docker/ # Dockerfiles, Nginx configs, PHP configs
|
||||
- **`fetchUrl` signature:** write it as a single-expression arrow (not a block body) so the architecture test can statically locate the fragment path. Pattern: ``(uuid) => new URL(`path/${uuid}...`, appBase).toString()``.
|
||||
- **Architecture test** `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer({ fetchUrl })` has matching `*-fragment($id).php` + `*-fragment(none).phtml`.
|
||||
- **Filename rule:** URL parameters go only into the `.php` action filename (`*-fragment($id).php`). The `(none).phtml` view filename must NOT contain `($id)` — MintyPHP's router parses paren groups and breaks silently on nested ones.
|
||||
- **Token-select field** (combobox + flat removable list): use `tokenSelectForm()` from `core/Support/helpers/ui.php` for any multi-select where the selected set is potentially large and a chip-header or full dropdown becomes unreadable. The primitive renders an inline typeahead input with popover suggestions and a flat alphabetical list of selected items with per-row remove. No drawer, no grouping — keep callers flat.
|
||||
- **Server:** `tokenSelectForm($id, $name, $label, $placeholder, $items, $selected, $disabled, $labelKeys, $formId, $labelOverrides)`. Hidden `<select multiple name="<name>[]">` is the form submission contract — the consuming action reads it verbatim via `$request->body('<name>', [])`, no parsing required.
|
||||
- **Data contract:** each item is `['id' => int]` plus ONE of the keys listed in `$labelKeys` (default: `['description', 'label', 'name']`). Optional `'key' => string` is a hidden search hint that participates in fuzzy matching but is NOT displayed to the user.
|
||||
- **Label overrides:** pass `$labelOverrides` (10th arg) with any of `emptyState`, `noMatches`, `removeTooltip`, `clearAll`, `countSuffix`, `errorMessage` to replace the generic defaults with domain-specific text. Values are already-translated strings (call `t(...)` at the call-site).
|
||||
- **Readonly:** pass `$disabled = true` — combobox, clear-all and remove buttons are suppressed; the list renders as pure presentation.
|
||||
- **Client:** `initTokenSelect` from `/js/components/app-token-select.js` is auto-registered as `token-select` in `app-init.js`. The component reads the hidden `<select>` as source of truth, syncs it on every add/remove, and dispatches a native `change` event that callers can listen to for dependent UI.
|
||||
- **Remove button:** uses the shared `.app-icon-button` primitive + `<i class="bi bi-x-lg">` — never hand-roll a close button.
|
||||
- **Coexistence:** `multiSelectForm()` (vendor MultiSelect) remains available for small closed lists where the chip-header is acceptable. Pick `tokenSelectForm()` when the selected set can grow beyond ~10 items or typeahead is the expected interaction.
|
||||
- **Tests:** helper contract lives in `tests/Support/Helpers/TokenSelectFormHelperTest.php`; component lifecycle + entrypoint registration are enforced by `tests/Architecture/FrontendRuntime*ContractTest.php`.
|
||||
|
||||
### Never Do This
|
||||
|
||||
|
||||
Reference in New Issue
Block a user