Skip to content

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 ​

typescript
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.

ParameterTypeDefault

fields

GenericFieldsInterface (Record<string, FieldBase>)

required

Map of member name → element instance

params.access

Access

default 'editable'

What the group sends to its own container, and the access applied to its members through effectiveAccess. See What a container serializes.

params.originalValue

GroupValueInput<T>

default same as value

Baseline for isChanged. When value is absent or explicitly undefined, it is also applied to the members as their initial value. When value is given, each member it contains takes its part as its originalValue, through every nested group; a List member takes its part as its own originalValue, and its rows keep what they hold

params.touched

boolean

default false

Initial interaction flag, propagated to every child

params.value

GroupValueInput<T> (Partial<FieldsToValues<T>> | null)

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 undefined value assigns nothing, so every member keeps the value it was created with; an explicit null clears all of them. When a value is applied, every element below the group records what it holds at the end of the construction as its originalValue, so the members start with isChanged false

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 ​

TypeDefinitionPurpose
GenericFieldsInterfaceRecord<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>> | nullWhat 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).

typescript
const form = Group.createFromFormData({ name: 'Alice', score: 42 });

Properties ​

PropertyTypeWritable

fields

T

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 TypeError; addField() and removeField() change the set. The read is tracked, so a template rendering it re-renders when members are added or removed

fullValue

FieldsToFullValues<T>

read-only

Overrides FieldBase.fullValue: what the group holds (value is what it sends): every member's fullValue, regardless of access. A nested group includes its own full structure. A binding of the group copies this value

value

reads GroupValue<T>, accepts GroupValueInput<T>

writable

Overrides FieldBase.value: object of the values the members send, by the serialization rule below; {} when no member sends anything (a group without members, or one whose members the rule all leaves out). The group's value is never null. Each key has its member's own value type, optional and nullable: for Group<{ age: Field<number> }>, group.value.age is number | null | undefined, because a 'disabled' age is left out and a 'disabled-null' one is null. The object is built once per change and returned to every reader until the next change. It is frozen: writing into it throws in strict mode and is ignored otherwise. An array or object a member Field holds is not frozen; assign a new one instead of writing into it. The setter takes a Partial: absent keys are not changed, and assigning null sets every member to null

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.

typescript
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 TypeError where field already belongs to a container; pass a bind() of it instead.
  • throws TypeError where field is the group itself or a container above it.
  • throws TypeError where the group has a view and fieldName is 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.

typescript
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.

typescript
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
MemberDescription
busytrue 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
touchedtrue where any child is touched; assigning it assigns every child
validtrue 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
MemberDescription
accessWhether 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
contributionWhat the element sends to its container's value
declarationThe element this one was declared as: itself, or the element a binding was made from
effectiveAccessThe access that applies once the containers above are taken into account
effectiveEnabledtrue where effectiveAccess is 'editable'
enabledtrue where access is 'editable'
errorsCurrent validation errors of the element; writable
extraThe extended properties the element holds, frozen; written through setExtendedValues()
fieldNameKey name within the parent Group
isChangedtrue 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
originalValueBaseline for isChanged; writable
parentContainer the element belongs to
pendingvalidating || 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
validatingtrue while an asynchronous validation is in flight on the element or below it
visibilityHow 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

Released under the MIT License.