Skip to content

Dynamic values: consumer API — standalone settings dialog, labels, portalContainer - #95

Merged
nicolas-jaussaud merged 8 commits into
mainfrom
feat/dynamic-values-consumer-api
Sep 27, 2026
Merged

nicolas-jaussaud merged 8 commits into
mainfrom
feat/dynamic-values-consumer-api

Conversation

@KapplerJulien

@KapplerJulien KapplerJulien commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Why

Products that pick dynamic values from outside a field — a page builder's own dialog, a block editor's toolbar (Tangible Certificates' template builder is the first) — had no way in. The settings modal and its helpers were internal to the bundle, the API object needed a host field (dynamicValuesAPI(value, setValue, …)), and the dialog's strings were hardcoded.

Everything here is additive; defaults reproduce today's behaviour and BaseWrapper is unchanged.

What

Exports (assets/src/index.tsx): DynamicFieldSettings, DynamicValueSettings (a value's settings sub-form on its own) and a dynamicValues namespace — createAPI, buildGroupedChoices, stringify, parse, regex, defaultLabels.

createDynamicValuesAPI({ types, categories, mode }) returns the same shape as dynamicValuesAPI with the value-bound members inert, so the modal, wrappers and choice builder don't care which one they got.

DynamicFieldSettings props

  • modes — ['builtin'] hides the Field Type toggle (a consumer whose "custom" means something of its own).
  • labels — every string of the dialog.
  • portalContainer — the same prop renderField() accepts. The dialog goes through usePortalContainer like every other dialog in the module, which creates the interface wrapper (tf-interface, tf-context-<name>, tui-interface) inside it; the hook learns to take the portal container and wrapper explicitly when there is no context above it. Our styles are scoped under those classes and TUI portals the pickers' panels into the nearest .tui-interface, so the panel is styled by the module's own TUI copy (without it a consumer's TUI styles the panel; we hit a half-width search box).
  • context — the tf-context name when rendered outside any field.
  • onSubmit(raw, meta) — raw is the full [[name::setting=value]] token, the saved-value format render_value() takes (it used to omit the delimiters, an internal convenience of the editor's serialiser — review follow-up). meta describes the pick: mode, value, label, group, settings.
  • Blank settings no longer travel in the token: [[user_meta::meta_name=x]], not [[user_meta::source=::meta_name=x]] (the parser defaults missing settings to '' anyway).

Refactor: the modal's settings loop moves into DynamicValueSettings; the modal uses buildGroupedChoices instead of its own copy. Both work without a ControlContext above them (EnsureControlContext provides the same context renderField() builds).

Consumer example

const { DynamicFieldSettings, dynamicValues } = window.tangibleFields

<DynamicFieldSettings
  open={ open }
  onOpenChange={ setOpen }
  dynamic={ dynamicValues.createAPI({ categories: ['user', 'general'] }) }
  modes={ ['builtin'] }
  labels={ { title: 'Insert a field', select: 'Field' } }
  portalContainer={ host }
  context="wp"
  onSubmit={ (token, meta) => insertChip(token, meta.label) }
/>

Verification

  • Jest: 10 new tests in tests/jest/cases/dynamic/consumer-api.test.tsx (factory, modal props, portal wrapper, editing, sub-form); dynamic-values cases at 54 passing, full suite at 572.
  • Storybook: Dynamic Values / Settings dialog — autodocs plus four stories (default, built-in narrowed, custom labels, editing a reference). Built and driven headlessly.
  • Real page: driven from a WordPress block editor via window.tangibleFields — grouped list, settings sub-form, edit-in-place, blank settings dropped.
  • tsc reports no errors in the changed files (the two in index.tsx are pre-existing).

Notes for review

  • An earlier revision carried an extraGroups prop (consumer-provided groups listed after the registry). Dropped: the consumer that motivated it keeps its document-scoped choices in its own picker, and without a use case it was the one client-side notion in a registry-driven dialog.
  • An earlier revision mounted the dialog straight into a consumer's element and added the interface classes to it. Replaced by portalContainer through the shared hook, so no consumer DOM is touched and the module keeps one concept for "where overlays go".
  • Windows: storybook build needs Node ≥ 20.19 and two native bindings npm ci skips (@oxc-parser/binding-win32-x64-msvc, @rolldown/binding-win32-x64-msvc) — unrelated to this PR, noting it for whoever builds Storybook next.

🤖 Generated with Claude Code

KapplerJulien and others added 4 commits September 17, 2026 17:28
Products that pick dynamic values from outside a field — a page builder's
dialog, a block editor's toolbar — had no way in: the settings modal and
its helpers were internal, and the API object needed a host field.

- index.tsx exports DynamicFieldSettings, DynamicValueSettings (a value's
  settings sub-form on its own) and a `dynamicValues` namespace: createAPI,
  buildGroupedChoices, stringify, parse, regex, defaultLabels.
- createDynamicValuesAPI({ types, categories, mode }) returns the same
  shape as dynamicValuesAPI without a value to read or write.
- DynamicFieldSettings gains `modes` (one mode hides the Field Type
  toggle), `extraGroups` (consumer-provided groups listed after the
  registry; keys submitted unchanged, no settings), `labels` (every
  string — the dialog was hardcoded English), `container` (which also
  receives the interface wrapper classes while open, so the module's
  styles and TUI's portal roots resolve inside it) and `context`; onSubmit
  receives a second argument describing the pick. Blank settings no longer
  travel in the token. Defaults keep today's behaviour; BaseWrapper is
  unchanged.
- The settings loop moves out of the modal into DynamicValueSettings, and
  the modal uses buildGroupedChoices instead of duplicating it. Both work
  without a ControlContext above them (EnsureControlContext).
- Tests for the factory, the modal's new props and the sub-form; a
  Storybook page with five stories under Dynamic Values / Settings dialog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The consumer that motivated it (Certificates) keeps its document-scoped
custom fields in its own picker, behind a Dynamic field / Custom field
toggle of its own, so nothing lists client-side groups next to the
registry. Without a use case the prop was the one client-side notion in
a registry-driven dialog; the rest of the consumer API stands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@KapplerJulien KapplerJulien changed the title Dynamic values: consumer API — standalone settings dialog, extra groups, labels Dynamic values: consumer API — standalone settings dialog, labels, container Sep 17, 2026
KapplerJulien and others added 2 commits September 17, 2026 18:32
`container` was a name of this dialog's own, and mounting straight into
the consumer's element meant adding the interface classes to someone
else's DOM. The module already has the concept: renderField() accepts
`portalContainer`, and usePortalContainer creates the interface wrapper
inside it for every dialog. The standalone dialog now takes the same
prop and goes through the same hook, which learns to accept the
portal container and wrapper explicitly when there is no context above
it. No consumer DOM is touched; the wrapper is the module's own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@KapplerJulien KapplerJulien changed the title Dynamic values: consumer API — standalone settings dialog, labels, container Dynamic values: consumer API — standalone settings dialog, labels, portalContainer Sep 17, 2026
@KapplerJulien
KapplerJulien marked this pull request as ready for review September 21, 2026 16:14

@nicolas-jaussaud nicolas-jaussaud left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

The only thing we might want to change is the format of raw returned by onSubmit() (see associated comment)

Comment thread assets/src/components/dynamic/settings-modal/DynamicFieldSettings.tsx Outdated
KapplerJulien and others added 2 commits September 23, 2026 15:46
…HasSettings

Review follow-ups (#95):

- The dialog's onSubmit returned the token without its delimiters — an
  internal convenience of the ProseMirror editor, whose serialiser adds
  them back, that had become the public contract. Everywhere else the
  saved value carries [[ ]], so a consumer can store what it receives and
  hand it to render_value() as is. BaseWrapper no longer wraps; the editor
  strips before storing in its node; custom mode wraps what was typed
  unless the brackets were typed too. `meta` still carries the value name
  and settings for anyone who prefers structure to parsing.
- valueHasSettings() from choices.ts replaces the dialog's own copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nicolas-jaussaud

Copy link
Copy Markdown
Contributor

Merged despite failing checks. Tests are passing and issue is linked to #96

@nicolas-jaussaud
nicolas-jaussaud merged commit 3ca37ca into main Sep 27, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants