Group
Group<T> holds a named set of elements (fields, nested groups or lists) and exposes their combined value as a plain object. It extends Container, which extends FieldBase, and uses the same action and validation system as Field. This page describes what Group adds or overrides.
Creating a group
import { Field, Group } from '@dynamicforms/vue-forms';
const form = new Group({
firstName: new Field({ value: 'John' }),
lastName: new Field({ value: 'Doe' }),
age: new Field<number>({ value: 30 }),
});new Group(fields, params?)
params is an IFieldParams<GroupValueInput<T>, X>: the parameter type every element takes, with the group's value shape substituted. A group takes extended properties like every other element: augment Extras once and every group carries them, or declare them as the second type argument for one group, new Group<Fields, Presentation>(fields, { label: … }). Both are read through group.extra. actions, errors, validators and visibility apply to the group itself as on every element; see Constructor parameters, which also describes the order in which the parameters are applied.
| Parameter | Type | Default |
|---|---|---|
|
| required |
Map of member name → element instance | ||
| default | |
What the group sends to its own container, and the access applied to its members through | ||
|
| default same as |
Baseline for | ||
|
| default |
Initial interaction flag, propagated to every child | ||
|
| default not assigned |
Initial values applied to matching members; members whose key is absent keep the value they were created with. An absent or explicitly | ||
The constructor throws if fields is not an object of element instances (Invalid fields object provided). It also throws a TypeError when it receives an element that already belongs to another group or list: an element belongs to one container at a time. Each group needs its own element instances; create one with field.bind(). A List releases the rows it removes and a Group releases the element removeField() removes, so both can be added to a container again.
Member names are ordinary keys of the fields map, so names that collide with Object.prototype members (toString, constructor, __proto__, …) are accepted: the map has no prototype, and value and fullValue build their result the same way. The value setter assigns only from the object's own keys, so group.value = {} leaves a member named toString unchanged and does not assign Object.prototype.toString to it. addField() throws Error('Field <name> is already in this form') for a name the group already holds.
fields returns a guarded view of the member map, so the set of members cannot be changed through it: group.fields.name = otherField, Object.defineProperty(group.fields, 'name', …) and delete group.fields.name all throw a TypeError naming the method to use instead. An element assigned that way would get no parent, fieldName or change notifications, and the group would keep counting the validity of a member it no longer holds. The set changes only through addField() and removeField(). Reading through the view is a tracked read of the set of members, so a template rendering group.fields re-renders when a member is added or removed.
The group writes parent and fieldName of each member; see parent. A Group that is a row of a List gets the List as its parent. Two groups compare by identity; see Comparing elements.
Types
| Type | Definition | Purpose |
|---|---|---|
GenericFieldsInterface | Record<string, FieldBase> | The constraint on Group's and List's type argument. Extend it to declare a form's shape: interface UserForm extends GenericFieldsInterface { name: Field<string> } |
FieldsToValues<T> | { [K in DataKeys<T>]: T[K]['value'] | null } | Maps a fields interface to the value object it sends. DataKeys<T> (not exported) is the keys of T whose member is not an Action, so an action member has no key. A nested Group sends its own value object, a nested List its row array; every member may be null, because a 'disabled-null' member is sent as null |
GroupValue<T> | Partial<FieldsToValues<T>> | The type of group.value. The group's value is never null. Every key is optional: a 'disabled' member is left out of the object the group builds, so each one is possibly undefined |
GroupValueInput<T> | Partial<FieldsToValues<T>> | null | What group.value and params.value accept; null writes null into every member |
FieldsToFullValues<T> | { [K in DataKeys<T>]: T[K]['fullValue'] } | The type of group.fullValue: every member's fullValue, regardless of access; an Action member has no key |
Group.createFromFormData(data)
Creates a Group from a plain Record<string, any> so that its fullValue equals the data: a plain object becomes a Group, an array a List without an item template (whose rows are built by the same rule), and any other value a Field, at every level. Useful when building a form from raw API data. Passing null returns an empty group; passing an already built form structure throws (data is already a Form structure, should be a simple object).
const form = Group.createFromFormData({ name: 'Alice', score: 42 });Properties
| Property | Type | Writable |
|---|---|---|
|
| read-only |
The typed map of members. It is a guarded view over the group's map: reads return the members themselves, and every write throws a | ||
|
| read-only |
Overrides | ||
| reads | writable |
Overrides | ||
Serialization rule
Group.value includes every member according to its access: an 'editable' or 'readonly' member with its value, a 'disabled-null' one as null, and a 'disabled' one not at all. A nested container is a member like any other, so a 'disabled' one is left out regardless of what it holds. See What a container serializes for the full rules.
Methods
addField(fieldName, field): this
Adds field to the group under fieldName and returns the group. The group then holds it exactly as it holds a member passed to the constructor: the element has parent and fieldName set, its validity counts towards the group's, its pending runs count towards the group's validating, and its own validators that refer to another member of the form (a CompareTo by name, a validator that reads a sibling) run over the record it now belongs to.
const form = new Group({ name: new Field({ value: 'Jan' }) });
form.addField('email', new Field({ value: 'jan@example.com' }));
form.value; // { name: 'Jan', email: 'jan@example.com' }The change is announced like any other: inside a transaction it is committed with the rest of the transaction, and the group announces its final value once. A member that sends a value fires ValueChangedAction on the group; a 'disabled' one, which the group leaves out, fires nothing. The baseline of isChanged is not changed: a group that gains a member holds a value its originalValue does not contain, and isChanged is true until originalValue is assigned. The group's own errors whose rule removes them on 'change' (ErrorPolicy) are removed, also where the added member is 'disabled'.
The map is typed T, which lists the members declared in the group's type. A member added beyond those is held and sent like any other, but is not part of the type. Declare it in T to have it typed.
- throws
Error(Field <name> is already in this form) where the group already holds that name; - throws
TypeErrorwherefieldalready belongs to a container; pass abind()of it instead. - throws
TypeErrorwherefieldis the group itself or a container above it. - throws
TypeErrorwhere the group has a view andfieldNameis a name a view reserves ($,then, a name that starts with__v_).
bind(data?, overrides?): Group<T, X>
Overrides FieldBase.bind(), which describes overrides, the construction through this.constructor and the detached result. Returns a new Group over data: every member is bound in turn, and the actions and extended properties of the group and of each member are copied. data is applied to the members as a value passed to the constructor; a member whose key is absent gets the originalValue of the declared member, as in rebind(). Once the group is built, every element below it records what it holds as its originalValue, so isChanged is false on the group and on every member, row and row member. overrides is an IBindParams<GroupValueInput<T>, X>; its access and visibility apply to the group itself, and each member keeps its own. Its originalValue is the baseline of the group and of each member it contains, as params.originalValue next to value. bind(null) returns a group with every member cleared.
A subclass whose constructor does not take (fields, params) would ignore the bound members and produce a group with the declaration's data instead of the record's; bind() throws a TypeError in that case. Such a subclass must override bind() and construct itself.
field(fieldName): T[K] | null
Type-safe accessor for a single member. Returns null if the key does not exist.
const first = form.field('firstName'); // typed as Field<string>rebind(data): this
Overrides FieldBase.rebind(), which has the full description, to take a GroupValueInput<T> record. The record need not name every member: a member whose key is absent from data takes the originalValue of the corresponding member of the group's declaration, and for a group that is its own declaration, its own baseline. It never takes the previous record's value, so a row reused this way ends up in the same state as a new bind() of the item template. See Clearing and resetting for rebind(group.originalValue) (reset) and rebind(null) (empty). group.value = null also empties the group, but only rebind resets touched, clears the group's own errors and revalidates. group.value = {} is not a reset: the setter writes the keys the object names and leaves the other members as they are.
Emptying a group writes null into every member below it, whatever the member's T allows: a Field<string> inside an emptied group holds null at runtime while its type is string. A group that is emptied this way declares its fields as T | null; a group with stricter types is reset member by member.
removeField(fieldName): FieldBase | undefined
Removes the member held under fieldName from the group and returns it, or returns undefined where the group holds no member of that name. The element is fully released: parent and fieldName are cleared, its validity and its pending runs no longer count towards the group's, and it can be added to another container. It keeps its own state: its value, its errors and the baseline of isChanged.
const email = form.removeField('email'); // the Field instance, detached
form.value; // { name: 'Jan' }The group's value and validity are recomputed over the remaining members and announced the same way as for addField(), and the group's own errors whose rule removes them on 'change' are removed. The baseline of isChanged is not changed. When the transaction it ran in is rolled back, the member set is restored.
Inherited members
Inherited from Container
| Member | Description |
|---|---|
busy | true while an Action.execute() in any child has not settled |
confirm(params?) | Executes the action that confirms the container: its SubmitAction, or the only shown defaultConfirm action |
notifyValueChanged() | Records that a child changed its value, for the open transaction to announce |
reject(params?) | Executes the action that rejects the container: its RejectAction, or the only shown defaultReject action |
touched | true where any child is touched; assigning it assigns every child |
valid | true where the container's own errors are empty and every child it counts is valid |
validate(revalidate?) | Revalidates every child first with revalidate: true, then computes the container's validity |
Inherited from FieldBase
| Member | Description |
|---|---|
access | Whether the element accepts input, what it sends to its container, and whether it is validated |
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 |
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 |
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 |
visibility | How a rendering layer shows the element; writable |
NullableGroup
Type alias for Group | null.
See also: The model, FieldBase, Container, Basic Form example, Conditional statements example
