Container
Container is the abstract base of Group and List: a form element that holds other elements and composes its own state out of theirs. It is also the type of every element's parent.
import { Container, Field, Group } from '@dynamicforms/vue-forms';
const name = new Field({ value: 'Ada' });
const form = new Group({ name });
form instanceof Container; // true
name.parent === form; // true; name.parent is typed Container | undefinedGroup holds named members and List holds rows by position. This page describes what the two share. Child access (group.fields.name, list.get(0)) is specific to each class and is described on its page.
Properties
| Property | Type | Writable |
|---|---|---|
|
| read-only |
Overrides | ||
|
| writable |
Overrides | ||
|
| read-only |
Overrides | ||
Methods
confirm(params?): Promise<any> | undefined
Executes the action that confirms this container, with params, and returns what its execute() returns. A rendering layer calls it for a confirm gesture, such as Enter in a dialog. The action is looked up in three steps; the first step that finds a candidate decides:
- an action at or below the container with a
SubmitActionwhose target is this container; - an action with
defaultConfirmamong the container's direct members; - an action with
defaultConfirmat a lower level, such as in a bar of actions.
Only actions whose visibility is 'full' are candidates, so a form can hold a "Create" and a "Save" action and show one of them. One candidate is executed if it is executable; where it is not, nothing is executed and the later steps are not searched. Two or more candidates in one step are ambiguous: nothing is executed and a warning is logged. Returns undefined where nothing is executed.
onKeydown('Enter', () => form.confirm());notifyValueChanged(): void
Records that a child changed what it holds or sends, so that the open transaction computes at commit what the container holds and sends, announces each once, fires ValueChangedAction where the value differs from the value last announced, and recomputes validity. The mutation methods call it when a child's value changes, so a direct call is rarely needed.
reject(params?): Promise<any> | undefined
Executes the action that rejects this container, looked up as in confirm() with RejectAction and defaultReject. A rendering layer calls it for a reject gesture, such as Escape in a dialog.
validate(revalidate?): void
Overrides FieldBase.validate(). With revalidate: true, every child is revalidated first and the container then computes its own validity over the result, so it announces at most one net transition of its validity. A child that becomes valid while a later child is still unchecked produces no notification on the container.
Inherited members
value, fullValue, bind() and the other inherited members are typed with the container's value shape. value and bind() are abstract here and implemented by Group and List.
Inherited from FieldBase
| Member | Description |
|---|---|
access | Whether the element accepts input, what it sends to its container, and whether it is validated |
bind(data?, overrides?) | Returns a new element of the same class over data, with the same actions and extended properties |
bindingsOf(declaration) | Returns every element in the subtree whose declaration is the one given |
clearValidators() | Removes the element's validators and empties errors |
contribution | What the element sends to its container's value |
declaration | The element this one was declared as: itself, or the element a binding was made from |
effectiveAccess | The access that applies once the containers above are taken into account |
effectiveEnabled | true where effectiveAccess is 'editable' |
enabled | true where access is 'editable' |
errors | Current validation errors of the element; writable |
extra | The extended properties the element holds, frozen; written through setExtendedValues() |
fieldName | Key name within the parent Group |
fullValue | What the element holds; identical to value except on Group and List |
isChanged | true when value differs from originalValue (deep equality) |
markRecordIncomplete() | Marks that an eager action did not find a second element of a record not yet assembled |
originalValue | Baseline for isChanged; writable |
parent | Container the element belongs to |
pending | validating || busy |
rebind(data) | Replaces the data the element holds, in place |
registerAction(action) | Registers an action on the element; returns this |
registerActionBefore(action, before) | Registers action inside an existing chain, wrapped by before |
setExtendedValues(values) | Merges extended properties into extra |
settled() | Resolves once pending is false |
triggerAction(actionClass, ...params) | Fires an action class on the element and returns what the chain returns |
unregisterAction(action) | Removes an action from the element's declaration and its bindings |
validating | true while an asynchronous validation is in flight on the element or below it |
value | Current value; writable |
visibility | How a rendering layer shows the element; writable |
What a container serializes
TIP
The Cookbook applies these rules to common cases: fields that depend on a type, an optional section, loading a record, submitting.
A container has two values. value is what the form sends: the payload a save passes to the server. fullValue is what the form holds: every child's own fullValue, regardless of access. A child's access determines what it sends to value and whether it is validated.
access | Accepts input | Sends to value | Validated |
|---|---|---|---|
'editable' | yes | its value | over its value |
'readonly' | no | its value | over its value |
'disabled' | no | nothing: the key, or the row, is left out | no |
'disabled-null' | no | null in its place | over null |
The values follow HTML: a readonly input is submitted with its value and a disabled one is left out.
An Action sends nothing to value or fullValue, whatever its access, and is not validated. A container whose children are all actions, such as a bar of buttons, sends nothing either. A container without children sends {} or [].
A container's access applies to everything inside it, and effectiveAccess holds the result on each element. A container that is 'disabled' or 'disabled-null' sends none of its children, regardless of what they hold, so every element below it has effectiveAccess 'disabled': none of them is validated, and none carries an error from a validator. Below a 'readonly' container an 'editable' element is 'readonly'. Elsewhere an element's own access applies. Each child keeps the access it was given.
The rule is implemented in FieldBase.serializesAs(purpose), and every container composes value by calling it on each child. It applies equally to the members of a Group and the rows of a List. The method is protected: a subclass of an element that sends by a different rule overrides it, and every container holding that element uses the override.
An element keeps its value regardless of its access. Making it 'editable' again puts its value back into the container's value, and bind() copies it into a binding, so a rule that changes what is sent never discards entered data. visibility has no effect here: it only controls how a rendering layer draws the element.
Validation follows what is sent
An element's validators run over what it sends (its value, or null for 'disabled-null'), and only when it is sent. A container counts the validity of every child that sends something, null included. A 'disabled' child is not counted, so an error written into it directly (for example one returned by the server) stays on the child and does not make the container invalid. A Required on a section sent as null therefore fails, while the required fields inside that section are not checked until the section is sent again. A disabled section leaves no error and does not block a submit button. A change of access reruns the validators on the element and on every element below it whose effectiveAccess changed what it sends; a change between 'editable' and 'readonly' changes what an element accepts, not what it sends, and reruns none.
Where null comes from
null appears in a container's value in exactly two cases: a child whose own value is null, or a child that is 'disabled-null'. A container's own value is never null. Where no child sends anything (every member disabled, a list without rows), a Group reads {} and a List reads []. Assigning null to a container empties it: a Group writes null into every member, a List releases every row.
| Meaning | Declare it as |
|---|---|
| a value | a child with that value, 'editable' or 'readonly' |
| "this holds nothing": the server clears it | a child with value null, or a child that is 'disabled-null' |
| "this is not part of the form": the server leaves it unchanged | a child that is 'disabled' |
The two ways to send null differ in what the form keeps. value = null empties the child and discards its previous value. 'disabled-null' sends null while the child keeps its values. This suits a section switched off by a toggle: switching it back on restores what was entered.
const billing = new Group({ street: new Field({ value: '' }), city: new Field({ value: '' }) });
const form = new Group({ customer: new Field({ value: 'Ada' }), billing });
billing.access = 'disabled-null'; // billed to the delivery address
form.value; // { customer: 'Ada', billing: null }
billing.access = 'editable'; // the address holds what was typed into itThese rules do not apply to writes: an assignment is applied to the element regardless of its access, and value is composed from what the elements hold when it is read. The order in which a form's rules change access therefore never loses data.
Loading a record into the form is a plain assignment and does not change access: form.value = record writes every member, disabled ones included, and { billing: null } empties the billing address. What is sent is determined by the form's rules (a type field, a toggle), not by the data.
Reading the values
value is for sending: every key is optional, because a 'disabled' member is left out, and nullable, because a 'disabled-null' one is null. Pass it to the server unchanged.
fullValue is for reading what the form holds, including every member regardless of access, and its keys have the members' own types:
form.value.billing; // null while the billing address is 'disabled-null'
form.fullValue.billing; // what the address holds, sent or notWhat a change announces
A container announces two things, and a change can affect either one without the other. ValueChangedAction announces what the container holds (its fullValue), and ContributionChangedAction announces what it sends to its own container (its value where its access sends it, null, or undefined for nothing). A write into a 'disabled' field changes what the form holds and not what it sends; a change of access changes what it sends and not what it holds.
A container that follows its children
A container is not disabled automatically when its children are. Where a form needs that, one effect sets it:
import { watchEffect } from 'vue';
// left out of the parent's value while no member is enabled
watchEffect(() => {
address.access = Object.values(address.fields).some((field) => field.enabled) ? 'editable' : 'disabled';
});
// sent as null while no member is enabled
watchEffect(() => {
address.access = Object.values(address.fields).some((field) => field.enabled) ? 'editable' : 'disabled-null';
});The first leaves the key out of the parent's value, the second sends it as null, matching the two rows of the table above.
parent
Every element's parent is typed Container | undefined: a Group holds its members and a List its rows, and the type does not specify which of the two holds the element. Container declares no child accessors, so a sibling lookup must name the container type it expects:
// checked: the branch runs only where the parent is a Group
if (field.parent instanceof Group) field.parent.fields.other.validate(true);
// cast: where the structure guarantees the parent is a Group
(field.parent as Group | undefined)?.fields.other.validate(true);A cast is not checked at runtime: where the parent is a List, fields reads undefined and the lookup after it throws. Passing the sibling's name to CompareTo, which resolves it, needs neither.
