useModelGroup
Creates a collection of component models.
TIP
Before proceeding with useModelGroup, learn first about the useModel composable.
Usage
<script setup>
import { useModelGroup } from '@8ctavio/vergil'
const modelGroup = useModelGroup({
username: { value: '' },
password: { value: '' }
})
</script>
<template>
<InputText v-model="modelGroup.username"/>
<InputText v-model="modelGroup.password" type="password"/>
</template>Description
The useModelGroup composable creates an object of component models that allows to perform bulk operations with them; it accepts two parameters: fields and validator.
The fields parameter expects an object whose own enumerable string-keyed properties are mapped into the useModelGroup's created object as model objects with the same property names. The fields property values must be model specification objects which are internally used to create corresponding models with the useModel composable; a model specification object's value property and the rest of the properties are used, respectively, as the useModel's value and options parameters. Therefore, in general, an specification object of the form { value, ...options } is transformed into the model useModel(value, options).
useModelGroup({
model: { value, ...options } // ↦ useModel(value, options)
})The validator parameter is a function, similar to that of a model, but here used to validate a model group's payload (see Model group validation).
Nested model groups
A model group may be nested into another model group. The useModelGroup's fields parameter, however, does not directly accept useModelGroup-returned objects. In order to nest a model group, a fields property value must be an object returned by the useModelGroup.nested method. This method's parameters are the same as the useModelGroup's, but returns an special object internally handled and used to create a corresponding model group object.
const group = useModelGroup({
foo: { /* ... */ },
bar: useModelGroup.nested({
baz: { /* ... */ },
})
})
console.log(isModel(group.foo)) // true
console.log(isModelGroup(group.bar)) // true
console.log(isModel(group.bar.baz)) // trueModel group validation
The useModelGroup composable accepts as a second argument a validator function to validate a model group's payload, and which is invoked by the validate method after all group descendants are validated.
An useModelGroup's validator function accepts three parameters:
payload: The model group'spayloadto validate.error: A function to register validation errors; it receives two arguments:pathandmessage, wherepathis the model group's dot-notation path string to the nested model on which to register the error, andmessageis the error message string.isValid: A function to determine if a nested model has collected any errors up to that moment. It receives as a single argument a model group's dot-notation path string to the model to inspect.
To illustrate, consider the example below. If testFoo returns true, the group.foo model will have a new error registered. On the other hand, the group.bar.baz model is further validated only if it has had no errors registered (e.g., in the model's validator).
const group = useModelGroup({
foo: { /* ... */ },
bar: useModelGroup.nested({
baz: { /* ... */ },
})
}, groupValidator)
function groupValidator(payload, error, isValid) {
if (testFoo(payload.foo)) {
error('foo', 'Error Foo')
}
if (isValid('bar.baz') && testBaz(payload.bar.baz)) {
error('bar.baz', 'Error Bar-Baz')
}
}WARNING
Model group's nested model values should not be mutated inside that model group's validator.
modelGroup.reset
The reset method recursively resets all model group's models.
modelGroup.getPayload
The getPayload method creates and returns a model group's payload, which is an object of the same structure as the model group's, but whose own property values store either the model-values of the corresponding model group's models, or the payloads of the corresponding nested model groups.
const group = useModelGroup({
foo: { value: 1 },
bar: useModelGroup.nested({
baz: { value: 2 },
})
})
console.log(group.getPayload())
// {
// foo: 1,
// bar: { baz: 2 }
// }modelGroup.validate
The validate method recursively validates all model group's models. If the model group was created with a validator function, it is executed after descendants are validated.
By default, validate returns a boolean indicating whether all model group's descendant models are valid, i.e., none have any errors.
In addition, validate accepts an includePayload boolean parameter that defaults to false. When includePayload is set to true, a two-element array is returned instead, where the first element is the normally returned, validation result boolean, and the second element is the model group's payload.
const isValid = modelGroup.validate()
// or...
const [isValid, payload] = modelGroup.validate(true)modelGroup.clear
The clear method recursively clears errors of all model group's models.
modelGroup.error
The error property is a trackable, readonly accessor property that returns a boolean indicating whether any model group's nested model has errors.
modelGroup.forErrors
The forErrors method allows to recursively iterate over all model errors in a model group. It receives as its first argument a callback function executed for each nested model with errors. The callback receives four arguments:
errors: the current model's errors array,field: the property name the current model is stored at in its owner model group object,path: a dot-separated property name list that represents the current model's location or path within its root model group object, andmodel: the current model object.
Additionally, forErrors accepts as a second argument a filter function to exclude models from the iteration. This function is called for both model and model group objects, and receives four arguments:
actions: An object whose properties should be returned byfilterto handle how to filter the current model or model group; available properties areSKIP,ACCEPT,ACCEPT_CHILDREN, andACCEPT_DESCENDANTS(detailed below),path: the current model or model group's path from its root model group object as a dot-separated property name list,isGroup: a boolean indicating whether the value at the given path is a model group object, andvalue: the current model or model group object.
The type of object passed to, and the value returned by the filter function determine how a model or model group is filtered. If a model is received, either actions.SKIP or actions.ACCEPT should be returned to respectively skip or include a model in the forErrors iteration. On the other hand, when filter is passed a model group object, multiple descendant models may be affected by the returned actions property:
SKIP: All model group's descendant models are excluded from the iteration (this implies that no model group's nested object is further passed tofilter).ACCEPT: All model group's child objects continue to be passed tofilter.ACCEPT_CHILDREN: All model group's child models are included in the iteration (only child model groups continue to be passed tofilter).ACCEPT_DESCENDANTS: All model group's descendant models are included in the iteration (no model group's descendant object is further passed tofilter).
TIP
When forErrors is called, errors refs of models included in the iteration can be tracked by effects.
Definition
function useModelGroup(
fields: ModelGroupFields,
validator?: ModelGroupValidator
): ModelGroup
function useModelGroup.nested(
fields: ModelGroupFields,
validator?: ModelGroupValidator
): ModelGroupSpec
type ModelGroupFields = {
[Key: string]: ModelSpec | (ModelGroupFields & ModelGroupInternal)
}
type ModelGroupValidator = (
payload: Record<string, unknown>,
error: (path: string, msg: string) => void,
isValid: (path: string) => boolean
) => void
type ModelGroup<F extends ModelGroupFields> = {
reset(): void
getPayload(): ModelGroupPayload
validate(includePayload?: boolean): boolean | [boolean, ModelGroupPayload]
clear(): void
get error(): boolean
forErrors(
callback: (
errors: string[],
field: string,
path: string,
model: Model
) => void,
filter: (
actions: FilterActions,
path: string,
isGroup: boolean,
value: Model | ModelGroup
) => FilterActions[keyof FilterActions]
): void
} & {
[K in keyof F]: Model | ModelGroup;
}
type ModelGroupPayload = Record<string, unknown>
interface ModelSpec<T> extends ModelOptions<any> {
value: T;
formLabel?: string;
}
type ModelGroupSpec<F extends ModelGroupFields> = F & ModelGroupInternal
type ModelGroupInternal = {
readonly __modelGroup: true;
readonly __validator: ModelGroupValidator;
}
type FilterActions = {
SKIP: false;
ACCEPT: true;
ACCEPT_CHILDREN: 1;
ACCEPT_DESCENDANTS: 2;
}