You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Closes#625. The cross-repo chain is now complete:
| Repo | Released | Notes |
|---|---|---|
| django-admin-rest-api 1.3.0 | PyPI | emits `widget: "custom"` + `widget_class` |
| django-admin-react 1.8.0 | this PR | dispatches to consumer-registered widgets |
## Plugin protocol
A consumer registers a vanilla mount fn for any `formfield_overrides`
widget the SPA doesn't natively render:
```html
<!-- before the SPA bundle runs (custom change_form_template, a
shared base template, or any <script> ahead of the SPA's bundle): -->
<script>
window.darFieldWidgets = window.darFieldWidgets ?? {};
window.darFieldWidgets['mypkg.widgets.MarkdownEditor'] = {
mount(container, props) {
// Render whatever — vanilla JS, jQuery, mini-React, etc.
// `props.value` — current draft (live via getter)
// `props.onChange` — emit a new value
// `props.error` — per-field validation errors
// `props.widgetClass` — the dotted class path
// return optional cleanup fn called on SPA unmount.
},
};
</script>
```
Why vanilla JS rather than React: the SPA's React is bundled +
tree-shaken; exposing it on a global so consumer React modules
could use it is fragile across bundle rebuilds. A vanilla
mount-fn contract keeps the consumer's widget framework-agnostic
(jQuery, Stimulus, mini-React, vanilla DOM all work). The SPA
wraps the mount fn in a thin React effect with the latestProps
ref pattern so value / onChange / error reach the widget without
re-mounting on every render.
## Two registration paths
- **No-build path:** assign to ``window.darFieldWidgets[<class>]``
in a regular ``<script>``. Works with no npm publish, no
consumer-side bundler.
- **Module path:** import ``registerFieldWidget`` from ``@dar/ui``
in a consumer's own SPA build (when they ship their own React
module sideloaded into the page). Module-level registrations
win over the window global on conflict.
## Fallback when no registration matches
The SPA renders a default text input + an amber note
(``Custom widget <class> is not registered; using the default text
input.``). The operator can still complete the form; the gap is
explicit and recoverable, not a silent break. Consumers can keep
affected models on the legacy admin via ``LEGACY_ADMIN_URL_PREFIX``
deeplink until they wire the widget.
## What's new
- ``frontend/packages/ui/src/custom-widget.ts`` — the registry
module + ``registerFieldWidget`` / ``lookupFieldWidget`` exports.
- ``frontend/packages/form/src/CustomWidgetMount.tsx`` — React
adapter wrapping the consumer's mount fn (latestProps ref so
re-renders forward value/onChange/error without re-mounting).
- ``FieldInput.tsx`` — new render branch for ``widget === 'custom'``
with the registered-widget path and the missing-registration
fallback.
- ``contract.ts`` — ``WidgetHint`` extends ``'custom'``;
``FieldDescriptor`` gains ``widget_class?: string``.
- README — new "Custom widgets (formfield_overrides +
registerFieldWidget)" section with worked example, props table,
and fallback semantics.
- i18n catalogs (es / pt / fr) — translated the two new fallback
strings.
## Dep bump
``django-admin-rest-api ^1.2.0`` → ``^1.3.0``. The new
``widget_class`` field + ``"custom"`` widget value land in 1.3.0;
older API versions will never trigger the new SPA branch (they
just emit no hint).
## Verification
- ``pnpm test`` — **222 / 222 ✓** (up from 216; +6 new in
``custom-widget.test.ts``)
- ``poetry run pytest -q`` — **64 / 64 ✓** on Django 4.2.30
- ``pnpm -r typecheck`` ✓
- ``pnpm lint`` ✓
- ``pnpm -w build`` ✓
## Minor bump rationale
``1.7.0`` → ``1.8.0``. New user-visible capability (consumer-
supplied custom widgets render in the SPA) per SemVer's "additive
features" guideline. Matches the symmetric ``1.3.0`` minor on
``django-admin-rest-api``.
Closes#625.
Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments