List
List<R> manages a dynamic array of rows of type R. A row is any form element: a Group for a list of records, a Field for a list of plain values, another List for a list of lists. The list supports adding, removing and replacing rows, and uses the same action and validation system as Field and Group. It extends Container, which extends FieldBase. This page describes what List adds or overrides.
Creating a list
import { Field, Group, List, Validators } from '@dynamicforms/vue-forms';
// Define the item template
const itemTemplate = new Group({
name: new Field({ value: '' }),
score: new Field<number>({ value: 0 }),
});
// Empty list
const list = new List(itemTemplate);
// Pre-populated list
const list2 = new List(itemTemplate, {
value: [
{ name: 'Alice', score: 95 },
{ name: 'Bob', score: 80 },
],
});
// A list of plain values: every row is a Field, and the value is an array of strings
const tags = new List(new Field<string>({ validators: [new Validators.Required()] }), {
value: ['urgent', 'billing'],
});
tags.push('refund');
tags.value; // ['urgent', 'billing', 'refund']The type argument is the row type: new List(itemTemplate) infers it from the item template, so the two lists above are a List<Group<{ name: Field<string>; score: Field<number> }>> and a List<Field<string>>. A list declared without an item template is typed List<Group>; specify the row type where it holds something else:
const names = new List<Field<string>>();
names.push('Ada'); // a Field row
names.value; // ['Ada']Without an item template every row is built from its own item: a plain object becomes a Group of fields, an array a List, and anything else (a string, a number, null, a Date) a Field holding it.
new List(itemTemplate?, params?)
params is an IFieldParams<ListValueInput<R>, X>: the parameter type every element takes, with the list's value shape substituted. A list takes extended properties like every other element: augment Extras once and every list carries them, or declare them as the second type argument for one list, new List<Group<Fields>, Presentation>(template, { label: … }). Both are read through list.extra. Every row built from the item template is a binding of it, so the item template's members copy their extended properties into each row. length and items are read-only members declared by List, so a parameter of either name throws a TypeError, as valid and busy do. Give a presentation property with that meaning a different name. actions, errors, validators and visibility apply to the list itself as on every element; see Constructor parameters, which also describes the order in which the parameters are applied.
| Parameter | Type | Default |
|---|---|---|
|
| default |
Template bound to each new item's data: every row is | ||
| default | |
What the list sends to its own container, and the access applied to its rows through | ||
|
| default same as |
Baseline for | ||
|
| default |
Accepted, but without effect: | ||
|
| default |
Initial array of item values. When absent, | ||
Properties
| Property | Type | Writable |
|---|---|---|
|
| read-only |
Overrides | ||
|
| read-only |
The rows themselves; see The rows | ||
|
| read-only |
The number of rows the list holds. Reading it builds no array | ||
| reads | writable |
Overrides | ||
ListValue<R> is exported as (R['value'] | null)[], ListValueInput<R> as ListValue<R> | null and ListFullValue<R> as R['fullValue'][], each with R defaulting to Group.
Every mutation (push(), insert(), remove(), pop(), clear() and assigning value) is tracked by Vue, so a v-for over list.items or list.value re-renders without additional wiring.
The rows
items returns the rows in list order, and length is their number. Both reads are tracked, so a template rendering either re-renders when rows are added or removed:
<div v-for="(row, index) in lineItems.items" :key="index">
<input v-model="row.fields.description.value" />
<button @click="lineItems.remove(index)">Remove</button>
</div>
<p>{{ lineItems.length }} lines</p>The array items returns is a frozen copy of the rows the list held at the time of the read. Writes do not pass through it; the set changes only through push(), insert(), remove(), pop(), clear() and assigning value. A caller may keep the array. The rows in it are the live elements, so a row read from it returns its current state.
The copy is built once per change of the set and returned to every reader until the next change. A write inside a row changes what the list sends but not which rows it holds, so items returns the same array before and after such a write. get(index) returns a single row without building an array.
Scale
A List is designed to hold thousands of rows. The cost of an operation depends on what it touches, not on the length of the list:
| operation | cost |
|---|---|
reading length, or items again with the set of rows unchanged | constant |
reading value or valid again with nothing changed in between | constant (both are cached) |
| writing one field of one row | that row, plus the depth of the nesting it sits in |
push(), insert(), remove(), pop() | one row, plus native array operations over the row references: the shift of splice() and the copy a transaction keeps for a rollback |
reading an index or length of view(list) | constant |
reading value after a change | one array of the current length, plus a rebuild of the rows that changed |
assigning value, or validate(true) | the whole list (both apply to every row) |
The rows are held outside Vue's reactivity, so a splice() does not route the rows it shifts through a proxy. Every change of the set of rows increments a tracked counter, so length, get(), items and a template that reads them re-run on every such change.
The caches are invalidated by the write itself, so no manual refresh is needed. value is rebuilt on the first read after a change and reused until the next change, so two consecutive reads return the same object. That object is frozen, rows included: writing into it throws in strict mode and is ignored otherwise, so assign a new value instead. originalValue is a separate copy and is not frozen. The freeze covers only the objects the containers build. An array or object that a Field holds as its value is not frozen: writing into it changes the field's value without a ValueChangedAction, so assign a new array or object instead.
An assignment reuses the existing row objects, position by position, so list.get(0) returns the same instance after list.value = rows when the new array has the same length. A keyed v-for over the rows therefore does not remount them on every assignment. Only a row the list built from its item template is reused; a row that was passed to push() or insert() as an element is replaced by a new row, so it keeps the data of the members the item template does not have. A reused row is reset to the state of a new row built for that position: a member whose key is absent from the new item gets the item template member's originalValue; originalValue, isChanged, touched and errors are reset; the row and its members take the access and visibility of the item template, and their conditional actions apply their statements again over the new item. Extended properties stay as they are. The new set is built separately and installed as a whole, so a validator that reads list.value during the assignment never sees an unfilled position.
Methods
Every method that adds or removes rows (push(), insert(), remove(), pop(), clear(), rebind()) changes the list's value: the list and every container above it remove their errors whose rule removes them on 'change' (ErrorPolicy).
bind(data?, overrides?): List<R, X>
Overrides FieldBase.bind(), which describes overrides (an IBindParams<ListValueInput<R>, X>), the construction through this.constructor and the detached result. Returns a new List over data, with a binding of the item template, the actions and the extended properties. Binding an empty list returns an empty list. Without data, the new list gets what this one holds (its fullValue, every row regardless of access), not what it sends. bind(null) returns an empty list.
A subclass whose constructor does not take (itemTemplate, params) would ignore the bound item template and produce a list with the declaration's rows; bind() throws a TypeError in that case. Such a subclass must override bind() and construct itself.
clear(): void
Removes all items and triggers a value-changed notification. Every row is released, exactly as remove() releases the row it removes. It does not change the list's own touched, errors or originalValue; for a reset that revalidates, see Clearing and resetting and use rebind(null) instead.
get(index): R | undefined
Returns the row at index, or undefined if out of range.
insert(item, index): number
Inserts item at index and returns its resulting position. A negative index counts back from the end and is clamped at the front, as in Array.prototype.splice: on a three-item list -1 inserts before the last item and returns 2, and -100 inserts at the front and returns 0. A non-negative index is the position itself, so the return value equals the argument. An index that is not an integer (a fraction, NaN, Infinity) throws a RangeError. If index is beyond the current length, the gap is filled with bindings of the item template, which hold the item template's own values, not empty ones. Without an item template the padding items are empty elements of the kind item is built into: empty groups for a plain object, empty lists for an array, fields holding undefined for anything else.
ListItemAddedAction fires once per item added to the list: once for each padding item, with that item's index, and finally for item at its position. That position is the value insert() returns, so a negative index is reported resolved.
pop(): R | undefined
Removes the last item and returns it (undefined if the list is empty). Triggers ListItemRemovedAction.
push(item): number
Appends an item to the end of the list. item is either the data a row is built from (bound to the item template, or built into an element by its type where the list has no item template) or an existing element, which becomes the row itself. Returns the new length of the list. Triggers ListItemAddedAction with the index the item was appended at.
An existing element that already belongs to a container throws a TypeError, as does the list itself or a container above it. The same applies to insert().
item is typed ListItemInput<R> (R['value'] | R). The row type R defaults to Group, so a list without an item template that holds rows of other kinds is declared with its row type: new List<FieldBase>().
list.push({ name: 'Charlie', score: 70 });rebind(data): this
Overrides FieldBase.rebind(), which has the full description, to take a ListValueInput<R>: an array of rows, or null, which empties the list. Existing rows are reused by position, as in a value assignment. list.originalValue holds what the list sends, so a row that was 'disabled' when the baseline was recorded is not in it, and list.rebind(list.originalValue) does not restore that row. list.rebind(null) releases every row as clear() does, and also resets the list's touched, clears its own errors and revalidates.
remove(index): R | undefined
Removes the item at index and returns it: the row instance itself, which list.get(index) returned before the call and which is passed to ListItemRemovedAction. Triggers ListItemRemovedAction. An index outside the list, negative included, removes nothing and returns undefined; an index that is not an integer (a fraction, NaN, Infinity) throws a RangeError.
The removed row is released: its parent is cleared, it no longer counts towards the list's validity, and it can be pushed into another list or back into this one. It keeps its state: a row edited before removal has isChangedtrue, keeps the errors its validators produced, and its originalValue is the data it was bound to.
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 |
NullableList
Type alias for List | null.
See also: FieldBase, Container, The model for how a row is built, Bindings and records for what a record is, Actions reference for
ListItemAddedActionandListItemRemovedAction
