Changelog
All notable changes to @dynamicforms/vue-forms will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[3.0.0-alpha.2] - 2026-10-15
Added
ActionsMap.registrationsandActionsMap.restore(registrations): the array of registered actions, which is not modified later, and its reinstatement with the order of the handlers and every duplicate registration.
Changed
onCommitandonElementAdoptedrun after the transaction is closed. A write they make opens a transaction of its own and is announced; an exception they throw does not roll back the committed change and is rethrown once every hook of the commit has run.List.insert()andList.remove()throw aRangeErrorfor an index that is not an integer (a fraction,NaN,Infinity).- A list view's
splice()reads its arguments asArray.prototype.splicedoes: with none it removes nothing, and an explicitundefinedcount removes nothing. Setting a list view'slengthto a value an array refuses throws aRangeError.
Fixed
- Removing a validator cancels its runs on every element it is removed from, the rows of a
Listincluded, and no other validator's: a run on a row no longer adds its errors after the validator was removed from the item template, and removing one validator no longer discards the result of another the field keeps. - A registration whose eager run throws is rolled back with its transaction, so the action is no longer left registered.
- A rolled-back
unregisterAction()orclearValidators()restores the actions in their previous order. - A container throws a
TypeErrorwhen it is to take itself or a container above it, which madeaddField()andpush()loop without end. installPlugin()leaves a plugin whosesetup()throws uninstalled.- A declaration removes the references to its collected bindings when it records a binding a minute or more after the last removal, so a declaration bound for every record a dialog opens no longer accumulates one
WeakRefper binding. CompareToremoves theValueChangedActionit registered on the compared field when it is unregistered from its last field. A conditional action removes its handler from the fields its statement reads when it leaves its last element, also when rows of a list it applied to were collected before.- A rule of a dropped form that reads a field outside it (
CompareTo, a conditional action) no longer keeps the form reachable, and its handler on that field is removed by the next registration on the field a minute or more after the previous check. - A rolled-back
removeField()puts the member back at its position, so the order offields, of the keys ofvalueand of the members a group value writes is the one before the transaction. - Once a group has a view,
addField()refuses the member names a view reserves ($,then,__v_*). - The devtools record nothing where there is no
window, as on a server that renders pages, and drop the entry of an element that has been garbage-collected.
[3.0.0-alpha.1] - 2026-10-12
3.0.0 makes the library a state layer for forms and for application state alike, and moves rendering out of it:
- Application state, not only forms. Fields, groups and lists also hold application state, such as a shopping cart or the filters of a list view, with the validation, transactions and actions of a form. The devtools inspector lists every root element by the file or component that constructed it, and plugins, with the tracking plugin, make a write into an object a field holds a change of the field.
- No UI in the library. The package ships no components, styles, configuration or translations and no longer depends on
@dynamicforms/translatable. An error is data (code,params, an Englishdetail,origin); the application, or@dynamicforms/vuetify-inputs, renders and translates it. Actionis a command. An action executes in the library:SubmitActionvalidates a container, sends its value to a handler and rebinds it to the result,RejectActionreverts it, andContainer.confirm()andreject()execute them. An action holds no data: it is left out of its container's value,isChangedand validity.ErrorPolicydecides whether errors from origins other than validators, such as the server, count towardvalidand when they are removed.- Stricter semantics. The built-in validators other than
RequiredandCompareTopass an empty value, access and visibility events fire once at commit, andoriginalValueandisChangedhold at every level of a bound group or list.
Most of the breaking changes keep compiling. The migration guide lists them first.
Added
- Vue devtools support in development: a "vue-forms" inspector listing every root element, grouped by the file or the component that constructed it, with its members and state, and the state a component constructed shown in that component's inspector.
describeState()andhideState()name and hide an element;configureDevtools()turns the devtools off, selects what is listed and turns off the capture of the constructing file. The location section of an element shows that file and the frames of the construction stack outside the library. Values, access, visibility, touched and extended properties are editable in the inspector; an edit is a write through the element's setter. The devtools are a plugin the library installs in development; a binding, such as a list row, records nothing, and a member keeps no captured stack. In a production build everything devtools-related is removed by the bundler.DevtoolsOptions,DevtoolsRegistrationandStateDescriptiontype the API. SubmitAction(target, handler, options?): anExecuteActionthat waits for validation, refuses an invalid target or a concurrent run with aSubmitRefusedExceptionwhosereason(SubmitRefusalReason) says which, removes the errors at and below the target whose rule removes them on'submit', sends the target's value tohandlerwith an'editable'target'readonly'until it returns (lock), resolves with aSubmitFailedException(cause: the error) where the handler fails, rebinds the target to the result and resolves with{ action, sent, received }(SubmitResult). Both exceptions extend the abstractCommandException, the base of the exceptions aTargetedExecuteActionends with, so the resolved value isSubmitResult | CommandException.SubmitOptionstypes the options.RejectAction(target)rebinds the target to itsoriginalValue.FieldActionBase.resetBinding(binding): called when a binding is reset to a new binding of its declaration; an override drops the per-element state it keeps.ConditionalStatementActionforgets the result it last applied.TargetedExecuteAction, the base class ofSubmitActionandRejectAction: anExecuteActionwhosetargetFor(action)resolves aCommandTarget, an element or a callback that returns one.Container.confirm()andreject(): execute the action whoseSubmitActionorRejectActiontargets the container, else the only shown action withdefaultConfirmordefaultReject.defaultConfirmanddefaultRejectinActionValue, with getters onAction.Action.executable, andExecuteAction.canExecute(), which it reads.pendingon every element:validating || busy, reactive.settled()resolves when it turnsfalse.ErrorDescription, the interface of an error:code,params,detailandorigin.ErrorPolicy: the rules of the error origins other than'validator'for an element and every element below it. A rule (ErrorPolicyRule) states whether an error of its origin counts towardvalid(validEffect) and when the element removes it (removeOn, anErrorRemoval): when its value changes ('change'), whentouched = trueis written after its value changed ('edit'), when aSubmitActionsends it ('submit') or never ('never'). The nearest policy naming an origin applies.setDefaultErrorPolicies()sets the rules where no policy names an origin; by default'server'errors count and are removed on'edit', and every other origin counts and is never removed.ErrorPoliciestypes the rules anderrorRemovalslists the removals.Validators.ValidationErrorOptions, andValidators.isEmptyValue(), the test by which the built-in validators pass an empty value.ActionsMap.ofClass(identifier): the registered actions with that class identifier, in registration order.paramson every error.installPlugin(plugin): a plugin'sonSetValueandonSetOriginalValuehooks run as a pipeline on every write of a field's value and of any element'soriginalValue;onElementCreatedruns at every construction,onElementAdoptedwhen a container takes a member, andonCommitafter every committed transaction.PluginContext.changeInPlace()makes a write into the value a field holds a change of the field, andisInternal()tells a binding or an item template from a root.Pluginis the type of a plugin andPluginContextthat of the context itssetup()receives.- The tracking plugin,
@dynamicforms/vue-forms/plugins/tracking: a field holds a copy of an object value, and a write into a plain object, an array, aMap, aSetor aDateit holds is a change of the field.untracked()andsetTrackingWarnings(). "sideEffects": falseinpackage.json: no module of the package runs code on import, so a bundler drops the modules an application does not use.
Changed
- A
valueassignment firesListItemRemovedActionandListItemAddedActionfor the rows it removes and builds, andclear()firesListItemRemovedActionfor every row.sort()andreverse()of a list view reorder the rows in place and fire neither. List.push()andinsert()takeListItemInput<R>(R['value'] | R) instead ofany; a list without an item template that holds rows other than groups is declarednew List<FieldBase>().Group.createFromFormData()builds aGroupfor a nested object and aListfor an array, at every level.Operator.isDefined()returnsfalsefor an unrecognised string instead of throwing.- The listener a conditional action installs on the fields its statement reads runs outermost in their chains and is removed when the action is taken off its last element.
- Every built-in validator except
RequiredandCompareTopasses an empty value (null,undefined,'',[],{}).MinValue,MaxValueandValueInRangecompare a number with a number, a bigint with a bigint, a string with a string and a date with a date, and refuse any other value,NaNand an invalid date. AccessChangedAction,EnabledChangedActionandVisibilityChangedActionfire at commit, once, with the net change, beforeValueChangedAction.- A construction parameter is assigned to the element only where it names an accessor; a parameter named like a method (
validate,bind) is an extended property. rebind()gives aGroupmember whose key is missing from the data theoriginalValueof the declaration's member, sogroup.rebind(group.originalValue)restores a'disabled'member.- A member of a group bound from a declaration, reset by
rebind(), and aListrow reused byrebind()or by avalueassignment take theaccessandvisibilityof the declaration's element, and their conditional actions apply their statements again over the new record. The elementrebind()is called on, and a member of a group that is its own declaration, keep theirs. - An
Actionsends nothing: it is left out of its container'svalueandfullValue, does not affect the container'sisChangedor validity, and its validators do not run. A container whose members are all actions sends nothing either. ValidationErroris data:new ValidationError(code, params, detail, origin?). It carries thecodeof what failed, theparamsit failed with, an English plain-textdetailand anorigin, and implementsErrorDescription. The library does not render it.- Error codes are snake_case and name what failed:
minismin_value,maxismax_value,rangeisvalue_in_range,min-lengthismin_length,max-lengthismax_length,range-lengthislength_in_range,in-allowed-valuesisin_allowed_values,compare-toiscompare_toandvalidation-failedisvalidation_failed.requiredandpatternstay. - The built-in validators take
ValidationErrorOptions({ code? }or{ code, detail? }: adetailrequires acode) as their last argument instead of a message.RequiredOptionsaddstrimto it.CompareTono longer requires it. - The English details of the built-in validators are plain text, without markdown.
Validator's protectedreplacePlaceholders()andreplacePlaceholdersFunction()are replaced by the protectederrorFor(options, code, detail, params), which returns theValidationErrora built-in validator reports.Validators.AllowedValuesisT[] | Ref<T[]> | (() => T[])instead ofany.- An eager pass runs the rules before the validators, and the validators receive what the element sends after the rules ran.
sameAscompares the class,code,params,detailand the stated origin, so an error whose params changed replaces the instance on the field.
Fixed
bind()of aGrouprecords every member's part of the data as the member'soriginalValue, at every level: a member, a nested list, aListrow built from the item template and the members of a row start out withisChangedfalse. A member whose key the data leaves out takes the declared member'soriginalValue.- A
Groupconstructed withvalue, or withoriginalValuealone, records what every element below it holds as itsoriginalValue; aparams.originalValuegiven next tovalue, and theoriginalValueofbind()overrides, is the baseline of each member it contains, at every nested group. A member reportedisChangedtrueagainst the value it was created with. - A member that a conditional access enables while it is bound is validated over the value it holds; it kept the error of a validation over
undefined. rebind()and aListassignment that reuses rows record the baseline after the conditional actions applied over the new record. A rebound element whose rule enables or disables a member reportedisChangedtrue.- A
tx.rollback()whose signal atry/catchin the callback caught let the transaction commit; it rolls back. Requiredand the length validators measure aMapand aSetby size and an object without a prototype by its keys; such an object threw.- A substituted value that contains a placeholder is not substituted again.
- A list assignment replaces a row that was not built from the item template, instead of resetting it through the template and dropping the members the template lacks.
- An action class without a
classIdentifieris refused at registration; it was registered and broke every later trigger on the element. - View error messages name
$.addField(),$.removeField()and$.value. - A
Patternwith thegoryflag passed and failed the same value in turn; it tests with a copy without them. - A
VisibilityChangingActionthat returned the current visibility did not refuse the write. - A conditional action registered on a second element of a record did not apply the current result to it.
- Writing
NaNoverNaNannounced a change. List.insert()andremove()cost a proxied write per row after the position; the rows are held outside Vue's reactivity and the cost no longer grows with the list beyond native array operations.- Reading an index or
lengthofview(list)copied the row array on every read.
Removed
beginValidating(),endValidating()andvalidationEpochfrom the public API; validators reach them through keys the package does not export.MessagesWidget,RenderableValue,MdString,SimpleComponentDef, theRenderContenttypes,ClassType,ClassTypes,isSimpleComponentDef,isCallableFunctionand the stylesheetstyle.css.- The plugin
forms,getConfig,setConfig,FormsConfiganduseMarkdownInValidators. ValidationErrorTextandValidationErrorRenderContent.buildErrorMessage.- The dependency on
@dynamicforms/translatable, together withtranslateStrings,translatedMessageand thestringsdictionary.
[2.0.3] - 2026-10-06
Changed
@dynamicforms/translatableis a peer dependency instead of a dependency, as it is in every other@dynamicformslibrary, so an application resolves one copy of it for all of them.
[2.0.2] - 2026-10-01
The first 2.0 release. 2.0.0 and 2.0.1 were withdrawn from npm; what changed relative to 1.1.0 is listed here.
Added
access, what an element accepts and what it sends:'editable'and'readonly'send its value,'disabled'leaves it out and'disabled-null'sendsnullin its place; only'editable'accepts input. The typeAccess,accessValues,defaultAccessandisAccess()are exported with it.effectiveAccess: the access that applies once the containers above are taken into account. Below a'disabled'or'disabled-null'container every element is'disabled', and below a'readonly'one an'editable'element is'readonly'.contribution: what an element sends to its container — its value,null, orundefinedfor nothing.ContributionChangedAction, told what an element sends to its container once that changed over a transaction. A switch of access fires it and notValueChangedAction; a write into a'disabled'field firesValueChangedActionand not it.AccessChangingActionandAccessChangedAction, asked before and told afteraccessis written.ConditionalAccessAction(statement, whenTrue = 'editable', whenFalse = 'disabled').- An error's
origin:'validator','server','application'or a string of the application's own. A validator's errors are'validator'and any other error is'application'unless its author states an origin, the last constructor argument of every error class. - The type
Visibility,visibilityValues,defaultVisibilityandisVisibility(). view(element): a form element seen as its data. A group's members and a list's rows are plain properties — a field as its value, a container as its view — and the element itself is$. A list's view is an array whose mutations are carried out as the list's own operations. One element has one view.Container, the abstract baseGroupandListextend. It composesvalid,busyandtouchedover the children, cascadesvalidate(true)to them and takes their change notifications, so the two share one implementation of each.- A
Listrow is any form element.new List(new Field<string>())is a list of plain values: every row is aField,list.valueis astring[], and every row carries the template's validators and actions.new List(new List(...))is a list of lists. - A list without an item template builds each row from its own item:
new List().push('abc')holds aFieldand reads back['abc'].
Changed
- Breaking:
enabledis read fromaccessand has no setter: it istruewhereaccessis'editable', andeffectiveEnabledwhereeffectiveAccessis. A parameter object or abind()override namingenabledthrows aTypeError.EnabledChangingActionis asked before, andEnabledChangedActiontold after, a write ofaccessthat changesenabled; the changing action lets the write through or refuses it, since its answer cannot setenableditself.ConditionalEnabledActionis replaced byConditionalAccessAction. - Breaking:
DisplayModeis removed.visibilityis aVisibility—'full','invisible','hidden'or'suppress'— and is presentation alone: it changes nothing about what an element sends or whether it is validated. A visibility that is none of the four, numbers included, throws.ConditionalVisibilityActiontakes the visibilities to set aswhenTrueandwhenFalse, defaulting to'full'and'suppress'. - Breaking: validators run over what an element sends, and only where it is sent at all. An element whose
effectiveAccessis'disabled'is not validated and carries no error from a validator, so a disabled field, or any field inside a container that is'disabled'or'disabled-null', no longer makes its form invalid; a'disabled-null'element is validated overnull. A container does not count a'disabled'child, so an error written into one by hand holds nothing back either. A switch of access, and a member taken into or out of a container, runs the validators again wherevereffectiveAccessmoved. - Breaking: a container that is
'disabled'is left out of its parent's value whatever it holds. - Breaking: an enabled container is never
null. AGroupnone of whose members is sent reads{}and aListthat sends no row reads[];GroupValue<T>andListValue<R>no longer includenull. - Breaking: a disabled row is left out of a
List'svalue, by the rule aGroupapplies to its members. The rule for both is stated once, in the protectedFieldBase.serializesAs(purpose), which a subclass of an element may override. - Breaking:
ValueChangedActionon aGroupor aListreports what the container holds — itsfullValue— rather than what it sends, so a write into a disabled member fires it and a switch of access does not. The container's own validators run over what it sends. - Breaking: a disabled
FieldorActiontakes a write tovalue;accessdecides what is sent and whether input is accepted, not whether a write reaches the element. A record assigned to a form reaches every member whatever its access. - Breaking:
parentis typedContainer | undefinedon every element.FieldandActionno longer narrow it toGroup | undefined, sofield.parent?.fields.otherneedsinstanceof Groupor a cast first. - Breaking:
List's type argument is the row rather than the row's fields:List<Group<Fields>>where it wasList<Fields>.ListValue<R>takes the row the same way, andlist.valueis typed by it rather than asRecord<string, any>[]. - Breaking: a list without an item template builds a
Groupfrom a plain object only. An array becomes aList, and any other item —nullincluded — aFieldholding it, where each used to become aGroup. Padding past the end of such a list follows the inserted item the same way.
Fixed
- A
Groupcompletes the records of its members once it has written the data it was built with, so a rule reading a sibling - a validator on aListrow that reads another field of the row - reaches its verdict over the row's data rather than over the values its members were bound with.
[1.1.0] - 2026-09-05
Added
isEqual(a, b), a structural comparison that treats aFieldBaseanywhere inaorbas the value it holds rather than as itself.isEqual(fieldA, fieldB)andisEqual(list.items, other.items)answer whatisEqual(fieldA.value, fieldB.value)already does, without unwrapping every element by hand — including one nested inside a plain object or an array, at any depth. Everything that is not aFieldBasecompares exactly as lodash's ownisEqualwould. TwoFieldBaseoperands skip the customizer dispatch this needs for the general case and compare.valuedirectly, about 40% faster measured comparing two single-field elements.
[1.0.0] - 2026-09-01
Changed
- No functional change from 0.18.0. This release is the stability commitment: the public API is frozen, and a further breaking change bumps the major version rather than the minor one, per Semantic Versioning.
[0.18.0] - 2026-09-01
Added
- Every built-in validator's message is translatable.
translateStrings, from@dynamicforms/translatable, replaces the English default of one or more ofRequired,MinValue,MaxValue,ValueInRange,MinLength,MaxLength,LengthInRange,Pattern,InAllowedValues, andValidationFailed- an error already on screen updates in place, placeholders re-interpolated, the moment it is called; a key it is not given for keeps its English default. buildErrorMessageaccepts aRef<string>in addition to a plain string, returning aComputedRefthat re-applies the markdown/plain-text choice as the ref's value changes.
[0.17.1] - 2026-08-21
Changed
An
AbortEventHandlingExceptionraised in a chain that has an asynchronous handler in it is answered with rather than raised, the way it is in a wholly synchronous one.Action.execute()resolves with the exception instead of rejecting with it, so a caller that read it off acatchbranch reads it off the answer:typescriptconst answer = await save.execute(); if (answer instanceof AbortEventHandlingException) reportRefusal(answer.message);A handler that reads an abort off its own
suprcall is unaffected: inside the chain it is a throw, and only the trigger converts it. A rejection carrying anything other than an abort still reaches the caller as a rejection.
Fixed
- A handler that ends its run from an asynchronous handler leaves no unhandled rejection behind. The
*Changed*setters discard what the trigger answers with, so an abort travelling as a rejection reached nothing but the runtime's unhandled-rejection reporting - under node's default settings, the end of the process. ActionsMap.triggerEager()ends only the identifier group the abort was raised in where that group is asynchronous, as it does where it is synchronous. The other groups run, and the abort is not reported as unhandled.
[0.17.0] - 2026-08-20
Added
Extras, an empty exported interface that is what theXtype argument of every element defaults to. A UI layer declares the properties it renders forms with once, by augmentingExtrasin adeclare moduleblock, and every element in the consuming application then carries them — including the fields written inline in aGroupdeclaration, which no type argument on the group can annotate. Where an element states anXof its own it replaces the default, soField<string, Extras & Local>is how it carries both, andActiondefaults toExtraswithout the keys ofActionValuebecauselabelandiconare members it declares itself. The default reachesFieldBase<T>as well, so a validator or an action handler reads the augmented properties off the element it receives without a cast. Nothing changes for existing code:Extrasis empty until something augments it, and an element that states its ownXis unaffected.effectiveEnabledon every element:truewhere the element and every container above it are enabled. A rendering layer binds it to draw the inputs of a disabled section disabled, instead of walking the parent chain for each of them. It is a read —enabledon each element stays what was written to it, a write to a member of a disabled container is accepted as before, and what a container serializes is decided by the members' ownenabled.
Changed
Breaking.
ActionValuedeclareslabelandiconasunknown, andAction's accessors for them read their type off the value rather than fixingstring | undefined. What a label and an icon are is the rendering library's to say, andunknownis what lets it say so:interface RichValue extends ActionValue { label?: string | MdString }is legal where astringin the base refused it, and a subclass cannot widen an accessor the base class typed — that isTS2416, which no cast on the subclass's side reaches. A subclass therefore restates the two members in its value type and the inherited accessors answer at that type, with no accessor override needed unless the read is to be filtered.What moves for a consumer: on an
Actionthat states no value type,action.labelandaction.iconread asunknown, soaction.label?.toUpperCase()no longer compiles and the reader states what it expects —action .label as string | undefined, or a value type on the action. An action built from a literal is narrower on its own, becauseTis inferred from it:new Action({ value: { label: 'Save' } })readslabelasstring. Writes are unaffected at every shape, and nothing changes at runtime.
[0.16.1] - 2026-08-20
Added
FieldBase.constructed(params), the hook a subclass overrides to complete what the element was built with.Field,Action,GroupandListcall it at the end of a construction, inside the transaction the construction is and before the element records what it was built as, so what the override writes -_value, a member,originalValue- belongs to the construction rather than being a change of it: noValueChangedActionannounces it, an element builtenabled: falsedoes not refuse a write to_value, and what the hook leaves is what the eager actions and the validators then run over. Where the parameters named nooriginalValuethe element is baselined on the value the hook left, so it starts unchanged; where they named one it is measured against that, as any element is. It receives the parameter object the constructor was given, where it was given one. A write made aftersuper.init()returns is none of that, being a change to a finished element.- Three specs holding the library from a consumer's seat rather than from its own: a binding layer's computed over
valueand the reads it repaints from, anActionand aFieldsubclassed with a widened value, and a serializer readingvalueandfullValueback and writing a record into a form.
Fixed
- An
Actionkeeps a value that states something other than a label or an icon.Actionis meant to be subclassed with a wider value type, and the emptiness test readlabelandiconalone: a value naming a render style, a name or a set of per-breakpoint options and neither of those two was taken for empty and replaced by the pair ofundefineds, so the action came out holding nothing at all and a button rendered from it came out blank. The question is asked over every member the object carries, which answers the same for the shape the base class declares. - An
Action's baseline carries exactly what it was declared with.params.originalValuewas copied into{ label, icon }, so a widened baseline lost every member beyond those two and the action it belonged to was measured against a fraction of what was declared for it. - An
Actiondeclared with a value and a matchingoriginalValuereportsisChangedfalse from construction, and so does everyGroupholding it. This is the base class and not only a subclass:new Action({ value: { label: 'S' }, originalValue: { label: 'S' } })reportedisChangedtrue, becauseisChangedis a structural comparison and reads own-key sets, and the two objects were built differently - the value kept by identity, carrying the one key it was declared with, and the baseline reshaped into the pair the base class names. The baseline is a frozen copy of what was declared, and the value itself where nothing declared one. - A bare
new Action()reportsisChangedfalse. Its baseline was never assigned, so the pair ofundefineds it holds was measured againstundefinedand every action constructed without parameters reported itself changed, its containers with it.
Documentation
- Widening the value in a subclass on the
Actionreference: what a subclass adds and what it keeps, and that a subclass readinglabeloriconin a shape of its own declares the getter and the setter together, the setter delegating to the base withsuper.label = newValue. A getter declared alone defines the whole property, which then has no setter at all, soaction.label = 'Save'- the documented way to write either member - throws aTypeErrorin module code. A narrowed read and an unnarrowed write do not answer each other;action.value.labelis the unfiltered read. - The value rules on the same page state what a construction settles on: a
params.valuecounts as empty when every member it carries isnullor absent, andparams.originalValueis frozen carrying every member it was given. - Extended properties and the extended properties example hold the two rules apart: an action's presentation property of another name is an extended property, while a differently shaped read of
labeloriconis an accessor pair on the subclass. The Action example points at the same section.
[0.16.0] - 2026-08-20
Changed
Breaking: a trigger answers with the
AbortEventHandlingExceptiona handler threw, where it answerednull. A run a handler ended and a run that reached no handler at all were the same answer, so neither the library nor a caller could tell them apart.triggerAction()hands the exception back, and code testing fornullsees the difference - which is the point, because the two mean different things.Breaking:
AbortEventHandlingExceptionthrown from anEnabledChangingActionor aVisibilityChangingActionrefuses the write. Those handlers are asked before the value is written, and the exception was caught and the write went through anyway, so a handler that meant to stop a change watched it happen. Nothing is written and nothing is announced now. Thrown from a*Changed*handler it is unchanged: the value is already written by then, and ending the run stops the handlers below it and nothing else.Breaking: an action belongs to the element's declaration, and a binding reads that one rather than a copy of it. A rule registered on a
List's item template therefore drives every row - the rows that already exist as much as the ones added later - and registering on one row registers on the template, because a row is a binding of it.unregisterAction()andclearValidators()read the same way, so a call on one row names the rule every row reads. What stays per row is the data: the value, the errors the rule produces there, the verdict. A handler that does not callsuprnow ends the run for every handler registered before it on that declaration, since the handlers of one declaration stand in one chain.Memory per field drops by about half, from roughly 1700 bytes to roughly 870: a row carried its own map of actions and now carries the declaration's. A 1000-row list of 8 fields goes from about 13.3 MB to about 6.8 MB.
ActionsMap.clone()is gone with the copying.ActionsMapholds one array and no maps. The two it kept - one grouping the actions of an identifier, one grouping the eager ones - indexed what registration order already states, and a trigger now walks the array backwards and passes over the actions of other identifiers. Measured over three actions the walk is about twice as fast asMap.get, and a standalone field carrying one validator falls from about 1420 bytes to about 655.Operator.NOTis stated with one operand. It reads its first alone, and the constructor asked for a second one that it never read, sonew Statement(field, Operator.NOT)did not compile. An operator held in a variable - whatOperator.fromString()answers with - still names both, because the compiler cannot tell it fromNOT, and a second operand underNOTis accepted as it always was.Action.execute()documents the failure path it always had: the promise it answers with rejects where the handler throws, so awaiting it is how a failure is reported, and a call that neither awaits nor catches leaves the rejection to the runtime.busyis cleared either way. The template case has its own example, since an event handler there is not awaited.
[0.15.0] - 2026-08-19
Changed
Breaking:
EmptyFieldis gone. It was a module-level singletonFieldthe package exported, shared by everyone who touched it, and nothing used it - not the library, not its tests beyond the one asserting its own warning, not any of the packages built on it. A missing element isnull:Group.field()answers with it andNullableField<T>names the type.Breaking:
Validatorand the types that go with it -ValidationFunction,ValidationFunctionResult,ValidatorBindingState- are reached through theValidatorsnamespace alone. They were exported from the package root as well, while every concrete validator was in the namespace only, so one member of the set had two spellings and the rest had one.import { Validator }becomesValidators.Validator. The error classes,MdStringandbuildErrorMessageare unaffected: they are what a field hands back rather than what validates it, and they stay at the root, where they were never duplicated.Breaking: nothing in
DisplayModefalls back toDisplayMode.FULLany more.fromString()throws for a string that names no constant,fromAny()throws for a number that is none of them, for a string that names none, and for input that is neither a number nor a string; all three errors read<value> is not a DisplayMode constant, so a caller recognises one wherever it was raised. A mode nobody defined is an error where it arrives, rather than a field that renders fully and is never questioned. Code that fed a wire payload tofromString()/fromAny()and relied on the fallback has to catch the error and choose the mode it wants, or askisDefined()first.Breaking:
DisplayMode.isDefined()judges a string against the constant names instead of routing it throughfromString(), soisDefined('HIDEN')answersfalsewhere it answeredtrue. It is the one member that does not throw - it answersfalsefor a number that is no constant, for a misspelled name, and for input of any other type. Thevisibilitysetter asks it, which makes both spellings of the same mistake throw:field.visibility = 'HIDEN'andfield.visibility = 999alike raiseError('visibility must be a DisplayMode constant')and leave the property as it was, where a misspelled name silently becameDisplayMode.FULL. A constant's name is still accepted, case insensitive.A form element whose parameters name no visibility still starts at
DisplayMode.FULL. That is a starting value, and it no longer stands in for input a parse could not read.Breaking:
parentis typed per class.FieldBase.parentisGroup | List | undefined, which is what the link holds: a row of aListgets theList.Fieldnarrows it toGroup | undefinedandActioninherits that narrowing, because aListholds rows and a row is aGroup, so a field is never aList's child. The sibling lookupfield.parent?.fields.othertherefore compiles unchanged on a field, androw.parent.fieldsis a compile error where it was a promise the type could not keep - the declaredGroupwas the container's type for a member of a group and the wrong one for a row. Code holding an element as aFieldBase- which is the type an action executor and aValidationFunctionreceive - narrows the container itself:(field.parent as Group)?.fields.other.Breaking: a structural comparison of two elements answers identity.
isEqual(fieldA, fieldB)read nothing either element holds - the state is in private class fields - and answeredtruefor any two instances of the same class; it isfalsenow unless they are the same element, and what they hold is compared asisEqual(a.value, b.value).FieldBasecarries aSymbol.toStringTagaccessor naming the element's class, which is the first thing such a comparison reads and a tag it does not know ends it there. The accessor is on the prototype, so an element carries nothing for it, andObject.prototype.toString.call(field)answers[object Field]where it answered[object Object].The build tooling moves to
eslint-config-velis3, which states its plugins as peer dependencies rather than carrying them, so the thirteen it names are declared here: eslint 10,@typescript-eslint8.67,eslint-plugin-unicorn73, prettier 3.9,@types/node26 and the rest.npm auditgoes from 18 findings to 3, and the three that remain are VitePress's pinned dev server with no fix published. None of it reaches the published package, which declareslodash-esandvue.A rule written against a field of an enclosing row now reads the row it runs in. Resolution answered within one record and took an element belonging to any other for the one element every record reads alike - true of a form field above a list, false of a field of the row a nested list sits in - so the lines of an order compared against the item template's
totalrather than against that order's. It walks the containers of the record outward before it settles for that, which is what the name form always did, so the two forms agree on the same rule.Breaking: writing what an element already holds is not a change.
visibilityandenabledran their*Changing*handler, enrolled the element in the open transaction and fired their*Changed*event for a write of the value already there; they now return before any of it, the way the value setter always has. A handler that answers with a value of its own is therefore not reached by such a write.A validator that re-runs and produces the message the field already carries leaves the instance standing.
ValidationError.sameAs(other)is what it asks - same class, same code, and the same component, bindings, body and classes - because a structural comparison of two errors answers nothing useful:ValidationErrorRenderContentholds a Vuecomputed, and two of those are never structurally equal, so every run replaced the error and every reader offield.errorsre-rendered over a verdict that had not moved.isSimpleComponentDef(null)answersfalserather than raising.typeof nullis'object'andinrefuses null, so the guard reached the operator and threw where it was asked a question it can answer.AbortEventHandlingExceptionis covered by tests: what a run it ends leaves unreached, that it does not escape the setter and leaves the value that was written standing, thattriggerAction()answers null for that run, that it ends only the run it was thrown in, that every other exception reaches the caller and unwinds the transaction, and that a*Changing*handler throwing it does not veto the write.CI loads the built ESM artifact and exercises it - the export list, a list composing and validating,
valueagainstfullValueover a disabled member, and a transaction announcing once and rolling back on a throw. The specs importsrc/, so nothing else reached what the package actually publishes.defaultDisplayModeis exported from the package. It names the mode an element starts at, so code that has to choose one for input it could not parse states the same constant the library does rather than repeatingDisplayMode.FULL.Breaking: a disabled
Listis serialized by theGroupabove it while its rows compose something, the way a disabledGroupalready was. The exception is one rule now - a disabled container is kept where its composed value is non-empty and left out where it is empty - and it holds whichever container the member is. A disabled leaf is left out as before. A form that readgroup.valueto submit it now carries the rows of a disabled list it previously dropped.Breaking:
List.valuerefuses a value that is neither an array nor null with aTypeError('Invalid value provided: a list takes an array of rows, or null to empty it'), where such a value was accepted and silently did nothing. The setter is typed, so this reaches a JavaScript caller or one writing throughas any; the constructor'svalueandoriginalValueare refused the same way.
Fixed
- A
Listconstructed with anoriginalValueand novaluetakes its rows from it, the way aFieldand aGroupalready did: it held no rows, read backnulland reportedisChangedastrueagainst the very value it was declared with. An explicitvalue: nullstill leaves the list empty - null is a value the caller means - and an absent value with nooriginalValuebeside it still starts the list empty.
[0.14.0] - 2026-08-19
Changed
- Breaking:
IBindParamsnamesoriginalValue,enabled,visibilityand the extended properties, where it named everything a constructor takes but the value.bind()read three of those and dropped the rest, sof.bind(v, { validators: [...] }),{ actions },{ touched }and{ errors }compiled and did nothing. They are refused by the type now:validatorsandactionsare carried from the declaration rather than supplied, andtouchedanderrorsare what a binding establishes for itself as it validates. - Breaking:
Group.bind()andList.bind()construct throughthis.constructor, asField.bind()already did, so a subclass binds into its own class instead of into the base one. A subclass whose constructor does not take(fields, params)- or(itemTemplate, params)on aList- never sees what it is handed and would answer with the declaration's data instead of the record's; both refuse that with aTypeErrornaming what to do about it, rather than returning a binding whose data is quietly wrong.
[0.13.0] - 2026-08-19
Added
settled()on every element answers with a promise that resolves once nothing at or below it is running - no asynchronous validation, noAction.execute()yet to settle. It resolves at once where nothing is running, so a submit path awaits it instead of readingvalidatingandbusyand reading them again. It answers for the moment it resolves and states nothing about the one after: work started later leaves the element running again.ValidationError.codenames what failed, so a program reacting to a particular failure does not have to match message text that is translated and configurable. The built-in validators state theirs:required,pattern,min,max,range,min-length,max-length,range-length,in-allowed-values,compare-to, andvalidation-failedon the error the library raises when a validation promise rejects.ValidationErrorTextandValidationErrorRenderContenttake it as a third constructor argument; an error built by hand carries whatever its author gives it, or nothing.A
ValidationFunctionreceives a fourth argument,signal: AbortSignal, and hands it to the work it commissions. It aborts the moment the verdict the run would reach stops counting: a newer run over the same field, a field the validator was taken off withunregisterAction()orclearValidators(), or a transaction that was unwound. A cancelled run reaches no verdict at all, so a check that rejects on abort says nothing and reports nothing. The cancellation an unregistration brings waits for the commit, so a transaction that rolls back puts the validator, its epoch and the run in flight all back, and the field ends up carrying the verdict that run reaches rather than reporting itself valid over a value nothing checked. A function with nothing to cancel ignores the argument.busyon every element: true while anything at or below it is still running — an asynchronous validation, or anAction.execute()below it that has yet to settle. On anActionit answers for that action's ownexecute()runs, and an asynchronous validation of the action itself is reported byvalidatingalone. It is what a form asks to disable a submit button while the tree is still deciding.Group.addField(name, field)andGroup.removeField(name)change the member set after construction. Both are transactional and announce the value once the transaction closes; the group's verdict re-forms over the members it holds, and a rule of the added field that names another member of the form reaches it.addFieldthrowsErrorwhere the group already holds that name andTypeErrorwhere the field belongs to another container — pass abind()of it.removeFieldhands the field back, detached and free to be taken elsewhere, and answersundefinedfor a name the group does not hold. Neither rewrites the baseline behindisChanged.List.lengthandList.items.lengthis the number of rows.itemsis a frozen array of the live rows, built once per change of the set: a write inside a row leaves the array a reader took as it is, and so does an assignment tovaluethat every row survives, while apush,insert,removeorclearreplaces it.getConfig(),setConfig()and theFormsConfigtype are exported from the package entry point beside the plugin, so the global options can be read and written without the plugin.
Changed
Breaking:
Group.fullValueis typedFieldsToFullValues<T>andList.fullValueisFieldsToFullValues<T>[], where both answeredRecord<string, any>or the list's ownvaluebefore.fullValuestates what an element holds wherevaluestates what it serializes, so every key is present and none is null - reading through a nested group needs no?..Listgains an override of its own: it maps its rows through theirfullValuerather than answering withvalue, so a field disabled inside a row is in it, and an empty list reads back as[]rather than as null.Breaking:
busystates an execution and nothing else. It istruewhile anAction.execute()at or below the element has yet to settle; an element that is not an action executes nothing and answersfalse. An asynchronous validation is whatvalidatinganswers for, on the element and on everything below it. The two questions are separate, so a form that gates on an idle tree reads both or awaitssettled().Breaking:
Requiredtrims a string before it measures it, so a value of spaces alone is no value and the field is invalid. Where the spaces are part of what the field holds,trimturns it off. The constructor takes the options beside the message or on their own:typescriptnew Validators.Required(); // whitespace-only fails new Validators.Required({ trim: false }); // whitespace-only passes new Validators.Required('Please enter a name'); // message, trimming still on new Validators.Required('Please enter a name', { trim: false });RequiredOptionsis exported. Only strings are trimmed; an array, an object or any other value is measured as it stands. The two first arguments are told apart by shape, so an object naming a component is still a message.Breaking: the
Statementconstructor refuses an operand it cannot compare. An operand that isundefined— whatgroup.fields.typoNameanswers with — or a function — a field accessor handed over uncalled — throws aTypeErrornaming the position it was written at. Everything else stands: a field, a nested statement,null,NaN,0,'', an array, an object withincludes.Operator.NOTnever reads its second operand, so that operand is not checked under it. A name the group does not hold reachesStatementasundefinedonly throughgroup.fields;group.field('typoName')answersnull, which is a value a statement may legitimately compare against.Breaking:
validatinganswers for the whole subtree: a group or a list reports true while an asynchronous validation is in flight anywhere below it, where it used to answer for its own runs alone. A form asks one element what the tree is doing. The answer is a pair of counters rather than a walk, so the read costs nothing and a run that starts or settles costs the nesting depth.Breaking:
busyis a member of every element, so a parameter of that name is no longer an extended property:new Field({ busy: true })throws aTypeErrorthe wayvalidandvalidatingalready did.lengthanditemsare members ofListfor the same reason, sonew List(tpl, { length: 3 })andnew List(tpl, { items: [] })throw as well. A presentation layer that carried any of the three as a property of its own states it under another name.Breaking:
GroupValue<T>isPartial<FieldsToValues<T>> | null. A group leaves a disabled member out of the value it builds, so every member reads as possiblyundefined— which is what the runtime always handed out.Breaking: the five validators that never read their type parameter no longer take one:
Required,Pattern,MinLength,MaxLengthandLengthInRange.new Validators.Required<string>()becomesnew Validators.Required().InAllowedValues,MinValue,MaxValue,ValueInRangeandCompareTokeep theirs, where it types an argument or a callback.InAllowedValuestakesAllowedValues<T>— an array, aRef<T[]>or a() => T[]— and reads the list at each validation instead of at construction. A list that arrives from a server after the validator is built, or one another field's value leaves open, is the list the value is measured against and the one{allowedAsText}and{allowedValues}name.group.fieldshands out a guarded view of the member map. Reading it reaches the members themselves; assigning, deleting and now alsoObject.definePropertythrow aTypeErrornamingaddField()orremoveField()as the way to change the set.list.valueacceptsListValue, solist.value = null— the write that clears a list, and the onegroup.value = nullmakes into every member — type-checks.
Fixed
CompareTowithdraws the errors it placed on a field when it is taken off that field.unregisterAction()on aCompareTodropped the registration and left the error standing, so the field stayed invalid on an error no validator was left to take back.
[0.12.1] - 2026-08-19
Changed
- The build target is ES2022, up from ES2015. The only thing it changed is
field-base.ts: it is the sole file holding private class fields, and an ES2015 output has to lower each of its 51 access sites to a WeakMap lookup guarded by an access check. The artifact goes from 92 715 to 87 902 bytes, 25 728 to 24 547 gzipped, and the private fields ship as themselves. Every runtime the package already declares support for — Node 22 and up — runs ES2022 natively.
[0.12.0] - 2026-08-18
Added
unregisterAction(action)on every element drops one action and answers whether the element held it. The instance goes on serving every other element it was registered on, so unregistering a validator from one row of aListleaves the other rows validating. AValidatorwithdraws the errors it put on the element as it goes, so the element cannot be left invalid on an error no validator is around to take back.registerActionBefore(action, before)putsactioninside a handler already registered:beforewraps it and reaches it throughsupr. Registration order alone could not arrange that, so an action added to a form someone else built could only ever become the outermost handler.- A rollback takes back the actions its transaction registered and puts back the ones it unregistered, so "a transaction undoes everything it did" holds without an exception for registrations.
Changed
- Breaking: the package is ESM-only. The
requirecondition, themainfield, the UMD artifact (dynamicforms-vue-forms.umd.cjsand its map) and theindex.d.ctscopy of the declarations are gone;exportsresolves one build with one set of types. A CommonJS consumer reaches the library throughrequire()of an ES module, which Node supports from 20.19 and 22.12.lodashleavesdependencies— it was there only so the UMD artifact could require the CJS packaging oflodash-es— leavinglodash-esas the single runtime dependency. Shipping one format also removes the failure a mixed graph produced: the library compares elements withinstanceofand keys its internals with module-levelSymbol(), so a program holding both copies rejected a validFieldwithInvalid fields object provided. - Breaking:
engines.nodeis>=22, up from>=18. Node 18 reached end of life in April 2025, andrequire()of an ES module — what a CommonJS consumer now relies on — is stable from 22.12. - Breaking: the
vuepeer range is^3.5.2, down from^3.4. The declarationsvue-tscemits writeMessagesWidgetas aDefineComponentwith 20 type arguments; the type takes 19 through Vue 3.5.1 and 20 from 3.5.2, so anything below that floor reportsTS2707in a consumer type-checking withskipLibCheck: false. Nothing in the source needs a Vue newer than 3.0 — the range states what the shipped artifact is known to work against, and CI now type-checks the emitted declarations against exactly that floor on every run. - Breaking:
ActionsMapholds the actions of one identifier as a list rather than composing them into nested closures, and no longer extendsMap<symbol, FieldActionExecute>.triggerChain()andcloneWithoutValidators()are gone —trigger()is the one way to run a group and no longer runs the eager pass on the quiet, andclearValidators()unregisters its validators instead of rebuilding the map.unregister(),triggerEagerFor()andhasEagerare new. The order handlers run in is unchanged: newest registration first, reaching the ones before it throughsupr. - Breaking:
eageris read per action instead of perclassIdentifier. A lazy action standing under the same identifier as an eager one was previously dragged into every eager pass; now only the eager ones run. unregisterFrom(binding)runs inside the operation that drops the registration rather than after it commits, so an operation that unwinds puts back both the registration and what the action released.
[0.11.0] - 2026-08-18
Added
rebind(data)on every element: the exchangebind()makes, made in place. The element is the same instance afterwards — its identity, its actions, its extended properties and its place in whatever container holds it all stand — and it ends up over the new record, withoriginalValuebaselined to it,touchedback tofalseand the validators run. It is what recycles one element across records, which is what a virtualised renderer does with the rows it keeps:typescriptconst row = list.get(0)!; row.rebind({ name: 'Jane', age: 25 }); // same instance, same component, next recordThe element announces nothing of its own about the exchange: no
ValueChangedActionfires for it, the way none fires for an element that was just built. Its members do announce theirs, and a verdict that moves is announced as always, so a rebound row that is invalid says so to the list holding it. Inside an opentransaction(), a change the element is already owed an announcement for stands, and the commit reports it from where the element stood when the transaction opened. On aGroupthe record need not name every member: a key it leaves out is taken from the element'sdeclaration.IBindParams<T, X>, the exported type of the second argument ofbind().
Changed
Breaking:
clone()is nowbind(), and takes the data it binds as its first argument. The call does what it always did — a new element of the same class, carrying the same registered action and validator instances and the same extended properties, detached and withoriginalValuebaselined to the data — and it is now named for that. The value moves out of the override object and into an argument of its own:typescriptconst copy = field.clone(); // before const copy = field.bind(field.value); // after; field.bind() is the same thing const cleared = field.clone({ value: null }); // before const cleared = field.bind(null); // after const row = template.clone({ value: { name: 'John' } }); // before const row = template.bind({ name: 'John' }); // afterThe second argument is the rest of the overrides, typed
IBindParams<T, X>—IFieldParams<T, X>withoutvalue.originalValueis still read by key presence,enabledandvisibilitystill fall back to the element bound, extended properties it names are still written over the ones carried over, andvalidatorsandactionsare still accepted and ignored. Data ofundefinedcounts as none supplied and an explicitnullclears, onField,GroupandListalike.Breaking:
List.remove()andList.pop()hand back the row itself, released of the list, rather than a copy of it.ListItemRemovedActionreceives that same instance, and it is the instancelist.get(index)answered with before the call. What the row holds comes with it: its values, its errors, and the change history behindisChanged— which the copy erased, so an edited row used to come back reportingisChangedasfalse. The row carries noparent, so it can be pushed straight into another list, or back into this one.
[0.10.2] - 2026-08-18
Added
Extended properties. Every form element now carries whatever properties your application declares for it beyond the members of its class — a label, a hint, a css class, a permission flag — so a form whose shape arrives from a server has somewhere to put them and a UI layer has somewhere to read them from. Declare them as the element's second type argument, pass them to the constructor alongside everything else, read them through
extraand write them withsetExtendedValues():typescriptinterface Presentation { label: string; hint?: string } const name = new Field<string, Presentation>({ value: 'John', label: 'First name' }); name.extra.label; // 'First name' name.setExtendedValues({ hint: 'as in your passport' }); // label stays as it wasField<T, X>,Action<T, X>,Group<Fields, X>andList<Fields, X>all take it,Xdefaults to{}and is never inferred, so existing code compiles unchanged and an element that declared no extended properties still rejects one. The read is tracked like every other read through an element, so a template renderingfield.extra.labelre-renders when the property is written, and a write inside atransaction()that rolls back is put back with it.clone()carries them over, and overrides it is given are written over them — which also gives every row aListbuilds the properties its item template carries.extrareads back asReadonly<Partial<X>>: a parameter object carries as few of them as it likes andsetExtendedValues()writes as few as it likes, so a property is there once something has put it there.IFieldParams<T, X>, the exported type of the parameter object taken by every constructor and everyclone().IFieldConstructorParams<T>goes on naming the members every element takes.
Changed
- A constructor parameter naming something the element's class does not declare is now an extended property rather than a property written onto the element itself. It was already a compile error to pass one; code that passed one past the type system with
as anyand read it back asfield.myPropnow reads it asfield.extra.myProp. Parameters naming a member the class does declare are unaffected:enabledstill setsenabled,validstill throws aTypeError, and anAction'slabelandiconstill reach its value. A subclass of your own is a member the class declares when it declares an accessor; a class field is defined on the instance after the base constructor has applied the parameters, so a parameter of that name becomes an extended property and the field keeps its initializer.
[0.10.1] - 2026-08-17
Documentation only. No behaviour changes, and no source under src/ was touched.
Added
- The model, a new guide page placed ahead of the API reference. It states the whole design in one place — elements, declarations and clones, how a
Listbuilds rows, what a record is and how a shared rule resolves within one, when each event fires, where validity comes from and where a value comes from — for a reader who has never used the library. The reference pages carry the per-symbol truth; this is the shape they belong to. - An
Actionexample, showing an action end to end: declared with a label and an icon, enabled by the form's validity through aConditionalEnabledAction, executed, and reportingbusythrough an asynchronous submit that can fail. It says plainly whyActionis the one deliberate exception to the library being UI-agnostic and that a UI library is expected to extend it, and it cross-links to@dynamicforms/vuetify-inputs. - A single upgrade path from 0.6.1 to 0.10.x at the top of the migration guide, leading with the three breaks that fail with nothing in the console —
watch(field, cb),readonly(field)andisEqualover two elements — and ending in a checklist. The per-release sections stay below it for a project crossing one release. - A versioning and support statement in Getting Started: what a
0.xminor means, and the supported Vue, Node, module-format and browser matrix. The changelog is now reachable from the site navigation.
Fixed
Field.valueno longer claims that assigning an object firesValueChangedActioneven for the same reference. Values are compared by identity: a new object announces a change even when it is deeply equal to the old one, and the very object the field already holds announces nothing. Mutate a copy and assign it.field.errorsis documented as it behaves.validfollows an error pushed into the array immediately, on the field and on every container above it; what waits forvalidate()is theValidChangedActionannouncing the transition. The array is reactive, sofield.errors[0] === myErrorisfalsefor the error a validator returned — compare by content, or unwrap withtoRaw().- The reactivity claims are corrected everywhere they appeared. A form element has not been a Vue proxy of itself since 0.7.0; every read through it is tracked, and the watch form to use is
watch(() => field.value, cb). List.value's setter is described as it behaves: an array assigns, anull— which is whatgroup.value = nullwrites into a nested list — releases every row, and any other value leaves the rows untouched.ActionsMap's documented surface liststriggerChain()andwillTrigger(), which it has always had, andStatement'soperand1Value/operand2Valueare documented.- A hand-written validator that reads a sibling is shown calling
field.markRecordIncomplete(), which is what aListrow needs to carry its own verdict from the moment it exists. TheListexample no longer revalidates each row from aListItemAddedAction, which coveredpush()andinsert()and missed every other way a row is built. MessagesWidgetrenders acomponentNamedirectly only for the common HTML tag names; every other name, including an uncommon tag, is resolved as a globally registered component.
[0.10.0] - 2026-08-17
Added
field.declarationnames the element a field was declared as: itself for a field built from parameters, and the field it was cloned from for a clone, transitively. Every row aListbuilds from an item template is a clone, solist.get(0).fields.a.declaration === template.fields.a.field.bindingsOf(declaration)lists the elements of a subtree that were declared as the given one —list.bindingsOf(template.fields.a)is theafield of every row.Statement.evaluate(scope?)takes the element whose record the field operands are read in, so one statement serves every row of aList. Without an argument it reads the fields it was built from, as before.CompareToaccepts the field to compare against as a name or as a callback receiving the field being validated, next to the field itself. The exported type of the parameter isValidators.CompareToTarget.FieldActionBase.state(key, init)holds what an action remembers between runs against the element it ran over, or against the record that element belongs to. An action instance is shared by every clone of the element it was registered on, so what it keeps on itself is shared by every row; what it keeps here is not, and is released with the element.field.markRecordIncomplete()lets an eager action say that it looked for a second element of the record and the record was not assembled yet. The container that completes the record runs the element's eager actions again, and so does a container that takes the record in afterwards.
Changed
- Breaking:
CompareTo's first constructor parameter widens fromFieldBasetoFieldBase | string | ((field: FieldBase) => FieldBase | null | undefined). Passing a field keeps working, and is now resolved within the record being validated rather than read as the one field it names. - Breaking: the two optional hooks of
FieldActionBaseare renamed:boundToField(field)isboundToBinding(binding), and it now runs for every element the action comes to serve rather than only for the one it was registered on;unregister()isunregisterFrom(binding), which names the element the validator was dropped from. An action that overrode neither is unaffected. - Breaking: a cross-field rule that was inert inside a
Listnow applies, so a form holding one may report errors, hide fields or disable fields it did not before. The verdicts are the ones the data always called for. ActionsMap.bindTo(owner)tells every action in a map that it servesowner; cloning an element calls it, which is what makes an action registered on an item template serve every row.Validatorkeeps its per-field run sequence throughbindingState(field), and a subclass widens that record by overridingnewBindingState(). The exported type isValidatorBindingState.
Fixed
- Conditional actions work inside a
List. Registered on the item template, a conditional action serves every row, each row holds a result of its own, and a change in one row reaches that row alone. Previously the rows shared a single result and a row built by the list never had the action bound at all, so the condition was silently dead. CompareTocompares within the row it is validating. It read the item template's field, so a row whose two fields matched was reported invalid and a row where they differed was reported valid — password/confirmation and date-from/date-to being the cases it exists for.clearValidators()on one element leaves the same validator instance validating every other element it was registered on. Dropping the validators of one row of aListsilenced the validator in every row, so the form reported itself valid when it was not.- A statement's fields are read once per record rather than once per row of listeners: one handler is registered on each field the statement reads, however many rows read it.
- A row that holds the very values its item template holds carries its own verdict. A cross-field rule is run again once the record is assembled, so a
Listrow, an item the list builds to fill a gap, the groupremove()hands back and aclone()all report what their own fields support instead of reporting themselves valid. - A cross-field rule registered on a single row is that row's rule. A change of the field it compares against, or of a field its statement reads, no longer plants errors on the rows that never took the rule on — errors those rows had no validator to clear.
- Registering a validator again after
clearValidators()re-arms it fully: it listens to the field it compares against again, where before it only re-validated on writes of the field's own value. - A
CompareTonaming a field the form above the list holds resolves it. A row reaches the form only once the list takes it in, which is now where the rule is run again.
[0.9.0] - 2026-08-17
Added
Action.busyreports whether an execution of the action has yet to settle.execute()raises it as the chain is entered and clears it once the run has settled, whether it resolves or rejects; overlapping runs are counted, so it stands until the last of them settles. It is a form-state flag a button binds to in order to disable itself while a submit runs.
Changed
- Breaking:
Action.execute(params?)is asynchronous. It answers what theExecuteActionchain returned, as a promise, where it previously discarded the result and answeredundefined. The chain is still entered synchronously, so a handler has already run by the time the call returns, but a handler that throws now rejects the promise instead of throwing out of theexecute()call - a caller that wrappedexecute()intry/catchhas toawaitit or attach a.catch(). A call that does neither leaves the rejection unhandled, which under node's default settings ends the process; a Vue template handler needs no change, because Vue attaches its own catch to the promise a handler returns.paramsis now optional. - Breaking: writing
Action.labelorAction.iconassigns a new value object instead of writing into the one the action holds. Both are now ordinary value changes:ValueChangedActionfires,isChangedanswers over them, and a disabled action refuses the write, where previously none of the three happened. A value object you passed asparams.valueand kept a reference to no longer follows the action once either setter has run; reading still sees your writes to that object until then. Writing the label or icon the action already holds changes nothing and announces nothing, and clearing one withundefinedleaves the action reporting itself unchanged against a baseline that never carried it.
Fixed
labelandiconare assignable on an action constructed without a value.new Action({}).label = 'X'threw aTypeError, because the action held the frozenoriginalValuebaseline as its value.
[0.8.0] - 2026-08-17
Added
transaction(fn)runs several writes as one atomic change. The events they produce are announced once, at the end, over the net result: two writes to two members of a group announce oneValueChangedActionon the group instead of two, and a value that goesA->B->Ainside one transaction announces nothing at all. A nested call joins the transaction it found. The callback must be synchronous -transaction()throws aTypeErrorthe moment it returns a thenable - so a transaction structurally cannot cross anawait.- A transaction can be undone.
tx.rollback()unwinds it without an error and the call answersundefined, and a throw out of the callback rolls back and rethrows. A rollback puts back the whole of every element the transaction modified -value,originalValue,touched,errors,enabled,visibility, the validators aclearValidators()dropped, and aList's rows, with the rows the transaction created dropped and the ones it removed re-adopted at their old positions - and announces nothing. An asynchronous validation started inside a transaction that was rolled back runs to the end and its verdict is discarded, so a field is never left invalid over a value the form never held. What a rollback cannot undo are side effects - a handler that called a server already did - and actions registered while it was open, which stay registered. - The handle
transaction(fn)hands its callback is usable only for the duration of that call. Callingrollback()on a handle kept beyond it throws aTypeErrorrather than unwinding whatever transaction happens to be open at the time.
Changed
- Breaking: events are announced when the operation carrying them finishes rather than as it runs, and each is announced once. Every mutating operation is a transaction; where you open none, the operation is the transaction, so a single write still announces exactly one change. What moves is the timing and the count for everything else:
- an operation that changed one element several times announces the net change, and announces nothing where the element ends where it started;
- a handler is never shown a half-applied state.
insert()past the end of a list pads the gap first and announces each addition over the set the operation ended on, where it used to announce each over the set that stood at the time; - the order within one operation is now the causal one: an element announces its value before the verdict formed over it, and the deepest element announces before the container above it. A field carrying a validator used to announce its new verdict before its new value.
- Breaking: a handler that throws no longer leaves a container half-applied. The operation rolls back and the throw propagates, so
group.value = {...}whose members' handlers fail partway leaves the group exactly as it was, with its verdict and its value caches intact.AbortEventHandlingExceptionis unchanged and still stops the chain without undoing anything. - Validators run at the write that triggers them, which is inside the transaction, so a validator reading a sibling sees the sibling's working value. Their verdict is what the commit announces.
- Breaking:
ActionsMap.cloneWithoutValidators()returns the copy without releasing the validators it left out;unregister()on each of them is now the caller's to make, and the newActionsMap.validatorslists them.clearValidators()on a field is unchanged for a caller: it still releases them, once the operation it ran in has finished rather than as it runs, so an operation that unwinds leaves the field's validators intact. - Measured on 1000 rows of 8 fields, against 0.7.0: a whole-list assignment 18.8 ms to 24.3 ms, one field write 0.0081 ms to 0.0097 ms,
remove()andpush()-filling within a few per cent. The added cost is the record a rollback restores from, taken once per element an operation modifies, plus the commit's own bookkeeping.
[0.7.0] - 2026-08-17
Changed
- Breaking:
watch(field, cb)andwatch([field], cb), with a form element passed directly as the watch source, no longer fire. A form element is no longer a Vue proxy of itself - its mutable state is held in a reactive object beside it, and the element carries__v_skip- so the deep traversal Vue starts for a reactive source stops immediately and the watcher subscribes to nothing. Nothing is logged and nothing throws, so this is worth searching for:watch(() => field.value, cb)is the replacement, and every other read is unchanged. Templates,computed,watchEffectand a getter passed towatchall track field members exactly as before.toRaw(field)now returns the field itself. A write the value setter refuses - the same value, or any value on a disabled field - no longer re-runs the effects that read the value. - Breaking:
readonly(element)no longer protects anything, and fails as silently as the watch above. Vue'sreadonly()stops on__v_skipand hands the element straight back, soreadonly(field) === field,isReadonly()on the result isfalse, and a write through it reaches the field. Hand out the value -field.valueandgroup.valueare frozen - or acomputedover it. - Breaking: an element's state is held in private class fields, so nothing outside the element reaches it.
parentandfieldNameare read-only accessors over that state instead of own properties - assigning either yourself throws aTypeErrorrather than being silently accepted.Object.keys(element),Object.getOwnPropertySymbols(element),JSON.stringify(element)and lodashisEqualsee none of an element's state: a structure that contains its own descendants' back-references is walked by all four without climbing back up, andisEqualover two elements answerstruefor any two instances of the same class. Comparea.valuewithb.valueinstead. - A
Listno longer walks itself to answer a change. Every mutation and every field edit anywhere inside a list used to rebuild the whole list's value, deep-compare it with the previous one and re-check the validity of every field of every row, so oneinsert()cost work proportional to the entire list and filling a list was quadratic in its length.Group.valueandList.valueare now cached behind a version that a write raises along its own branch,validis read through a lazily built computed and maintained for events as a tally, and a mutation that nothing listens to no longer builds a value at all. Measured on 1000 rows of 8 fields: filling bypush()13.1 s to 0.37 s, writing one field 33 ms to 0.017 ms, readinglist.valid11.3 ms to 0.0006 ms,remove()27 ms to 0.75 ms.list.value = rowsreuses the row objects positionally instead of rebuilding them, so a same-length assignment keeps each row's identity and a keyedv-forstops remounting every row. A reused row is reset to the state the row built for that position would have been in, down tooriginalValue,isChanged,touchedand its errors, and the new set is installed whole, so a validator readinglist.valueduring the assignment sees the whole list at every point. - A
DisplayModelookup no longer rebuilds the enum's value list on every call, and a form element allocates its action map only when it has an action to hold. - The object
Group.valueandList.valueread back is frozen, rows included: the same object answers every read until the next change, and writing into it throws in strict mode. Assign a new value instead.originalValueholds a copy of its own, so it is never the objectvaluereads back. - A
Listreleases the rows it drops -remove(),pop(),clear()and an assignment that shortens the list. A released row loses itsparent, stops counting towards the list's validity and can be pushed into another list or back into the one that held it. A container still refuses an element that belongs to one, with aTypeError.remove()returns a clone of the row, as before.
[0.6.1] - 2026-08-17
Added
FieldBase.validationEpoch, a read-only counter of the validator generations a field has been through.clearValidators()raises it, and a validator that reads it when a run starts can tell that the result it is about to apply belongs to validators the field no longer carries. Custom asynchronous validators can use it the same way the built-inValidatordoes.
Changed
List.insert()resolves a negative index before it reports one. The number it returns, and the index it announces withListItemAddedAction, are the position the item occupies rather than the argument as given: on a two-element listinsert(item, -1)reports1andinsert(item, -99)reports0, where both used to report the argument back. Where the item lands is unchanged - the argument has always reachedsplice, which counts back from the end and stops at the start - so a caller who ignores both the return value and the event payload sees the same list.- A
Groupconstructor no longer firesValueChangedActionfor the value it is given.new Group(fields, { value, actions: [new ValueChangedAction(fn)] })calledfnonce during construction, withnullas the old value, because the members were filled in through the public setter; they are now filled in directly and the cache is primed from the result. A handler that relied on that call to see the initial value should readgroup.valueafter construction instead. - The UMD build exposes the global
DynamicFormsVueForms, withvueread asVueand lodash as_. lodashandlodash-esare declared asdependencies, so they install with the package.vueis the only peer dependency.engines.nodedeclares>= 18.prepackbuilds the package, sonpm packandnpm publishcannot produce a tarball whosedistpredates the sources it was cut from.package-lock.jsonis tracked in the repository, so a checkout resolves the dependency tree the release was built and tested against and CI can install withnpm ci.
Fixed
ListItemAddedActionreports the real index of every elementinsert()creates. The elements inserted as padding, to reach an index past the end of the list, were each announced with the index one past their own position, solist.insert(item, 3)on an empty list reported1, 2, 3, 3instead of0, 1, 2, 3.Statement.evaluate()returns a boolean for every operator.AND,ORandXORreturned the operand itself andINreturned whatever the container'sincludes()produced, so a conditional callback received e.g. the number0instead offalse, andConditionalStatementActionfired again when that operand changed tofalse- a non-transition. For these operators the truth values are unchanged.NOT_INis the negation ofIN. Anoperand2that carries no callableincludes-null,undefined, a number, a plain object - made both operators reportfalse, so a statement and its negation agreed.INisfalsefor such an operand andNOT_INistrue.- One
ValidationErrorinstance may be reported by more than one validator. The second validator to receive it threwTypeError: Cannot redefine property: source. Each validator now holds an error of its own - the instance itself, or a copy that keeps its prototype and every property and therefore renders identically - and withdraws only that one, in whatever order the fields are cleared. Two validators on the same field that both report one instance consequently leave two errors on it, as two equal instances always have. - A
Groupconstructed with parameters that carry novaluekeeps the values its member fields were built with.new Group({ a: new Field({ value: 1 }) }, { visibility: DisplayMode.HIDDEN })cleared every member tonull; it is now{ a: 1 }, andisChangedisfalse. Avalueofundefinedcounts as no value, so spreading an optional property empties nothing;{ value: null }is a value the caller means and does clear the members, andoriginalValuealone still seeds the value. new Field({ value: null })keeps thenull. An explicitly null value fell back tooriginalValue; only an absent orundefinedone does now.clone()takes a value override only from a value the caller supplied.clone({ value: null })clones with thenullinstead of falling back to the value of the field it was cloned from, sogroup.clone({ value: null })clears the clone's members and aListcloned that way comes out empty. Avalueofundefinedis not a value the caller supplied and behaves like an absent key:field.clone(),field.clone({})andfield.clone({ value: undefined })all keep the current value, so spreading an optional property blanks nothing.originalValueis still read by the presence of its key.- A child that turns invalid without its value changing propagates its validity to the enclosing
GrouporList, soValidChangedActionfires there and the container'svalidfollows. The climb stops at the first ancestor whose own validity does not change. - A
Groupand aListannounce at most the net validity transition per operation, never a verdict over a state they held only halfway through one. Assigning{ a: 'x', b: '' }to a group whoseawas empty andbfilled firedValidChangedActiontwice,trueand thenfalse; so did assigning to a single member of a group that carries a validator of its own, assigning a whole list, andvalidate(true)on either container - each of which walks the members one at a time. The container now forms its verdict once, over the finished state: one event when the operation flips its validity, none when it leaves it where it found it, and the same at every level of nesting. Each member still announces its own transition. Group.value = nullclears a nestedList. AListignored an assignment that was not an array, so a list nested in a group kept its rows while every sibling field was emptied, andgroup.clone({ value: null })cloned it with its rows intact.ValueChangedActioncarries the real previous value. The first change of aGroupmember, and every change of aListafter an assignment, reportednullas the previous value.- Asynchronous validators are sequenced per field. A verdict from a run that a newer run has superseded is discarded instead of overwriting the newer one, and a rejected validation promise no longer surfaces as an unhandled rejection: if its run is still the current one, the validator replaces its own errors on the field with a single
Validation could not be completederror and reports the reason once asconsole.error('Validation failed', reason); if it has been superseded it is dropped silently, with nothing logged. A validator that cannot reach its server therefore produces a failed validation and not a passing one, so a value that was never checked cannot be submitted. The failure error is an ordinary error of that validator: the next successful run of the same validator withdraws it.field.validatingreturns tofalsein every case. clearValidators()cancels validation that is still in flight. A result arriving from a cleared validator is dropped, leaving the field with no errors,valid === trueandvalidating === false.clearValidators()announces the validity it produces. It setvalidsilently, so noValidChangedActionfired and an enclosingGrouporListwent on reporting itself invalid over a field that no longer had a validator. The transition now takes the ordinary path: the event fires on the field when the verdict changes, nothing is announced when it does not, and the containers above re-evaluate.- Eager actions passed to a
Field,Group,ListorActionconstructor - validators among them - run exactly once during construction, over the constructed value. Each registration ran them once more, over whatever the element held at that moment:undefinedon aFieldor anAction,nullon aList, so a validator reading the value had to guard against a value the element never held. - A validator message given as a
Refor acomputedkeeps its reactivity: it is unwrapped where it is read, so a change to the ref changes the rendered message. A ref holding anMdStringstill renders as markdown, with itsoptionsandplugins. - The CJS/UMD artifact requires
lodashinstead of the ESM-onlylodash-es, sorequire()of the package resolves on every supported Node version and under Jest. The ES artifact still importslodash-es. - The
repositoryandbugsURLs point at the current location,https://github.com/dynamicforms/vue-forms.
Documentation
- The API reference describes what this release settles: the order in which a constructor applies its parameters and the single eager run that closes it,
validationEpoch, the reach ofclearValidators(), the value rule ofclone(), the boolean result ofStatement.evaluate()andIN/NOT_INover an operand withoutincludes, the resolved indexinsert()returns and announces, and whenValidChangedActionfires on a container. The readme documents the two build flavours and their lodash packaging, the Node requirement, and asynchronous validation. - The GitHub links in the readme, on the documentation home page and in the site's social links point at
https://github.com/dynamicforms/vue-forms.
[0.6.0] - 2026-08-15
Added
FieldBase.beginValidating()andFieldBase.endValidating(), the pair an asynchronous validator brackets its work with to report that it is running.- The value types
FieldsToValues,GroupValue,GroupValueInputandListValueare exported.
Changed (breaking)
IFieldConstructorParamsnow lists only the writable members -value,originalValue,enabled,visibility,touched,errors, plusactionsandvalidators. Passing a derived member such asnew Field({ value: 1, valid: true })used to type-check and then throw at runtime; it is now a type error.IFieldConstructorActionsListlost its type parameter and its members are typedFieldActionBase[].- A
Fieldsubclass applies its parameters from the constructor, so the protectedinit(params)hook runs before the subclass's own class field initializers - the factory used to run it after the instance was fully constructed. An initializer now overwrites whatinitassigned to the same member, andinitreads such a member asundefined. Move that state into theinitoverride. field.validatingis a getter over the number of validators still running, so it is read-only. Asynchronous validators bracket their work with the newbeginValidating()/endValidating()methods. It is typedbooleanrather than the literalfalse, soif (field.validating === true)no longer reports TS2367.Group.valueandList.valuecarry their real value types: forGroup<{ name: Field<string> }>,group.valueis{ name: string } | nullwhere it used to collapse toany, andList.valueisListValue.Group.value's setter accepts a partial value structure, which is what it has always done at runtime.
Changed
- Set
rootDirexplicitly intsconfig.build.jsonand verify declaration output size in CI. TS 6.0 stops inferring it, and without it the rolled upindex.d.tscomes out empty with a green build.
Fixed
GroupandListare reactive from construction, asFieldandActionalready were.Group.errors- which is where a group-level validator writes -Group.visibility,Group.enabled, group-levelGroup.valid, and the structural changes made byList.push(),insert(),remove(),pop()andclear()now trigger a Vue re-render. They were plain properties on a plain object, so a group-level validation message, aConditionalVisibilityActionon a group and av-forover a list never repainted.List.clone()on an empty list. It spreadvalue, which isnullwhile the list holds no rows, so bothnew List(template).clone()and cloning aGroupthat holds an empty list threw aTypeError.- A
Groupfield may be named after anObject.prototypemember,__proto__included.new Group({ toString: new Field() })used to throw "Field toString is already in this form", and a field named__proto__- whichJSON.parsedoes produce - was dropped fromfieldsand fromvalue. The group's own value setter reads only own keys of the object it is given, so a field named after a prototype member is no longer assigned that member's value. Action.labelandAction.iconare settable on an action constructed without a value, such asnew Action({ actions: [new ExecuteAction(...)] }). Its value object was the frozen baseline used fororiginalValue, so assigning either threw "Cannot assign to read only property".- Add
typesconditions to packageexports, so consumers usingmoduleResolution: bundler,node16ornodenextresolve the library's types instead of falling back toany(TS7016). A separateindex.d.ctsis emitted for therequirebranch. - Expose the stylesheet as
@dynamicforms/vue-forms/style.css- it was shipped indistbut unreachable throughexports. Documented in the getting started guide and theMessagesWidgetreference.
Removed (breaking)
Field.create()andAction.create(). Fields are constructed withnew, the way groups and lists always were:new Field({ value: 1 }),new Action({ value: { label: 'Save' } }). The constructor guard that used to makenew Field()throw is gone as well. Type inference is unchanged -new Field({ value: 'a' })isField<string>.- The
reactiveValuemember onField,GroupandList. Every field is now a Vue reactive object from construction on, sofield.valueis itself reactive and needs no computed wrapper. Replaceconst out = form.reactiveValueplus{{ out }}with{{ form.value }}. - The
IFieldandIFieldActioninterfaces. Use the classesFieldBase(orFieldBase<T>) andFieldActionBaseinstead. The interfaces duplicated the class surface and promised a structural implementability the runtime never allowed:Grouprejects a field that is notinstanceof FieldBase, andActionsMaprejects an action that is notinstanceof FieldActionBase.
Documentation
- Audited the whole of
docs/against the source. Corrected statements that no longer matched the code (enabledcascading,Statementreactivity, theEnabledChangingActionreturn value, list defaults and every built-in validator's default message), and documented previously undocumented public API:Action,FieldBase,AbortEventHandlingException,buildErrorMessage,EmptyField,RenderableValueand theNullable*type aliases. - Removed passages that narrated the library's own history - a superseded claim left standing next to its correction, reassurances about doubts the reader never had, and leftovers of a mechanical API rename.
[0.5.0] - 2026-01-28
- Remove default class from MessagesWidget
[0.4.7] - 2025-10-29
Added
- Support functions for
RenderContentto support dynamically retrieving text and allow for translations.
[0.4.6] - 2025-09-24
Added
- Add plugins and options to MdString supporting markdown-it extensibility
[0.4.5] - 2025-09-19
Added
- Add field touched property to indicate when user had at least visited a field
[0.4.4] - 2025-09-08
Added
- Add revalidate parameter to field validate method forcing revalidation
[0.4.2,0.4.3] - 2025-09-06
Added
- add classes to ValidationError (and descendants)
- add messages-widget
[0.4.0] - 2025-09-02
Added
- Alias for ValidationErrorRenderContent: RenderableValue (it's not only usable for errors)
- Support for v-html content in RenderableValue
Changed - breaking changes
- renamed in error-message-builder: CustomModalContentComponentDef → SimpleComponentDef
[0.3.5] - 2025-06-03
Added
- Support type generics on group.value / group.reactiveValue
[0.3.3 - 0.3.4] - 2025-05-30
Added
- Extending classes: Support for extending the vue-forms classes, particularly the Action class
[0.3.0] - 2025-05-19
Added
- Async Validator Support: Re-introduced async validation with loading states
- Improved Reactivity: Enhanced reactivity for Field and Action components
- Simplified validator architecture for better maintainability
Fixed
- Better reactive behaviour for field state management
Removed
- Removed all async versions of functions due to implementation complexity
[0.2.6] - 2025-05-19
Added
- Field.clearValidators(): Method to dynamically clear all validators from a field
[0.2.5] - 2025-05-17
Added
- CompareTo Validator: Cross-field validation for comparing values between fields
[0.2.4] - 2025-04-19
Added (dev helpers)
- Package visualizer for bundle analysis
- Enhanced IDE configuration for Vite
Fixed
- Updated repository URL in package configuration
[0.2.3] - 2025-04-18
Fixed
- Fixed undefined value comparisons in MinValue/MaxValue/ValueInRange validators
- Fixed ValidationErrorRenderContent reactivity issues
- Improved documentation static build process
[0.2.2] - 2025-04-17
Added
- Validator System: Built-in validators
- Required, Pattern, MinValue/MaxValue/ValueInRange validators
- MinLength/MaxLength/LengthInRange validators for text/array validation
- InAllowedValues validator for restricted value sets
- Async Operations: Support for asynchronous field operations and validation
- Eager Actions: Actions that execute immediately on registration and value changes
- Enhanced Action System:
- Refactored classIdentifier system for better performance
- Added boundToField method for action-field relationship tracking
- Conditional Logic: Complete implementation of conditional form behavior
- Statement and Operator system for complex conditions
- ConditionalVisibilityAction, ConditionalEnabledAction, ConditionalValueAction
- Extensive Testing: Comprehensive unit test coverage for all validators
- Documentation Examples: Interactive demos for validators and conditional statements
- Markdown Support: Configurable markdown rendering in validation error messages
Improved
- Enhanced type safety in Group field methods
- Fixed circular import issues
[0.1.5] - 2025-03-21
Added
- Export of FieldBase class to allow extending
[0.1.4] - 2025-03-19
Fixed
- DisplayMode export issues in type declarations
[0.1.3] - 2025-03-19
Improved
- Type Declarations: Enhanced TypeScript support without requiring Reactive<> wrappers
- Simplified type usage for end users
[0.1.2] - 2025-03-19
Added
- Forced Reactivity: Field and Action instances are now automatically reactive
- reactiveValue Property: Available on all field types for computed value access
- Enhanced Exports: Better export organization with default namespace and individual imports
Improved
- Reorganized module exports for better developer experience
- Enhanced reactivity system across all components
[0.1.1] - 2025-03-12
Changed
- Dependencies: Migrated from lodash to lodash-es for better tree shaking
[0.1.0] - 2025-03-06
Added
- Core Architecture: FieldBase abstraction with Field, Group, List, and Action implementations
- Reactive State Management: Built on Vue's reactivity system
- Event System: Action-driven architecture with event chains
- Validation Framework: Error tracking and validation state management
- Display Modes: SUPPRESS, HIDDEN, INVISIBLE, and FULL visibility control
- Form Relationships: Parent-child relationships with value change propagation
- Development Setup: Vite build system, Vitest testing, ESLint configuration
- TypeScript Support: Full generic type support for Field components
- Documentation: Complete VitePress documentation site with interactive examples
