DEV: more reactive field data for form-kit (#29819)

A lot of the data of fields is decided at insertion time and is not dynamic afterwards, this commit attempts to solve this problem by making the fk-field-data a component with getters on the all the properties we need. It allows for example to implement a dynamic @disabled without having to pass @disabled everywhere. Generally speaking this solution limits props-drilling.

@format has received the same treatment than @disabled.
This commit is contained in:
Joffrey JAFFEUX 2024-11-28 14:30:36 +01:00 committed by GitHub
parent 2137f2bb74
commit 89ec7d8b98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 414 additions and 400 deletions

View File

@ -1,6 +1,6 @@
import Component from "@glimmer/component";
import { concat } from "@ember/helper";
import { action } from "@ember/object";
import { concat, fn } from "@ember/helper";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { eq } from "truth-helpers";
import FKLabel from "discourse/form-kit/components/fk/label";
import FKMeta from "discourse/form-kit/components/fk/meta";
@ -9,12 +9,6 @@ import concatClass from "discourse/helpers/concat-class";
import { i18n } from "discourse-i18n";
export default class FKControlWrapper extends Component {
constructor() {
super(...arguments);
this.args.field.setType(this.controlType);
}
get controlType() {
if (this.args.component.controlType === "input") {
return this.args.component.controlType + "-" + (this.args.type || "text");
@ -23,11 +17,6 @@ export default class FKControlWrapper extends Component {
return this.args.component.controlType;
}
@action
setFieldType() {
this.args.field.type = this.controlType;
}
get error() {
return (this.args.errors ?? {})[this.args.field.name];
}
@ -44,12 +33,13 @@ export default class FKControlWrapper extends Component {
"form-kit__field"
(concat "form-kit__field-" this.controlType)
(if this.error "has-error")
(if @disabled "is-disabled")
(if (eq @format "full") "--full")
(if @field.disabled "is-disabled")
(if (eq @field.format "full") "--full")
}}
data-disabled={{@disabled}}
data-disabled={{@field.disabled}}
data-name={{@field.name}}
data-control-type={{this.controlType}}
{{willDestroy (fn @unregisterField @field.name)}}
>
{{#if @field.showTitle}}
<FKLabel class="form-kit__container-title" @fieldId={{@field.id}}>
@ -72,7 +62,7 @@ export default class FKControlWrapper extends Component {
<div
class={{concatClass
"form-kit__container-content"
(if @format (concat "--" @format))
(if @field.format (concat "--" @field.format))
}}
>
<@component
@ -86,7 +76,6 @@ export default class FKControlWrapper extends Component {
@after={{@after}}
@height={{@height}}
@selection={{@selection}}
@disabled={{@disabled}}
id={{@field.id}}
name={{@field.name}}
aria-invalid={{if this.error "true"}}
@ -97,12 +86,7 @@ export default class FKControlWrapper extends Component {
{{yield components}}
</@component>
<FKMeta
@disabled={{@disabled}}
@value={{@value}}
@field={{@field}}
@error={{this.error}}
/>
<FKMeta @field={{@field}} @error={{this.error}} />
</div>
</div>
</template>

View File

@ -9,21 +9,23 @@ export default class FKControlCheckbox extends Component {
@action
handleInput() {
this.args.field.set(!this.args.value);
this.args.field.set(!this.args.field.value);
}
<template>
<FKLabel class="form-kit__control-checkbox-label">
<input
type="checkbox"
checked={{eq @value true}}
checked={{eq @field.value true}}
class="form-kit__control-checkbox"
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
{{on "change" this.handleInput}}
/>
<span class="form-kit__control-checkbox-content">
<span class="form-kit__control-checkbox-title">{{@field.title}}</span>
<span class="form-kit__control-checkbox-title">
{{@field.title}}
</span>
{{#if (has-block)}}
<span class="form-kit__control-checkbox-description">{{yield}}</span>
{{/if}}

View File

@ -7,7 +7,7 @@ import { escapeExpression } from "discourse/lib/utilities";
export default class FKControlCode extends Component {
static controlType = "code";
initialValue = this.args.value || "";
initialValue = this.args.field.value || "";
@action
handleInput(content) {
@ -27,7 +27,7 @@ export default class FKControlCode extends Component {
@content={{this.initialValue}}
@onChange={{this.handleInput}}
@mode={{@lang}}
@disabled={{@disabled}}
@disabled={{@field.disabled}}
class="form-kit__control-code"
style={{this.style}}
...attributes

View File

@ -22,9 +22,9 @@ export default class FKControlComposer extends Component {
<template>
<DEditor
@value={{readonly @value}}
@value={{readonly @field.value}}
@change={{this.handleInput}}
@disabled={{@disabled}}
@disabled={{@field.disabled}}
class="form-kit__control-composer"
style={{this.style}}
@textAreaId={{@field.id}}

View File

@ -13,14 +13,14 @@ export default class FKControlIcon extends Component {
<template>
<IconPicker
@value={{readonly @value}}
@value={{readonly @field.value}}
@onlyAvailable={{true}}
@options={{hash
maximum=1
disabled=@disabled
disabled=@field.disabled
caretDownIcon="angle-down"
caretUpIcon="angle-up"
icons=@value
icons=@field.value
}}
@onChange={{this.handleInput}}
class="form-kit__control-icon"

View File

@ -18,7 +18,7 @@ export default class FKControlImage extends Component {
}
get imageUrl() {
return isBlank(this.args.value) ? null : this.args.value;
return isBlank(this.args.field.value) ? null : this.args.field.value;
}
<template>

View File

@ -66,13 +66,13 @@ export default class FKControlInput extends Component {
<input
type={{this.type}}
value={{@value}}
value={{@field.value}}
class={{concatClass
"form-kit__control-input"
(if @before "has-prefix")
(if @after "has-suffix")
}}
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
{{on "input" this.handleInput}}
/>

View File

@ -23,11 +23,11 @@ export default class FKControlMenu extends Component {
<DMenu
@onRegisterApi={{this.registerMenuApi}}
@triggerClass="form-kit__control-menu"
@disabled={{@disabled}}
@disabled={{@field.disabled}}
@placement="bottom-start"
@offset={{5}}
id={{@field.id}}
data-value={{@value}}
data-value={{@field.value}}
@modalForMobile={{true}}
>
<:trigger>

View File

@ -59,9 +59,9 @@ export default class FKControlInput extends Component {
>
<input
type={{this.type}}
value={{@value}}
value={{@field.value}}
class="form-kit__control-password"
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
{{on "input" this.handleInput}}
{{this.focusState}}

View File

@ -22,9 +22,9 @@ export default class FKControlQuestion extends Component {
name={{@field.name}}
type="radio"
value="true"
checked={{eq @value true}}
checked={{eq @field.value true}}
class="form-kit__control-radio"
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
id={{uuid}}
{{on "change" this.handleInput}}
@ -44,9 +44,9 @@ export default class FKControlQuestion extends Component {
name={{@field.name}}
type="radio"
value="false"
checked={{eq @value false}}
checked={{eq @field.value false}}
class="form-kit__control-radio"
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
id={{uuid}}
{{on "change" this.handleInput}}

View File

@ -15,12 +15,7 @@ export default class FKControlRadioGroup extends Component {
>
{{yield
(hash
Radio=(component
FKControlRadioGroupRadio
groupValue=@value
field=@field
disabled=@disabled
)
Radio=(component FKControlRadioGroupRadio value=@value field=@field)
)
}}
</FKFieldset>

View File

@ -21,10 +21,10 @@ const FKControlRadioGroupRadio = <template>
name={{@field.name}}
type="radio"
value={{@value}}
checked={{eq @groupValue @value}}
checked={{eq @field.value @value}}
id={{uuid}}
class="form-kit__control-radio"
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
{{on "change" (withEventValue @field.set)}}
/>

View File

@ -19,13 +19,15 @@ export default class FKControlSelect extends Component {
<template>
<select
value={{@value}}
disabled={{@disabled}}
value={{@field.value}}
disabled={{@field.disabled}}
...attributes
class="form-kit__control-select"
{{on "input" this.handleInput}}
>
{{yield (hash Option=(component FKControlSelectOption selected=@value))}}
{{yield
(hash Option=(component FKControlSelectOption selected=@field.value))
}}
</select>
</template>
}

View File

@ -24,9 +24,9 @@ export default class FKControlTextarea extends Component {
<textarea
class="form-kit__control-textarea"
style={{this.style}}
disabled={{@disabled}}
disabled={{@field.disabled}}
...attributes
{{on "input" this.handleInput}}
>{{@value}}</textarea>
>{{@field.value}}</textarea>
</template>
}

View File

@ -13,8 +13,8 @@ export default class FKControlToggle extends Component {
<template>
<DToggleSwitch
@state={{@value}}
disabled={{@disabled}}
@state={{@field.value}}
disabled={{@field.disabled}}
{{on "click" this.handleInput}}
class="form-kit__control-toggle"
/>

View File

@ -0,0 +1,237 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import ValidationParser from "discourse/form-kit/lib/validation-parser";
import Validator from "discourse/form-kit/lib/validator";
import uniqueId from "discourse/helpers/unique-id";
/**
* Represents a field in a form with validation, registration, and field data management capabilities.
*/
export default class FKFieldData extends Component {
/**
* Unique identifier for the field.
* @type {string}
*/
id = uniqueId();
/**
* Unique identifier for the field's error element.
* @type {string}
*/
errorId = uniqueId();
/**
* Type of the field.
* @type {string}
*/
type;
/**
* Initializes the FKFieldData component.
* Validates the presence of required arguments and registers the field.
* @throws {Error} If `@title` is not provided.
*/
constructor() {
super(...arguments);
if (!this.args.title?.length) {
throw new Error("@title is required on `<form.Field />`.");
}
this.args.registerField(this.name, this);
}
/**
* Retrieves the current value of the field.
* @type {any}
*/
get value() {
return this.args.data.get(this.name);
}
/**
* Parses the validation rules for the field.
* @type {Object|null}
*/
get rules() {
return this.args.validation
? ValidationParser.parse(this.args.validation)
: null;
}
/**
* Updates the value of the field and triggers revalidation.
* @param {any} value - The new value for the field.
* @returns {Promise<void>}
*/
@action
async set(value) {
if (this.args.onSet) {
await this.args.onSet(value, {
set: this.args.set,
index: this.args.collectionIndex,
});
} else {
await this.args.set(this.name, value, {
index: this.args.collectionIndex,
});
}
this.args.triggerRevalidationFor(this.name);
}
/**
* Title of the field.
* @type {string}
*/
get title() {
return this.args.title;
}
/**
* Format of the field.
* @type {string}
*/
get format() {
return this.args.format;
}
/**
* Indicates whether the field is disabled.
* Defaults to `false`.
* @type {boolean}
*/
get disabled() {
return this.args.disabled ?? false;
}
/**
* Description of the field.
* @type {string}
*/
get description() {
return this.args.description;
}
/**
* Indicates whether to show the field's title.
* Defaults to `true`.
* @type {boolean}
*/
get showTitle() {
return this.args.showTitle ?? true;
}
/**
* Function to add errors to the field.
* @type {Function}
*/
get addError() {
return this.args.addError;
}
/**
* Constructs the unique name for the field.
* @type {string}
* @throws {Error} If `name` is not a string or contains invalid characters.
*/
get name() {
if (typeof this.args.name !== "string") {
throw new Error(
"@name is required and must be a string on `<form.Field />`."
);
}
if (this.args.name.includes(".") || this.args.name.includes("-")) {
throw new Error("@name can't include `.` or `-`.");
}
return (
(this.args.collectionName ? `${this.args.collectionName}.` : "") +
(this.args.collectionIndex !== undefined
? `${this.args.collectionIndex}.`
: "") +
this.args.name
);
}
/**
* Validation rules for the field.
* @type {string|Object}
*/
get validation() {
return this.args.validation;
}
/**
* Custom validation function.
* @type {Function}
*/
get customValidate() {
return this.args.validate;
}
/**
* Indicates if the field is required.
* Derived from validation rules.
* @type {boolean}
* @readonly
*/
get required() {
return this.rules?.required ?? false;
}
/**
* Maximum length of the field value.
* Derived from validation rules.
* @type {number|null}
* @readonly
*/
get maxLength() {
return this.rules?.length?.max ?? null;
}
/**
* Minimum length of the field value.
* Derived from validation rules.
* @type {number|null}
* @readonly
*/
get minLength() {
return this.rules?.length?.min ?? null;
}
/**
* Validates the field value.
* @param {string} name - The name of the field.
* @param {any} value - The value of the field.
* @param {Object} data - Additional data for validation.
* @returns {Promise<Object>} The validation errors.
*/
async validate(name, value, data) {
if (this.disabled) {
return;
}
await this.customValidate?.(name, value, {
data,
type: this.type,
addError: this.addError,
});
const validator = new Validator(value, this.rules);
const validationErrors = await validator.validate(this.type);
validationErrors.forEach((message) => {
let title = this.title;
if (this.args.collectionIndex !== undefined) {
title += ` #${this.args.collectionIndex + 1}`;
}
this.addError(name, { title, message });
});
}
<template>
{{yield this}}
</template>
}

View File

@ -1,5 +1,4 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import FKControlCheckbox from "discourse/form-kit/components/fk/control/checkbox";
import FKControlCode from "discourse/form-kit/components/fk/control/code";
@ -16,6 +15,7 @@ import FKControlSelect from "discourse/form-kit/components/fk/control/select";
import FKControlTextarea from "discourse/form-kit/components/fk/control/textarea";
import FKControlToggle from "discourse/form-kit/components/fk/control/toggle";
import FKControlWrapper from "discourse/form-kit/components/fk/control-wrapper";
import FKFieldData from "discourse/form-kit/components/fk/field-data";
import FKRow from "discourse/form-kit/components/fk/row";
const RowColWrapper = <template>
@ -32,57 +32,6 @@ const EmptyWrapper = <template>
</template>;
export default class FKField extends Component {
@tracked field;
@tracked name;
constructor() {
super(...arguments);
if (!this.args.title?.length) {
throw new Error("@title is required on `<form.Field />`.");
}
if (typeof this.args.name !== "string") {
throw new Error(
"@name is required and must be a string on `<form.Field />`."
);
}
if (this.args.name.includes(".") || this.args.name.includes("-")) {
throw new Error("@name can't include `.` or `-`.");
}
this.name =
(this.args.collectionName ? `${this.args.collectionName}.` : "") +
(this.args.collectionIndex !== undefined
? `${this.args.collectionIndex}.`
: "") +
this.args.name;
this.field = this.args.registerField(this.name, {
triggerRevalidationFor: this.args.triggerRevalidationFor,
title: this.args.title,
description: this.args.description,
showTitle: this.args.showTitle,
collectionIndex: this.args.collectionIndex,
set: this.args.set,
addError: this.args.addError,
validate: this.args.validate,
validation: this.args.validation,
onSet: this.args.onSet,
});
}
willDestroy() {
this.args.unregisterField(this.name);
super.willDestroy();
}
get value() {
return this.args.data.get(this.name);
}
get wrapper() {
if (this.args.size) {
return RowColWrapper;
@ -92,142 +41,134 @@ export default class FKField extends Component {
}
<template>
<this.wrapper @size={{@size}}>
{{yield
(hash
Custom=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlCustom
value=this.value
field=this.field
format=@format
<FKFieldData
@name={{@name}}
@data={{@data}}
@triggerRevalidationFor={{@triggerRevalidationFor}}
@title={{@title}}
@description={{@description}}
@showTitle={{@showTitle}}
@collectionIndex={{@collectionIndex}}
@set={{@set}}
@addError={{@addError}}
@validate={{@validate}}
@validation={{@validation}}
@onSet={{@onSet}}
@registerField={{@registerField}}
@format={{@format}}
@disabled={{@disabled}}
@collectionName={{@collectionName}}
as |field|
>
<this.wrapper @size={{@size}}>
{{yield
(hash
Custom=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlCustom
field=field
)
Code=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlCode
field=field
)
Question=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlQuestion
field=field
)
Textarea=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlTextarea
field=field
)
Checkbox=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlCheckbox
field=field
)
Image=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlImage
field=field
)
Password=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlPassword
field=field
)
Composer=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlComposer
field=field
)
Icon=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlIcon
field=field
)
Toggle=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlToggle
field=field
)
Menu=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlMenu
field=field
)
Select=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlSelect
field=field
)
Input=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlInput
field=field
)
RadioGroup=(component
FKControlWrapper
unregisterField=@unregisterField
errors=@errors
component=FKControlRadioGroup
field=field
)
errorId=field.errorId
id=field.id
name=field.name
set=field.set
value=field.value
)
Code=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlCode
value=this.value
field=this.field
format=@format
)
Question=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlQuestion
value=this.value
field=this.field
format=@format
)
Textarea=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlTextarea
value=this.value
field=this.field
format=@format
)
Checkbox=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlCheckbox
value=this.value
field=this.field
format=@format
)
Image=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlImage
value=this.value
field=this.field
format=@format
)
Password=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlPassword
value=this.value
field=this.field
format=@format
)
Composer=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlComposer
value=this.value
field=this.field
format=@format
)
Icon=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlIcon
value=this.value
field=this.field
format=@format
)
Toggle=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlToggle
value=this.value
field=this.field
format=@format
)
Menu=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlMenu
value=this.value
field=this.field
format=@format
)
Select=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlSelect
value=this.value
field=this.field
format=@format
)
Input=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlInput
value=this.value
field=this.field
format=@format
)
RadioGroup=(component
FKControlWrapper
errors=@errors
disabled=@disabled
component=FKControlRadioGroup
value=this.value
field=this.field
format=@format
)
errorId=this.field.errorId
id=this.field.id
name=this.field.name
set=this.field.set
value=this.value
)
}}
</this.wrapper>
}}
</this.wrapper>
</FKFieldData>
</template>
}

View File

@ -18,7 +18,6 @@ import Row from "discourse/form-kit/components/fk/row";
import FKSection from "discourse/form-kit/components/fk/section";
import FKSubmit from "discourse/form-kit/components/fk/submit";
import { VALIDATION_TYPES } from "discourse/form-kit/lib/constants";
import FKFieldData from "discourse/form-kit/lib/fk-field-data";
import FKFormData from "discourse/form-kit/lib/fk-form-data";
import { i18n } from "discourse-i18n";
@ -151,10 +150,9 @@ class FKForm extends Component {
);
}
const fieldModel = new FKFieldData(name, field);
this.fields.set(name, fieldModel);
this.fields.set(name, field);
return fieldModel;
return field;
}
@action

View File

@ -4,7 +4,7 @@ import FKErrors from "discourse/form-kit/components/fk/errors";
export default class FKMeta extends Component {
get shouldRenderCharCounter() {
return this.args.field.maxLength > 0 && !this.args.disabled;
return this.args.field.maxLength > 0 && !this.args.field.disabled;
}
get shouldRenderMeta() {
@ -24,7 +24,7 @@ export default class FKMeta extends Component {
{{#if this.shouldRenderCharCounter}}
<FKCharCounter
@value={{@value}}
@value={{@field.value}}
@minLength={{@field.minLength}}
@maxLength={{@field.maxLength}}
/>

View File

@ -1,145 +0,0 @@
import ValidationParser from "discourse/form-kit/lib/validation-parser";
import Validator from "discourse/form-kit/lib/validator";
import uniqueId from "discourse/helpers/unique-id";
/**
* Represents field data for a form.
*/
export default class FKFieldData {
/**
* Unique identifier for the field.
* @type {string}
*/
id = uniqueId();
/**
* Unique identifier for the field error.
* @type {Function}
*/
errorId = uniqueId();
/**
* Type of the field.
* @type {string}
*/
type;
/**
* Creates an instance of FieldData.
* @param {string} name - The name of the field.
* @param {Object} options - The options for the field.
* @param {Function} options.set - The callback function for setting the field value.
* @param {Function} options.onSet - The callback function for setting the custom field value.
* @param {string} options.validation - The validation rules for the field.
* @param {boolean} [options.disabled=false] - Indicates if the field is disabled.
* @param {Function} [options.validate] - The custom validation function.
* @param {Function} [options.title] - The custom field title.
* @param {Function} [options.subtitle] - The custom field subtitle.
* @param {Function} [options.description] - The custom field description.
* @param {Function} [options.showTitle=true] - Indicates if the field title should be shown.
* @param {Function} [options.triggerRevalidationFor] - The function to trigger revalidation.
* @param {Function} [options.addError] - The function to add an error message.
*/
constructor(
name,
{
set,
onSet,
validation,
disabled = false,
validate,
title,
subtitle,
description,
showTitle = true,
triggerRevalidationFor,
collectionIndex,
addError,
}
) {
this.name = name;
this.title = title;
this.subtitle = subtitle;
this.description = description;
this.collectionIndex = collectionIndex;
this.addError = addError;
this.showTitle = showTitle;
this.disabled = disabled;
this.customValidate = validate;
this.validation = validation;
this.rules = this.validation ? ValidationParser.parse(validation) : null;
this.set = (value) => {
if (onSet) {
onSet(value, { set, index: collectionIndex });
} else {
set(this.name, value, { index: collectionIndex });
}
triggerRevalidationFor(name);
};
}
/**
* Checks if the field is required.
* @type {boolean}
* @readonly
*/
get required() {
return this.rules?.required ?? false;
}
/**
* Sets the type of the field.
*/
setType(type) {
this.type = type;
}
/**
* Gets the maximum length of the field value.
* @type {number|null}
* @readonly
*/
get maxLength() {
return this.rules?.length?.max ?? null;
}
/**
* Gets the minimum length of the field value.
* @type {number|null}
* @readonly
*/
get minLength() {
return this.rules?.length?.min ?? null;
}
/**
* Validates the field value.
* @param {string} name - The name of the field.
* @param {any} value - The value of the field.
* @param {Object} data - Additional data for validation.
* @returns {Promise<Object>} The validation errors.
*/
async validate(name, value, data) {
if (this.disabled) {
return;
}
await this.customValidate?.(name, value, {
data,
type: this.type,
addError: this.addError,
});
const validator = new Validator(value, this.rules);
const validationErrors = await validator.validate(this.type);
validationErrors.forEach((message) => {
let title = this.title;
if (this.collectionIndex !== undefined) {
title += ` #${this.collectionIndex + 1}`;
}
this.addError(name, { title, message });
});
}
}