Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.7.8] - 2026-08-26
Added
setDfModalDefaults(defaults)sets reactive fallback values every<df-modal>reads for a prop it wasn't itself given - directly, or through themodalservice, which renders through<df-modal>as well.titleColoris the first one, applied as the title bar'scolorwhere a dialog states none of its own.DynamicFormsModalFormKit.install()accepts the same shape under a newmodalDefaultsoption, as a convenience for setting it once at startup; the setter itself works independently ofinstall()having run.
[0.7.7] - 2026-08-25
Added
<FormRender>emitsbreakpointwith the currently resolved breakpoint, once immediately and again on every change. A resolved layout can rearrange which row or column holds ateleportAnchor()cell between breakpoints, which makes Vue recreate that cell's<div :id>instead of patching it in place; a<Teleport :to>only re-resolves its target when thetostring itself changes, so it otherwise keeps pointing at the removed element and the teleported content renders empty. Key a<Teleport>on this emit to force it to remount and re-resolve alongside the anchor.
Changed
- The peer dependency moves to
vue^3.5.32: keying a<Teleport defer>on the newbreakpointemit needs vuejs/core#14642, which fixed a deferredTeleportlosing its content when it updates - here, remounts on a new key - before its deferred mount runs.
[0.7.6] - 2026-08-25
Added
VuetifyInputsComponentBuilder.dfImage(props), typed toDfInputComponentProps.DfImageProps, for thedf-imagecomponent@dynamicforms/vuetify-inputs0.10.1 adds. The peer dependency moves to^0.10.1.
[0.7.5] - 2026-08-25
Added
ComponentBuilderBase.teleportAnchor(idRef)adds a<div>anchor to the layout and writes the id it generates into an emptyidRef, reusing what's already there otherwise, for a consumer's own template to target with Vue's<Teleport>. A field's actual markup andv-modelthen live in the template, whileFormBuilderstill supplies where it lands in the layout - including across the field's own breakpoint redeclarations, which pass the same ref.useTeleportAnchor()pairs an id ref with the'#'-prefixed Teleport target string built from it, ready to hand to<Teleport :to="...">.<FormRender>warns if the layout it is about to render carries the same id on more than one component, since a<Teleport :to="'#' + id">only ever reaches the first element bearing it.
[0.7.4] - 2026-08-22
Changed
- The peer dependency moves to
@dynamicforms/vuetify-inputs^0.10.0.
[0.7.3] - 2026-08-21
Added
- A field states the component it is drawn as.
new Form.Field({ value: false, component: 'df-checkbox' })is a checkbox in the layoutmodal.message()/modal.yesNo()generate, where everyFieldwas a<df-input>whatever it held.componentis a key this library declares on@dynamicforms/vue-forms'Extras, so every element carries it, and it takesDfInputComponentTag- the tag of any component@dynamicforms/vuetify-inputsdraws with. A field that states none is drawn as a<df-input>. VuetifyInputsComponentBuilder.byTag(tag, props)builds the component a tag names, through thedf*method that owns the tag where there is one and throughgeneric(tag, props)where there is none. A layout built from data - one that reads the tag rather than writing it - therefore produces exactly what the hand-written method produces, and a tag the peer gains before this builder has a method for it still reaches the layout.VuetifyInputsComponentBuilder.dfInputHint(props)and.dfLabel(props), typed toDfInputComponentProps.DfInputHintPropsandDfLabelProps, which@dynamicforms/vuetify-inputs0.9.2 exports. The builder has one method per component that library draws with; the two had to go throughgeneric()withprops: any.
Changed (breaking)
ColumnProps.offsetisnumberandColumnProps.orderisnumber | 'first' | 'last', where both were declarednumber | 'auto' | boolean.<v-col>renders each into the classoffset-<value>/order-<value>, and Vuetify's stylesheet declares neitheroffset-autonororder-autonor a boolean form, so the declared type is now what the serialized props keep.order: 'first'andorder: 'last'are real classes and survive where they were dropped silently; an'auto'or a boolean, which rendered nothing either way, is a compile error rather than a value that vanishes.- A form breakpoint serializes as
FormJSONBreakpoint(Partial<FormJSON>), reached throughFormLayout.FormJSONBreakpoint, where it was typed as a wholeFormJSON. A breakpoint states only what it changes, so one that says nothing about the rows omits the key, and stating it that way needed a cast.rowsis optional on a read breakpoint with it:json.md.rows.lengthcompiled and does not, and readsjson.md?.rows ?? [].
Changed
- The peer floors move to
@dynamicforms/vue-forms^0.17.1and@dynamicforms/vuetify-inputs^0.9.2. Both releases exist to close findings this library raised, and the workarounds they replace are gone: nothing this library exports was renamed or removed, and the two peers are one upgrade. <modal-view>supplies@dynamicforms/vuetify-inputs' owndfInputComponentsByTagas the map a dialog body resolves component names against, in place of a nine-name list written out here. The caller'soptions.componentsstill win over it, and a component the peer gains is drawable in a dialog the moment the peer is upgraded -df-labelanddf-input-hintare two the list did not name.
Fixed
- An
AbortEventHandlingExceptionraised by an asynchronous handler is an answer, as one raised by a synchronous handler already was:action.execute()resolves with the exception, the dialog stays open, and a keyboard shortcut reports nothing toapp.config.errorHandler. Every handler that awaitedsupr- which is every handler wrapping an asynchronous executor - rejected instead, and the dialog resolver caught the abort itself to restore the contract.@dynamicforms/vue-forms0.17.1 converts it at the trigger, and that catch is gone. <df-modal>reads an action's render options throughgetRenderOptionsForBreakpoint(), the resolution<df-actions>draws the button from, rather than casting the raw value fordefaultConfirm/defaultReject. The button and the keystroke now answer one reading of the action. The resolver dropped both flags before@dynamicforms/vuetify-inputs0.9.2, which is why the cast was there.- An action option that only a breakpoint states is resolved at that breakpoint, so a dialog button stating
md: { icon: 'save-outline', showIcon: true }over a value that names no icon draws one frommdup. The cascade read the fields taking part in it off the base, and an action's value carries each option as a member of its own, so an option the base did not name was dropped at every width. A row, column or form layout is unaffected: each of those carries its children under one key the base always states.
[0.7.2] - 2026-08-21
Added
ModalOptions.componentsstates components the dialog body may name, over the ninedf-*ones<modal-view>supplies. A component the application wrote reachesmodal.custom()and aFormBuilderlayout without being registered globally, and a built-in name given here is replaced for that dialog alone.CloseablePromise.payloadcarries what the executor of the action that settled the dialog returned. The promise goes on resolving with the action's key, which stays astringaswitchreads; the payload is read off the promise after awaiting it. A dialog closed throughclose(value), or settled by an action that produced nothing, carries none.<df-modal>draws the header close button for the action Escape reaches, and a click on it runs that action. A dialog opened through themodalservice therefore settles with the action's own key -'close'frommessage(),'no'fromyesNo()- where the button used to emitupdate:model-value(false), which takes a service-owned dialog off the screen and leaves its caller's promise waiting.
Changed (breaking)
<df-modal>'sclosableprop is gone. Whether the header carries anxis the same question as whether Escape reaches an action: the button is that action's click-target, so a dialog that states a reachabledefaultRejectaction carries one and a dialog that states none carries nothing the button could do. A template dialog that passedclosableand states no such action statesAction.closeAction()instead, which answers both the button and the keystroke.
Changed
- One
<modal-view>draws the dialog stack. A second mounted view warns, as it did, and now renders nothing rather than drawing the same dialog over a second overlay; it takes the drawing over if the view that had it unmounts, so a route transition with both briefly mounted shows one dialog throughout.
Fixed
- The
modalservice carriesoptions.componentsonto the dialog it records, so what the caller states reaches the view that draws it. - CI states that
changelog.mdnames the version inpackage.json. A release carries both, written by hand in the pull request that makes it, andchangelog.mdships inside the tarball, so a heading that was never bumped shipped notes for the release before it.
[0.7.1] - 2026-08-21
Added
DfModalProps,DfModalSlots,FormRenderPropsandComponentRenderPropsare top-level exports. All four were declared indist/index.d.ts, where the components' own declarations refer to them, and exported from nothing, so a component wrapping<df-modal>,<form-render>or<component-render>had no name for the props it forwards.- CI type-checks
dist/index.d.tsagainst the lowestvuethe declared peer range admits, and builds the package and loads the built artifact on the node floorengines.nodestates. The declarations name Vue's own types, whose arity moves between Vue patch releases, so what the tarball ships can need a newer Vue than the range promises without a single source file saying so. changelog.mdships in the tarball, alongsidedist, the readme and the licence.
Fixed
- An Escape that a non-persistent overlay above the dialog consumes does not also reject the dialog. Vuetify's
VOverlaycloses a<df-select>menu or a<df-date-time>picker from awindowkeydown listener of its own and never callspreventDefault(), so the one keystroke closed the menu and rejected the dialog behind it. The dialog's overlay root carries adf-modalclass, and Enter and Escape reach an action only while an overlay of the dialog's own holds the highest z-index in.v-overlay-container- the reach a click has. - Unmounting the
<modal-view>a dialog is drawn in leaves that dialog on the stack, and a<modal-view>mounted afterwards shows it, still settling the promise its caller holds.<df-modal>owns the stack entry of a template dialog alone - one it was given nodialogIdfor - which is the rule it already applied when itsmodel-valuechanged; on unmount it removed the entry whoever owned it, so an api-owned dialog left the stack while its definition and its unsettled promise stayed behind and nothing could reach it again. - A dialog opens with a button that can settle it where every action the caller states is at
DisplayMode.SUPPRESS.<df-actions>draws no button for a suppressed action, so such a dialog reached the screen with nothing on it and no keyboard route out of it. The defaultclose, oryes/no, is stated where nothing the caller passed inoptions.actionsor declared onoptions.formis drawn. The read is the one the actions carry as the dialog opens: an action raised toFULLor dropped toSUPPRESSafterwards neither removes the dialog's own buttons nor adds them. FormBuilder.simple(),Row.simple()andColumn.simple()answerundefinedfor a key a runtime probes a value with - every symbol, plusthen,toString,valueOfandtoJSON- where they answered a component-adding function for every key. Returning a layout built throughsimple()out of anasyncfunction awaited it, which readsthen: that opened a row and a column and calledthenon the component builder, which declares no such method, so theawaitthrewTypeError: cmpt[prop] is not a functionover builder methods the caller never called.JSON.stringify()of the proxy and a console inspection of it built the same phantom row.ComponentJSON.propsisT | null, which is whatComponent.toJSON()emits for a component carrying no props. The serialized output is unchanged; TypeScript code reading a prop off a serialized component narrows thenull.- The readme, the getting-started guide and the migration guide state what a CommonJS consumer needs. Node supports
require()of an ES module from 22.12, whichengines.nodestates, but this package imports Vuetify's component entries - directly and through@dynamicforms/vuetify-inputs- and each of those imports its own stylesheet: the path runs through a bundler that answers for.css, not through plain node.
[0.7.0] - 2026-08-20
Changed (breaking)
- The peer dependencies move to
@dynamicforms/vue-forms^0.17.0,@dynamicforms/vuetify-inputs^0.9.1andvue^3.5.2, andengines.nodeis>=22.12.lodash-esmoves to^4.17.21: 4.17.12, which the declared range admitted, throwsReferenceError: root is not definedout of_createRound.jsthe moment anything imports it. The first three go together: vuetify-inputs requires vue-forms 0.17.0, and the vue and node floors are what those two libraries themselves demand. Nothing this library exports was renamed or removed; the work is in the consuming application's own use of the peers, which the migration guide points at. - The package is ESM-only. The UMD artifact, the
mainfield, therequireexport condition anddist/index.d.ctsare gone, and with the UMD thewindowglobal it defined, for which there is no replacement. Neither entry point it named could be loaded. Loaded throughrequire()it requires@dynamicforms/vue-forms, and that package has shipped no CommonJS build since its 0.12.0. The script-tag path put the library atwindow['dynamicforms-vuetify-modal-form-kit']['[name]'], becauselib.namewas written with a[name]placeholder that nothing interpolates, and read every peer off a global none of them defines. A CommonJS consumer reaches the ESM build throughrequire()of an ES module, which node supports from 22.12. A TypeScript consumer that emits CommonJS needsmoduleResolution: nodenexton TypeScript 5.8 or later, which is whererequire()of an ES module is typed;node16reportsTS1479, which is whatdist/index.d.ctsanswered.build.targetises2022. FormActionsand<df-modal>'sactionsprop are@dynamicforms/vue-forms'Action, where they were the subclass@dynamicforms/vuetify-inputsexports. Both widen, so nothing that compiled stops compiling. What the dialog reads off an action is its value -defaultConfirmanddefaultRejectare members ofActionRenderOptions, which is where<df-actions>reads them too - so the subclass is what an action needs in order to render responsively, not what it needs to be a dialog button. Code that readaction.defaultConfirmthrough the subclass accessor reads(action.value as ActionRenderOptions).defaultConfirm. This needs@dynamicforms/vuetify-inputs^0.9.1, which is where<df-actions>draws an action of either class; against 0.9.0 it threwTypeError: action.getBreakpointValue is not a functionas it drew the buttons.
Added
<modal-view>warns, once per form, about a member of the form it has no layout for. The layout it generates is one<df-input>perField; a nestedGrouporListis not on screen, while still validating and still counted byform.valid. The warning names the members and points at passing aFormBuilderlayout of your own.scripts/verify-artifact.mjs, which CI runs after the build. It imports the built ESM artifact, asserts the export list and the members of theFormLayoutnamespace, and exercises the fluent builder, a breakpoint and the JSON round trip. The specs importsrc/, so this is the only thing that loads what the package publishes.- A
prepackscript that builds the package, sonpm packandnpm publishship what the current source compiles to.
Fixed
- Executing an action settles the dialog it was opened into and no other. A registration belongs to an element's declaration, so one action chain serves every binding of that element and every dialog opened over one: the resolver answers only for the element it was registered on, and only while its own dialog is the one on screen. Two dialogs opened over two bindings of a single form each settle on their own.
- Nothing accumulates on the caller's actions. The resolver is dropped with
unregisterAction()when the dialog settles. Opening N dialogs over the sameActioninstance - a module-level action, or a form kept across openings - used to leave N handlers registered on it, each holding a settled promise and a dead dialog id, and all of them ran on every later click. await action.execute()answers what the action's own chain returned. The resolver awaited the chain and returned nothing, so an executor's return value never reached the caller.- An
AbortEventHandlingExceptionis an answer.execute()resolves with the exception rather than rejecting on it, the run ends, and the dialog stays open - an action that refuses to settle the dialog states so by aborting. - A failing keyboard shortcut reaches
app.config.errorHandler, withdf-modal keyboard shortcutas its context.execute()is asynchronous and the Enter/Esc listener is on the document, so nothing wraps it the way Vue wraps a template handler and a rejecting handler surfaced as an unhandled rejection. - Enter and Esc reach an action that is enabled all the way up, not one that merely carries
enableditself. Reachability readseffectiveEnabled, which is false where the action or any container above it is disabled.<df-actions>disables the button on the same read from@dynamicforms/vuetify-inputs0.9.1, so a click and a keystroke reach the same set. - A held Enter starts one run.
onKeydownreturns on a repeat event, and an action that is still running is unreachable whilebusy, so the second keystroke no longer starts a second run of a handler that has yet to settle. - The generated layout keeps a label the element carries.
@dynamicforms/vuetify-inputs0.9.0 declareslabelon vue-forms'Extras, and a prop wins over what the element carries, so a generatedlabelprop would override the one the caller declared on the field. The name-derived label is stated only for a field that carries none. - A form member at
DisplayMode.SUPPRESSis left out of the generated layout. It rendered nothing while still taking a row and a column of its own, which reached the screen as an empty gutter gap. <component-render>resolves the nested-form renderer in acomputed. It read thecomponentsmap once during setup, so a map that gains the renderer afterwards left the nested form renderingundefined.
[0.6.1] - 2026-08-16
Fixed
<form-render>renders a plain JSON layout, which itslayoutprop has always declared it takes. The prop wrapped anything that was not aFormBuilderinnew FormBuilder(json), which kept the plain objects, so the firstrow.toJSON()threwTypeError: row.toJSON is not a function. AFormBuilderand the JSON itstoJSON()produces now render the same thing, breakpoints and all..nestedForm(inner)renders.<component-render>consulted the component map before the nested-form branch, and the renderer registers itself in that map underFormBuilderName, so the map always won and the nested layout was spread onto<form-render>as attrs instead of reaching itslayoutprop. Nested forms inherit thecomponentsmap and resolve their own breakpoints, at any depth.<df-modal>binds its<v-dialog>one way.v-modelwrote to a computed that has no setter; the update now travels throughonModelValueUpdate.FormBuilder.toJSON()on a form with no rows produces a layout<form-render>renders.- A breakpoint that states nothing about its children serializes without the key.
Row.toJSON()omitscolumnsandColumn.toJSON()omitscomponentsfor such a breakpoint, andFormBuilder.toJSON()leaves the breakpoint out entirely. An empty list is how a breakpoint states that it has none, so emitting one where the breakpoint said nothing erased what the breakpoint inherits the moment the JSON was read back. DialogSize.isDefined(size)reports whether the value names a size. It wastruefor every string, because the check routed throughfromString(), whose fallbackDEFAULTis itself a member of the enum.fromString()is unchanged and still returnsdefaultDialogSizefor anything it does not recognise: a size handed to a dialog that is about to render ignores invalid values, while a caller asking whether a string names a size gets the truth.DEFAULThas no string identifier, soisDefined('default')isfalse.cols-<breakpoint>is gone fromColumnProps, andColumn.toJSON()drops the key.<v-col>names per-breakpoint widthssm/md/lg/xl/xxl, socols-mdonly ever reached the DOM as an inert attribute; naming one is now a type error rather than a layout that silently ignores it. Per-breakpoint widths go throughColumn.breakpoint(name, colCallback).offset-<bp>andorder-<bp>camelize onto real<v-col>props and are untouched.
Added
FormBuilder.fromJSON(),Row.fromJSON(),Column.fromJSON()andComponent.fromJSON()lift a serialized layout into instances, and leave one that is already an instance alone.RowandColumnconstructors take either their props or their JSON.modal.yesNo(),modal.message()andmodal.custom()warn when no<modal-view>is mounted by the end of the tick. The dialog goes onto the stack with nothing rendering it, so no action can resolve it and the returned promise settles only through its own.close(value). A dialog opened before a sibling<modal-view>mounts is supported and does not warn.
[0.6.0] - 2026-08-15
Changed (breaking)
- The peer dependencies move to
@dynamicforms/vue-forms^0.6.0,@dynamicforms/vuetify-inputs^0.8.1andvuetify^3.9. The three go together: vuetify-inputs 0.8.1 requires the other two, and its 0.7.x line cannot be combined with vue-forms 0.6.0. Nothing this library exports was renamed or removed; the work is in the consuming application's own use of the peers, whereField.create(),Action.create(),reactiveValueandIFieldare gone. package.jsonexportsdeclares atypescondition per branch and the build emitsdist/index.d.ctsbesidedist/index.d.ts. A consumer onmoduleResolution: bundler/node16/nodenextresolved this package's types asanybefore, and a CommonJS consumer onnode16reported TS1479.
Added
- A migration guide at
/guide/migration. ModalOptionsandFormActionsare exported as types. They are the parameter types ofmodal.message(),modal.yesNo()andmodal.custom(), and could not be named by a consumer.VBtnto the componentsregisterVuetifyComponents: trueregisters.<df-modal closable>renders one for its close button.
Fixed
- A dialog opened while another is on screen is displayed.
<modal-view>keeps one<df-modal>alive and swaps itsdialogId; the component compared the stack against the id it was created with, so every dialog after the first stayed invisible and its promise could not settle. - Row and column breakpoints reach the rendered grid.
<form-render>resolved the form-level breakpoint only and serialized the rest without one, soRow.breakpoint()andColumn.breakpoint()changed nothing. What such a breakpoint inherits follows from@dynamicforms/vuetify-inputs0.8.1, which this release requires: props merge key by key, and columns or components come from the nearest smaller breakpoint that adds any. A breakpoint that adds none used to render an empty row or column; assigning an empty list is now the way to state that. - A
Columngiven no width no longer serializescols: false.<v-col>defaultscolstofalseon its own, while stating it kept the column from inheriting a width at any breakpoint.toJSON()output changes accordingly:propsis{}where it used to be{ cols: false }. - Row props are bound onto
<v-row>.dense,align,align-contentandjustifywere serialized and dropped. noGutterssurvives the row props filter, which whitelisted the key under the spellingno-gutterswhile the value was written undernoGutters.align-contentis validated against the alignments it accepts.space-between,space-aroundandspace-evenlywere checked againstalign's values and discarded, both for the base key and its breakpoint variants.- Enter and Esc reach only the actions a click could reach. A
defaultConfirm/defaultRejectaction that is disabled, or hidden through itsvisibility, is no longer executed from the keyboard. ModalData.resolvecloses the dialog it settles. It held the bare promise resolver, so calling it left the dialog on the stack and on screen.modal.isInstalled()counts mounted<modal-view>instances instead of holding a flag that any one of them cleared on unmount.vuetifyis external to the bundle. Only its subpaths were, so the library shipped a copy of the display composable.- The
<v-col>breakpoint demo in the documentation writescol.props.cols, which is where a column's Vuetify props live;col.colswas read by nothing. - The documented plugin registration passes
{ registerComponents: true }, without which<modal-view />does not resolve, and the stylesheet import points at@dynamicforms/vuetify-inputs/styles.css— this package emits no CSS and exposes no such subpath. - The repository, issue and documentation links point at
github.com/dynamicforms/vuetify-modal-form-kit. The previous URLs resolved to no repository.
