Migration guide
Every breaking release has its own section below, newest first. If you are crossing several releases at once, work from the bottom of the page upwards.
This is the only page that names superseded APIs; everywhere else in this documentation only the current one exists.
Upgrading to v0.7.3 (from v0.7.2)
Both peer floors move by a patch each, and both of those patches carry behaviour this library used to work around. Nothing this library exports is renamed or removed, and the additions - a field naming the component it is drawn as, two more builder shorthands and byTag() - need nothing from existing code. Two things do reach yours: one type that narrowed, and layout and action overrides you may have written that were inert and are not any more. There is a checklist at the end of this section.
The peer ranges
| Peer | Before | Now |
|---|---|---|
@dynamicforms/vue-forms | ^0.17.0 | ^0.17.1 |
@dynamicforms/vuetify-inputs | ^0.9.1 | ^0.9.2 |
{
"dependencies": {
"@dynamicforms/vue-forms": "^0.17.1",
"@dynamicforms/vuetify-inputs": "^0.9.2",
"@dynamicforms/vuetify-modal-form-kit": "^0.7.3"
}
}Neither peer release renames or removes an export, so the upgrade itself is the version bump; what it moves is in the three sections below.
offset and order take what Vuetify paints
FormLayout.ColumnProps declared offset and order as number | 'auto' | boolean and kept only the number, so three quarters of what the type admitted vanished on the way to the serialized props. <v-col> renders each into the class offset-<value> / order-<value>, and Vuetify's stylesheet declares no offset-auto, no order-auto and no boolean form, so the declaration is now what the layout keeps: offset is a number, and order is a number, 'first' or 'last'.
col.props.order = 'first'; // a real Vuetify class, kept now, dropped before
col.props.offset = 'auto'; // a compile error now, and nothing on screen beforeThe type checker finds the 'auto' and boolean assignments for you. There is nothing to replace them with, because they never rendered: drop them, or state the width you meant.
An action option only a breakpoint states now takes effect
vuetify-inputs 0.9.2 takes the fields taking part in the responsive cascade from each breakpoint's own keys, where it took them from the base options. An action's value carries each option as a member of its own, so an option the base never named was dropped at every width, and is resolved now:
// draws no icon at any width before 0.9.2, and one from md upwards from it
new Action({ value: { label: 'Save', md: { icon: 'save-outline', showIcon: true } } });Load the dialogs whose action values state options at a breakpoint alone: those options are on screen from this release. Where the old rendering is the one you want, take the option out of the breakpoint.
A row, column or form layout is unaffected, whatever its breakpoints state. Each of those carries its children under a single key - rows, columns, components - which the base always states, so the field set the cascade walked always held it.
The same release makes getRenderOptionsForBreakpoint() answer name, defaultConfirm, defaultReject and passthroughAttrs, which <df-modal> now reads an action's flags through - the reading <df-actions> draws the button from. defaultConfirm and defaultReject belong to the action rather than to a screen width, so a breakpoint stating either is a type error where it was silently inert:
new Action({ value: { label: 'Cancel', defaultReject: true, md: { defaultReject: true } } }); // TS error at mdMove the flag to the value itself, which is where the dialog and the button row both read it.
An abort from an asynchronous handler is an answer
vue-forms 0.17.1 converts an AbortEventHandlingException at the trigger whether or not the chain ran through an asynchronous handler. Action.execute() resolves with the exception where it rejected with it, so a dialog action whose handler refuses to close the dialog reads the same either way:
// before: this catch fired for an abort raised in an `async` handler
try { await save.execute(); } catch (e) { if (e instanceof AbortEventHandlingException) reportRefusal(e); }
// now: the abort is the answer, and the catch is for genuine failures alone
const answer = await save.execute();
if (answer instanceof AbortEventHandlingException) reportRefusal(answer);Inside the chain nothing moves: a handler that awaits its own supr still meets the abort as a throw. What changes is the public edge, and with it the keyboard route - a refused run no longer reaches app.config.errorHandler as though the handler had failed.
Checklist for 0.7.3
- Bump both peers -
@dynamicforms/vue-forms@^0.17.1,@dynamicforms/vuetify-inputs@^0.9.2. - Let the type checker find the
offset/orderassignments of'auto'or a boolean, and drop them. - Load the dialogs whose action values state an option at a breakpoint alone: that option renders now.
- Search your action values for
defaultConfirm/defaultRejectinside a breakpoint object, and move them onto the value. - Replace any
catchthat read anAbortEventHandlingExceptionoffexecute()with a read of the answer.
Upgrading to v0.7.0 (from v0.6.x)
Both peers move at once — eleven releases of @dynamicforms/vue-forms, one of @dynamicforms/vuetify-inputs — and the package becomes ESM-only. Nothing this library exports is renamed or removed, so most projects compile untouched; the work is in your own use of the two peers, plus six points on this library's own surface, three of which are about the actions that settle a dialog. There is a checklist at the end of this section.
The peer ranges and the node floor
| Peer | Before | Now |
|---|---|---|
@dynamicforms/vue-forms | ^0.6.0 | ^0.17.0 |
@dynamicforms/vuetify-inputs | ^0.8.1 | ^0.9.1 |
vue | ^3.4 | ^3.5.2 |
lodash-es moves to ^4.17.21, and engines.node is >=22.12, where the package declared none. The old lodash-es floor did not work: 4.17.12 throws ReferenceError: root is not defined as it loads. The vue floor is vue-forms' own, which vuetify-inputs and this release both restate.
The two peers are one upgrade: vuetify-inputs 0.9.x requires vue-forms 0.17.0, and its 0.8.x line cannot be combined with vue-forms 0.17.0. Installing them one at a time leaves npm reporting unsatisfiable peers.
{
"dependencies": {
"@dynamicforms/vue-forms": "^0.17.0",
"@dynamicforms/vuetify-inputs": "^0.9.1",
"@dynamicforms/vuetify-modal-form-kit": "^0.7.0",
"vue": "^3.5.2",
"vuetify": "^3.9"
}
}Your own use of the peers migrates at the same time
This library re-exports none of the peer API your application builds forms out of: every Field, Group, List, Action and validator is vue-forms' or vuetify-inputs', and both cross a breaking range here. Work through the vue-forms migration guide, which is written for exactly this jump, and the vuetify-inputs migration guide. The sections below cover only what those two cannot know about, which is this package's own surface.
Four vue-forms breaks are worth searching for before you upgrade rather than after. The first three announce nothing at all — no log, no throw — so the code keeps compiling and stops working; the fourth is a compile error:
watch(element, cb)no longer fires. An element is no longer a Vue proxy of itself, so the deep traversal a reactive watch source starts stops immediately. Watch a getter over what you read:watch(() => field.value, cb).readonly(element)protects nothing. It hands the element straight back, and a write through the result reaches the element. Hand outelement.value, or acomputedover it.isEqualover two elements no longer compares their data. It answeredtruefor any two elements of the same class, and answersfalsenow unless they are the same instance. Comparea.valuewithb.value.clone()isbind(data, overrides). The data comes first:f.clone({ value: x, label: 'Name' })isf.bind(x, { label: 'Name' }). The type checker finds every call site.
Two changes reach dialog code in particular, both through the Action a dialog is handed. Action.execute() is asynchronous from vue-forms 0.9.0 — await it or attach a .catch() outside a template — and vuetify-inputs 0.9.0 leaves action.label / action.icon as the peer base class's plain value, so a read that wanted the value filtered by showLabel / showIcon is action.renderedLabel / action.renderedIcon.
The package is ESM-only
There is one build and one entry point. main, the require export condition, the UMD artifact and the dist/index.d.cts that went with them are gone, and build.target is es2022.
// unchanged: an import resolves exactly as it did
import { modal, FormBuilder, ModalView } from '@dynamicforms/vuetify-modal-form-kit';A CommonJS file reaches the same build through require() of an ES module, which node supports from 22.12 - the floor engines.node states:
const { modal } = require('@dynamicforms/vuetify-modal-form-kit');That call runs through a bundler that answers for .css: this package imports Vuetify's component entries - vuetify/components/VDialog and the rest, directly and through @dynamicforms/vuetify-inputs - and each of those imports its own stylesheet, which plain node has no loader for.
What the require condition pointed at could not be loaded in any case. The UMD bundle resolves its peers with require(), and @dynamicforms/vue-forms is ESM-only, so the first line of the bundle fails. The <script> tag path was never there either: the build named the browser global dynamicforms-vuetify-modal-form-kit.[name] literally, [name] included. Nothing replaces that global — load the ESM build through a bundler or a <script type="module">.
A dialog settles on the action of the form binding it was opened over
A registration belongs to an element's declaration in vue-forms 0.16 and later, so one chain serves every binding of that element and every dialog opened over one. The resolver this library attaches states that: it settles the dialog for the element it was registered on and for no sibling binding of it, only while its own dialog is the one on screen, and it is removed with unregisterAction() when the dialog settles.
The visible consequences:
- Repeated openings leave nothing behind. A module-level
Action, or a form kept across openings, ends each dialog with exactly the handlers it had before that dialog opened. Where an application built a freshActionper opening, or cloned the form, to keep resolvers from stacking up, that work can go. - A sibling binding settles nothing. The resolver answers for the element the dialog holds; executing another binding of the same declaration — a row of a
Listbuilt from it, say — passes straight down the chain. await action.execute()answers what the caller's own chain returned. The resolver returns the chain's answer instead of ending onundefined.
const save = new Action({ value: { label: 'Save' } });
save.registerAction(
new Form.ExecuteAction(async (field, supr, ...params) => {
await supr(field, ...params);
return api.save(form.value); // the record the backend answered with
}),
);
const closed = modal.message('Edit', 'change what you need', { form, actions: { save } });
const record = await save.execute(); // the record; it was undefined
await closed; // 'save'- An
AbortEventHandlingExceptionis an answer, not a rejection. A handler that raises one ends the run and leaves the dialog open, andexecute()resolves with the exception.execute()answeredundefinedbefore, whatever the run did: it discarded the chain's answer, and the chain answerednullfor an abort and for a run that reached no handler alike, so a handler that refused to close the dialog could not report why.
const save = new Action({ value: { label: 'Save' } });
save.registerAction(new Form.ExecuteAction(async (field, supr, ...params) => {
if (!form.valid) throw new Form.AbortEventHandlingException('Fix the highlighted fields');
return supr(field, ...params);
}));
const closed = modal.message('Edit', 'change what you need', { form, actions: { save } });
const answer = await save.execute();
if (answer instanceof Form.AbortEventHandlingException) showToast(answer.message); // the dialog is still openThe dialog's own resolver is what answers with the exception, and it stands outside every handler registered before the dialog opened. Register the aborting handler while the dialog is already open and it stands outside the resolver instead: its abort reaches nothing that catches it, and execute() rejects.
A dialog button is any vue-forms Action
FormActions and <df-modal>'s actions prop are typed Form.Action, where they were the Action that @dynamicforms/vuetify-inputs exports. Both widen, so every action you already declare goes on compiling; what is new is that an action of the peer library's base class is a dialog button too. <df-actions> draws a button from the action's value, so the subclass is what an action needs in order to render responsively — as a text link, or in a confirm / reject colour — not what it needs to be drawn at all.
This is what raises the @dynamicforms/vuetify-inputs floor to ^0.9.1. Against 0.9.0 the button row resolved every action through getBreakpointValue(), which only the subclass declares, and threw TypeError: getBreakpointValue is not a function over anything else.
One read moves with it. defaultConfirm and defaultReject are members of ActionRenderOptions — the shape an action's value takes — and the subclass exposed them as accessors as well:
// before
if (action.defaultConfirm) …
// after: where <df-actions> and <df-modal>'s keyboard read them
import { ActionRenderOptions } from '@dynamicforms/vuetify-inputs';
if ((action.value as ActionRenderOptions).defaultConfirm) …The accessors are still there on the subclass, so this is only needed where the action is held as a Form.Action — which is what FormActions now hands you.
Enter and Esc read effectiveEnabled and busy
The keyboard reaches an action that is rendered at DisplayMode.FULL, that is enabled all the way up, and that is not already running.
const buttons = new Form.Group({ save: new Action({ value: { label: 'Save', defaultConfirm: true } }) });
buttons.enabled = false;
buttons.fields.save.enabled; // true - what was written to the action
buttons.fields.save.effectiveEnabled; // false - and what Enter now askseffectiveEnabled is false where the action or any container above it is disabled, so Enter and Esc no longer execute an action inside a disabled group. busy is true from the call to execute() until the run settles, which is what keeps a held-down Enter from starting a second run of a handler that has yet to finish; a repeated keydown is dropped for the same reason.
<df-actions> disables its buttons on the same two reads from @dynamicforms/vuetify-inputs 0.9.1 — it draws a button loading while its action is busy — so a click and a keystroke reach the same set of actions.
execute() is asynchronous, and a document listener gets no Vue wrapper around it. A handler that rejects is routed to app.config.errorHandler, the way a rejection from a template handler is, and to console.error where the application installs none.
The generated dialog layout reads a field's own label
modal.message() and modal.yesNo() build a layout for the form they are given, one <df-input> per Field. The label on that input is the field's own where it carries one, and is derived from the field name only where it does not.
const form = new Form.Group({
vatNumber: new Form.Field({ value: '', label: 'VAT ID' }),
city: new Form.Field({ value: '' }),
});
// 'VAT ID', which used to render as 'Vat Number'; and 'City', from the name as beforevuetify-inputs 0.9.0 declares label on vue-forms' Extras, so every element carries one, and a prop wins over what the element carries — the generated prop overrode the label the caller had declared. An application that set label through setExtendedValues() or in an element's constructor sees it on screen now, wherever a name-derived label used to be. Where the derived label is what you want, drop the label from the field.
Two more things about the generated layout:
- A member at
DisplayMode.SUPPRESSis skipped entirely. It renders nothing, so a row and a column of its own left a gutter-sized gap. - A
GrouporListmember gets aconsole.warnnaming it, once per form. The generated layout has no row for a nested element, and such a member is still validated and still counted byform.valid— a dialog that will not close over an error nothing on screen shows. Pass aFormBuilderlayout of your own to render one.
A visibility naming no DisplayMode constant throws where the component renders
vuetify-inputs 0.9.0 resolves the visibility prop through vue-forms' DisplayMode.fromAny, which reads a name case-insensitively and refuses a value that names no constant — it throws from vue-forms 0.15, where it fell back to DisplayMode.FULL. A misspelling, and a mode a backend knows that this version does not, used to render the element fully and say nothing.
This reaches layouts rather than application templates: Component.fromJSON() passes props through by reference and never inspects it, which is what lets a backend send a whole layout. A visibility in that payload arrives at the component untouched.
const mode = Form.DisplayMode.isDefined(payload.visibility)
? Form.DisplayMode.fromAny(payload.visibility)
: Form.DisplayMode.FULL;Sanitise it where the payload is read, on the way into the layout, if an unknown mode has to be survivable.
Checklist for 0.7.0
- Upgrade both peers in one step —
@dynamicforms/vue-forms@^0.17.0,@dynamicforms/vuetify-inputs@^0.9.1— withvue@^3.5.2, and run on node 22.12 or newer. - Search for the three silent breaks first:
watch(with an element as the source,readonly(over an element, andisEqualover two elements. The type checker finds theclone(→bind(calls for you. - Work through the vue-forms and the vuetify-inputs migration guides for the rest of your own code.
awaitor.catch()everyAction.execute()outside a template, and rename theaction.label/action.iconreads that wanted the value filtered byshowLabel/showIcon.- Drop any per-opening cloning of actions or forms that was there to keep dialog resolvers from accumulating.
- Replace
new Form.Action(with vuetify-inputs'new Action(for every action you hand to a dialog throughoptions.form, and check the console for the warning that names the ones you missed. - Re-check the dialogs whose buttons sit in a disabled group: Enter and Esc no longer reach them.
- Load every dialog that hands
modal.message()a form: alabelyour elements carry is on screen now, in place of the label derived from the field name. - Look for the warning about
GroupandListmembers in the forms you pass tomodal.*, and give those aFormBuilderlayout. - Sanitise
visibilitywhere a layout arrives from a backend: a value naming noDisplayModeconstant throws where the component renders. - If anything of yours consumes this package through
requireor a<script>tag, move it to animport. The build is ESM-only.
Upgrading to v0.6.0 (from v0.5.x)
This release follows @dynamicforms/vue-forms 0.6.0 and @dynamicforms/vuetify-inputs 0.8.1. Nothing this library exports was renamed or removed, so the work is in your own use of the two peer libraries — Field.create(), Action.create(), reactiveValue and IField are gone there — plus two lines of application setup that this documentation used to get wrong. There is a checklist at the end of this section.
The peer dependencies move together
| Peer | Before | Now |
|---|---|---|
@dynamicforms/vue-forms | ^0.5.0 | ^0.6.0 |
@dynamicforms/vuetify-inputs | ^0.7.13 | ^0.8.1 |
vuetify | ^3.8 | ^3.9 |
The three are one upgrade: vuetify-inputs 0.8.1 requires vue-forms 0.6.0 and Vuetify 3.9, and 0.7.x cannot be combined with vue-forms 0.6.0. Installing them one at a time leaves npm reporting unsatisfiable peers.
Your own code migrates at the same time. Both peers document their own breaking changes:
// before
const submit = Action.create({ value: { label: 'Send' } });
const email = Field.create({ value: '' });
// after
const submit = new Action({ value: { label: 'Send' } });
const email = new Field({ value: '' });Follow the vue-forms migration guide and the vuetify-inputs migration guide for everything that is not on this page — reactiveValue, IField → FieldBase, and DFInputHint → DfInputHint in particular.
The plugin registers no components unless you ask it to
registerComponents and registerVuetifyComponents both default to false, and always did. Installing the plugin with no options and then writing <modal-view /> leaves Vue unable to resolve the tag, which means no dialog ever renders and every modal.* promise waits forever.
// before: <modal-view /> does not resolve
app.use(DynamicFormsModalFormKit);
// after
app.use(DynamicFormsModalFormKit, { registerComponents: true });The alternative is to import ModalView, DfModal, FormRender and ComponentRender in the components that use them. registerVuetifyComponents is for projects that do not install Vuetify globally; it registers the handful of Vuetify components this library renders.
There is no stylesheet to import
// before: fails to resolve - the package exports no such subpath and emits no CSS
import '@dynamicforms/vuetify-modal-form-kit/styles.css';
// after: the inputs rendered inside dialogs bring the styles
import '@dynamicforms/vuetify-inputs/styles.css';Types resolve through the exports map
package.json now declares a types condition for each of the import and require branches and ships an index.d.cts next to index.d.ts. A project on moduleResolution: bundler, node16 or nodenext used to fall back to any for everything this library exports, and a CommonJS consumer on node16 reported TS1479. Both resolve now. No source change is needed; delete any declare module shim you wrote to work around it.
What newly works
Four things this library documented but did not do:
- A second dialog opened while one is on screen. The dialog on top is the one most recently opened, and the one underneath reappears when it closes.
<modal-view>keeps a single<df-modal>alive across dialogs, and that component now follows the dialog it is given rather than the one it was created with. - Row and column breakpoints.
Row.breakpoint()andColumn.breakpoint()reach the rendered grid, and a breakpoint states only what changes: props merge key by key rather than replacing what the element was given, and its content carries over. Check your responsive layouts against what a breakpoint inherits. A column given no width also stops carryingcols: false, intoJSON()output as well —<v-col>defaults it tofalseanyway, and stating it kept the column from ever inheriting a width. - Row props.
dense,align,align-contentandjustifyare bound onto<v-row>.align-contentandnoGuttersalso survive the props filter, which used to drop them —align-content: 'space-between'in particular was validated against the values ofalignand thrown away. - The Enter / Esc shortcuts respect the action's state. A
defaultConfirmaction that is disabled, or hidden through itsvisibility, is no longer executed from the keyboard: the shortcut reaches exactly the actions a click could reach.
If your layout compensated for any of these — a breakpoint that restates every component, a class doing what justify should have done, a guard that re-checked enabled inside an action executor — the workaround can go.
Checklist for 0.6.0
- Upgrade all three peers in one step:
@dynamicforms/vue-forms@^0.6.0,@dynamicforms/vuetify-inputs@^0.8.1,vuetify@^3.9. - Work through the two peer migration guides for your own code:
Field.create(→new Field(,Action.create(→new Action(, deletereactiveValuereads, renameIField→FieldBaseandDFInputHint→DfInputHint. - Pass
{ registerComponents: true }toapp.use(DynamicFormsModalFormKit, ...), or import the components you use directly. - Replace the
@dynamicforms/vuetify-modal-form-kit/styles.cssimport with@dynamicforms/vuetify-inputs/styles.css. - Re-check any responsive layout: row and column breakpoints, and row props, now render. What was inert before is applied now.
- Drop the workarounds listed under What newly works.
See also: Getting Started, modal service, FormBuilder
