@modular-forms/solid
Version:
The modular and type-safe form library for SolidJS
1,708 lines (1,505 loc) • 78.4 kB
JavaScript
'use strict';
var solidJs = require('solid-js');
var web = require('solid-js/web');
var valibot = require('valibot');
/**
* Creates a validation functions that parses the Valibot schema of a field.
*
* @param schema A Valibot schema.
*
* @returns A validation function.
*/
function valiField(schema) {
return async value => {
const result = await valibot.safeParseAsync(schema, value, {
abortPipeEarly: true
});
return result.issues?.[0]?.message || '';
};
}
/**
* Creates a validation functions that parses the Valibot schema of a form.
*
* @param schema A Valibot schema.
*
* @returns A validation function.
*/
function valiForm(schema) {
return async values => {
const result = await valibot.safeParseAsync(schema, values, {
abortPipeEarly: true
});
const formErrors = {};
if (result.issues) {
for (const issue of result.issues) {
formErrors[valibot.getDotPath(issue)] = issue.message;
}
}
return formErrors;
};
}
/**
* Creates a validation functions that parses the Zod schema of a field.
*
* @param schema A Zod schema.
*
* @returns A validation function.
*/
function zodField(schema) {
return async value => {
const result = await schema.safeParseAsync(value);
return result.success ? '' : result.error.issues[0].message;
};
}
/**
* Creates a validation functions that parses the Zod schema of a form.
*
* @param schema A Zod schema.
*
* @returns A validation function.
*/
function zodForm(schema) {
return async values => {
const result = await schema.safeParseAsync(values);
const formErrors = {};
if (!result.success) {
for (const issue of result.error.issues) {
const path = issue.path.join('.');
if (!formErrors[path]) {
formErrors[path] = issue.message;
}
}
}
return formErrors;
};
}
/**
* Value type of signal object.
*/
/**
* Creates a simple reactive state with a getter and setter.
*/
function createSignal(value) {
const [get, set] = solidJs.createSignal(value);
return {
get,
set
};
}
/**
* Creates and returns the store of the form.
*
* @param options The form options.
*
* @returns The reactive store.
*/
function createFormStore({
initialValues = {},
validateOn = 'submit',
revalidateOn = 'input',
validate
} = {}) {
// Create signals of form store
const fieldNames = createSignal([]);
const fieldArrayNames = createSignal([]);
const element = createSignal();
const submitCount = createSignal(0);
const submitting = createSignal(false);
const submitted = createSignal(false);
const validating = createSignal(false);
const touched = createSignal(false);
const dirty = createSignal(false);
const invalid = createSignal(false);
const response = createSignal({});
// Return form functions and state
return {
internal: {
// Props
initialValues,
validate,
validateOn,
revalidateOn,
// Signals
fieldNames,
fieldArrayNames,
element,
submitCount,
submitting,
submitted,
validating,
touched,
dirty,
invalid,
response,
// Stores
fields: {},
fieldArrays: {},
// Other
validators: new Set()
},
get element() {
return element.get();
},
get submitCount() {
return submitCount.get();
},
get submitting() {
return submitting.get();
},
get submitted() {
return submitted.get();
},
get validating() {
return validating.get();
},
get touched() {
return touched.get();
},
get dirty() {
return dirty.get();
},
get invalid() {
return invalid.get();
},
get response() {
return response.get();
}
};
}
/**
* Creates and returns the store of the form as well as a linked Form, Field
* and FieldArray component.
*
* @param options The form options.
*
* @returns The store and linked components.
*/
function createForm(options) {
// Create form store
const form = createFormStore(options);
// Return form store and linked components
return [form, {
Form: (props
// eslint-disable-next-line solid/reactivity
) => Form(solidJs.mergeProps({
of: form
}, props)),
Field: props => Field(
// eslint-disable-next-line solid/reactivity
solidJs.mergeProps({
of: form
}, props)),
FieldArray: (props
// eslint-disable-next-line solid/reactivity
) => FieldArray(solidJs.mergeProps({
of: form
}, props))
}];
}
/**
* Returns the current input of the element.
*
* @param element The field element.
* @param field The store of the field.
* @param type The data type to capture.
*
* @returns The element input.
*/
function getElementInput(element, field, type) {
const {
checked,
files,
options,
value,
valueAsDate,
valueAsNumber
} = element;
return solidJs.untrack(() => !type || type === 'string' ? value : type === 'string[]' ? options ? [...options].filter(e => e.selected && !e.disabled).map(e => e.value) : checked ? [...(field.value.get() || []), value] : (field.value.get() || []).filter(v => v !== value) : type === 'number' ? valueAsNumber : type === 'boolean' ? checked : type === 'File' && files ? files[0] : type === 'File[]' && files ? [...files] : type === 'Date' && valueAsDate ? valueAsDate : field.value.get());
}
/**
* Returns a tuple with all field and field array stores of a form.
*
* @param form The form of the stores.
*
* @returns The store tuple.
*/
function getFieldAndArrayStores(form) {
return [...Object.values(form.internal.fields), ...Object.values(form.internal.fieldArrays)];
}
/**
* Returns the store of a field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
*
* @returns The reactive store.
*/
function getFieldArrayStore(form, name) {
return form.internal.fieldArrays[name];
}
/**
* Returns the index of the path in the field array.
*
* @param name The name of the field array.
* @param path The path to get the index from.
*
* @returns The field index in the array.
*/
function getPathIndex(name, path) {
return +path.replace(`${name}.`, '').split('.')[0];
}
/**
* Removes invalid field or field array names of field arrays.
*
* @param form The form of the field array.
* @param names The names to be filtered.
*/
function removeInvalidNames(form, names) {
getFieldArrayNames(form, false).forEach(fieldArrayName => {
const lastIndex = solidJs.untrack(getFieldArrayStore(form, fieldArrayName).items.get).length - 1;
names.filter(name => name.startsWith(`${fieldArrayName}.`) && getPathIndex(fieldArrayName, name) > lastIndex).forEach(name => {
names.splice(names.indexOf(name), 1);
});
});
}
/**
* Returns a list with the names of all field arrays.
*
* @param form The form of the field arrays.
* @param shouldValid Whether to be valid.
*
* @returns All field array names of the form.
*/
function getFieldArrayNames(form, shouldValid = true) {
// Get name of every field array
const fieldArrayNames = [...solidJs.untrack(form.internal.fieldArrayNames.get)];
// Remove invalid field array names
if (shouldValid) {
removeInvalidNames(form, fieldArrayNames);
}
// Return field array names
return fieldArrayNames;
}
/**
* Returns the RAW state of the field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
*
* @returns The state of the field array.
*/
function getFieldArrayState(form, name) {
const fieldArray = getFieldArrayStore(form, name);
return fieldArray ? solidJs.untrack(() => ({
startItems: fieldArray.startItems.get(),
items: fieldArray.items.get(),
error: fieldArray.error.get(),
touched: fieldArray.touched.get(),
dirty: fieldArray.dirty.get()
})) : undefined;
}
/**
* Returns a list with the names of all fields.
*
* @param form The form of the fields.
* @param shouldValid Whether to be valid.
*
* @returns All field names of the form.
*/
function getFieldNames(form, shouldValid = true) {
// Get name of every field
const fieldNames = [...solidJs.untrack(form.internal.fieldNames.get)];
// Remove invalid field names
if (shouldValid) {
removeInvalidNames(form, fieldNames);
}
// Return field names
return fieldNames;
}
/**
* Returns the store of a field.
*
* @param form The form of the field.
* @param name The name of the field.
*
* @returns The reactive store.
*/
function getFieldStore(form, name) {
return form.internal.fields[name];
}
/**
* Returns the RAW state of the field.
*
* @param form The form of the field.
* @param name The name of the field.
*
* @returns The state of the field.
*/
function getFieldState(form, name) {
const field = getFieldStore(form, name);
return field ? solidJs.untrack(() => ({
startValue: field.startValue.get(),
value: field.value.get(),
error: field.error.get(),
touched: field.touched.get(),
dirty: field.dirty.get()
})) : undefined;
}
/**
* Returns a tuple with filtered field and field array names. For each
* specified field array name, the names of the contained fields and field
* arrays are also returned. If no name is specified, the name of each field
* and field array of the form is returned.
*
* @param form The form of the fields.
* @param arg2 The name of the fields.
* @param shouldValid Whether to be valid.
*
* @returns A tuple with filtered names.
*/
function getFilteredNames(form, arg2, shouldValid) {
return solidJs.untrack(() => {
// Get all field and field array names of form
const allFieldNames = getFieldNames(form, shouldValid);
const allFieldArrayNames = getFieldArrayNames(form, shouldValid);
// If names are specified, filter and return them
if (typeof arg2 === 'string' || Array.isArray(arg2)) {
return (typeof arg2 === 'string' ? [arg2] : arg2).reduce((tuple, name) => {
// Destructure tuple
const [fieldNames, fieldArrayNames] = tuple;
// If it is name of a field array, add it and name of each field
// and field array it contains to field and field array names
if (allFieldArrayNames.includes(name)) {
allFieldArrayNames.forEach(fieldArrayName => {
if (fieldArrayName.startsWith(name)) {
fieldArrayNames.add(fieldArrayName);
}
});
allFieldNames.forEach(fieldName => {
if (fieldName.startsWith(name)) {
fieldNames.add(fieldName);
}
});
// If it is name of a field, add it to field name set
} else {
fieldNames.add(name);
}
// Return tuple
return tuple;
}, [new Set(), new Set()]).map(set => [...set]);
}
// Otherwise return every field and field array name
return [allFieldNames, allFieldArrayNames];
});
}
/**
* Filters the options object from the arguments and returns it.
*
* @param arg1 Maybe the options object.
* @param arg2 Maybe the options object.
*
* @returns The options object.
*/
function getOptions(arg1, arg2) {
return (typeof arg1 !== 'string' && !Array.isArray(arg1) ? arg1 : arg2) || {};
}
/**
* Returns the value of a dot path in an object.
*
* @param path The dot path.
* @param object The object.
*
* @returns The value or undefined.
*/
/**
* Returns the value of a dot path in an object.
*
* @param path The dot path.
* @param object The object.
*
* @returns The value or undefined.
*/
function getPathValue(path, object) {
return path.split('.').reduce((value, key) => value?.[key], object);
}
// Create counter variable
let counter = 0;
/**
* Returns a unique ID counting up from zero.
*
* @returns A unique ID.
*/
function getUniqueId() {
return counter++;
}
/**
* Returns whether the field is dirty.
*
* @param startValue The start value.
* @param currentValue The current value.
*
* @returns Whether is dirty.
*/
function isFieldDirty(startValue, currentValue) {
const toValue = item => item instanceof Blob ? item.size : item;
return Array.isArray(startValue) && Array.isArray(currentValue) ? startValue.map(toValue).join() !== currentValue.map(toValue).join() : startValue instanceof Date && currentValue instanceof Date ? startValue.getTime() !== currentValue.getTime() : Number.isNaN(startValue) && Number.isNaN(currentValue) ? false : startValue !== currentValue;
}
/**
* Updates the dirty state of the form.
*
* @param form The store of the form.
* @param dirty Whether dirty state is true.
*/
function updateFormDirty(form, dirty) {
solidJs.untrack(() => form.internal.dirty.set(dirty || getFieldAndArrayStores(form).some(fieldOrFieldArray => fieldOrFieldArray.active.get() && fieldOrFieldArray.dirty.get())));
}
/**
* Updates the dirty state of a field.
*
* @param form The form of the field.
* @param field The store of the field.
*/
function updateFieldDirty(form, field) {
solidJs.untrack(() => {
// Check if field is dirty
const dirty = isFieldDirty(field.startValue.get(), field.value.get());
// Update dirty state of field if necessary
if (dirty !== field.dirty.get()) {
solidJs.batch(() => {
field.dirty.set(dirty);
// Update dirty state of form
updateFormDirty(form, dirty);
});
}
});
}
/**
* Value type of the validate otions.
*/
/**
* Validates a field or field array only if required.
*
* @param form The form of the field or field array.
* @param fieldOrFieldArray The store of the field or field array.
* @param name The name of the field or field array.
* @param options The validate options.
*/
function validateIfRequired(form, fieldOrFieldArray, name, {
on: modes,
shouldFocus = false
}) {
solidJs.untrack(() => {
const validateOn = fieldOrFieldArray.validateOn ?? form.internal.validateOn;
const revalidateOn = fieldOrFieldArray.revalidateOn ?? form.internal.revalidateOn;
if (modes.includes((validateOn === 'submit' ? form.internal.submitted.get() : fieldOrFieldArray.error.get()) ? revalidateOn : validateOn)) {
validate(form, name, {
shouldFocus
});
}
});
}
/**
* Handles the input, change and blur event of a field.
*
* @param form The form of the field.
* @param field The store of the field.
* @param name The name of the field.
* @param event The event of the field.
* @param validationModes The modes of the validation.
* @param inputValue The value of the input.
*/
function handleFieldEvent(form, field, name, event, validationModes, inputValue) {
solidJs.batch(() => {
// Update value state
field.value.set(prevValue => field.transform.reduce((current, transformation) => transformation(current, event), inputValue ?? prevValue));
// Update touched state
field.touched.set(true);
form.internal.touched.set(true);
// Update dirty state
updateFieldDirty(form, field);
// Validate value if required
validateIfRequired(form, field, name, {
on: validationModes
});
});
}
/**
* Initializes and returns the store of a field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
*
* @returns The reactive store.
*/
function initializeFieldArrayStore(form, name) {
// Initialize store on first request
if (!getFieldArrayStore(form, name)) {
// Create initial items of field array
const initial = getPathValue(name, form.internal.initialValues)?.map(() => getUniqueId()) || [];
// Create signals of field array store
const initialItems = createSignal(initial);
const startItems = createSignal(initial);
const items = createSignal(initial);
const error = createSignal('');
const active = createSignal(false);
const touched = createSignal(false);
const dirty = createSignal(false);
// Add store of field array to form
form.internal.fieldArrays[name] = {
// Signals
initialItems,
startItems,
items,
error,
active,
touched,
dirty,
// Other
validate: [],
validateOn: undefined,
revalidateOn: undefined,
consumers: new Set()
};
// Add name of field array to form
form.internal.fieldArrayNames.set(names => [...names, name]);
}
// Return store of field array
return getFieldArrayStore(form, name);
}
/**
* Initializes and returns the store of a field.
*
* @param form The form of the field.
* @param name The name of the field.
*
* @returns The reactive store.
*/
function initializeFieldStore(form, name) {
// Initialize store on first request
if (!getFieldStore(form, name)) {
// Get initial value of field
const initial = getPathValue(name, form.internal.initialValues);
// Create signals of field store
const elements = createSignal([]);
const initialValue = createSignal(initial);
const startValue = createSignal(initial);
const value = createSignal(initial);
const error = createSignal('');
const active = createSignal(false);
const touched = createSignal(false);
const dirty = createSignal(false);
// Add store of field to form
// @ts-expect-error
form.internal.fields[name] = {
// Signals
elements,
initialValue,
startValue,
value,
error,
active,
touched,
dirty,
// Other
validate: [],
validateOn: undefined,
revalidateOn: undefined,
transform: [],
consumers: new Set()
};
// Add name of field to form
form.internal.fieldNames.set(names => [...names, name]);
}
// Return store of field
return getFieldStore(form, name);
}
/**
* Value type of the error response options.
*/
/**
* Sets an error response if a form error was not set at any field or field
* array.
*
* @param form The form of the errors.
* @param formErrors The form errors.
* @param options The error options.
*/
function setErrorResponse(form, formErrors, {
shouldActive = true
}) {
// Combine errors that were not set for any field or field array into one
// general form error response message
const message = Object.entries(formErrors).reduce((errors, [name, error]) => {
if ([getFieldStore(form, name), getFieldArrayStore(form, name)].every(fieldOrFieldArray => !fieldOrFieldArray || shouldActive && !solidJs.untrack(fieldOrFieldArray.active.get))) {
errors.push(error);
}
return errors;
}, []).join(' ');
// If there is a error message, set it as form response
if (message) {
form.internal.response.set({
status: 'error',
message
});
}
}
/**
* Sets the store of a field array to the specified state.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param state The new state to be set.
*/
function setFieldArrayState(form, name, state) {
const fieldArray = initializeFieldArrayStore(form, name);
fieldArray.startItems.set(state.startItems);
fieldArray.items.set(state.items);
fieldArray.error.set(state.error);
fieldArray.touched.set(state.touched);
fieldArray.dirty.set(state.dirty);
}
/**
* Sets the store of a field to the specified state.
*
* @param form The form of the field.
* @param name The name of the field.
* @param state The new state to be set.
*/
function setFieldState(form, name, state) {
const field = initializeFieldStore(form, name);
field.startValue.set(() => state.startValue);
field.value.set(() => state.value);
field.error.set(state.error);
field.touched.set(state.touched);
field.dirty.set(state.dirty);
}
/**
* Value type of the value options.
*/
/**
* Sets the specified field array value to the corresponding field and field
* array stores.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The value options.
*/
function setFieldArrayValue(form, name, {
at: index,
value
}) {
solidJs.batch(() => {
// Create recursive function to update stores
const updateStores = (prevPath, data) => {
Object.entries(data).forEach(([path, value]) => {
// Create new compound path
const compoundPath = `${prevPath}.${path}`;
// Set field store if it could be a field value
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date || value instanceof Blob) {
setFieldState(form, compoundPath, {
startValue: value,
value,
error: '',
touched: false,
dirty: false
});
}
// Set field array store if it could be a field array value
if (Array.isArray(value)) {
const items = value.map(() => getUniqueId());
setFieldArrayState(form, compoundPath, {
startItems: [...items],
items,
error: '',
touched: false,
dirty: false
});
}
// Update nested stores if it is a field array or nested field
if (value && typeof value === 'object') {
updateStores(compoundPath, value);
}
});
};
// Update store of field array index
updateStores(name, {
[index]: value
});
});
}
/**
* Returns a function that sorts field names by their array path index.
*
* @param name The name of the field array.
*
* @returns The sort function.
*/
function sortArrayPathIndex(name) {
return (pathA, pathB) => getPathIndex(name, pathA) - getPathIndex(name, pathB);
}
/**
* Updates the dirty state of a field array.
*
* @param form The form of the field array.
* @param fieldArray The store of the field array.
*/
function updateFieldArrayDirty(form, fieldArray) {
solidJs.untrack(() => {
// Check if field array is dirty
const dirty = fieldArray.startItems.get().join() !== fieldArray.items.get().join();
// Update dirty state of field array if necessary
if (dirty !== fieldArray.dirty.get()) {
solidJs.batch(() => {
fieldArray.dirty.set(dirty);
// Update dirty state of form
updateFormDirty(form, dirty);
});
}
});
}
/**
* Updates the invalid state of the form.
*
* @param form The store of the form.
* @param dirty Whether there is an error.
*/
function updateFormInvalid(form, invalid) {
solidJs.untrack(() => {
form.internal.invalid.set(invalid || getFieldAndArrayStores(form).some(fieldOrFieldArray => fieldOrFieldArray.active.get() && fieldOrFieldArray.error.get()));
});
}
/**
* Updates the touched, dirty and invalid state of the form.
*
* @param form The store of the form.
*/
function updateFormState(form) {
// Create state variables
let touched = false,
dirty = false,
invalid = false;
// Check each field and field array and update state if necessary
solidJs.untrack(() => {
for (const fieldOrFieldArray of getFieldAndArrayStores(form)) {
if (fieldOrFieldArray.active.get()) {
if (fieldOrFieldArray.touched.get()) {
touched = true;
}
if (fieldOrFieldArray.dirty.get()) {
dirty = true;
}
if (fieldOrFieldArray.error.get()) {
invalid = true;
}
}
// Break loop if all state values are "true"
if (touched && dirty && invalid) {
break;
}
}
});
// Update state of form
solidJs.batch(() => {
form.internal.touched.set(touched);
form.internal.dirty.set(dirty);
form.internal.invalid.set(invalid);
});
}
/**
* Focuses the specified field of the form.
*
* @param form The form of the field.
* @param name The name of the field.
*/
function focus(form, name) {
solidJs.untrack(() => getFieldStore(form, name)?.elements.get()[0]?.focus());
}
/**
* Value type of the set error options.
*/
/**
* Sets the error of the specified field or field array.
*
* @param form The form of the field.
* @param name The name of the field.
* @param error The error message.
* @param options The error options.
*/
function setError(form, name, error, {
shouldActive = true,
shouldTouched = false,
shouldDirty = false,
shouldFocus = !!error
} = {}) {
solidJs.batch(() => {
solidJs.untrack(() => {
for (const fieldOrFieldArray of [getFieldStore(form, name), getFieldArrayStore(form, name)]) {
if (fieldOrFieldArray && (!shouldActive || fieldOrFieldArray.active.get()) && (!shouldTouched || fieldOrFieldArray.touched.get()) && (!shouldDirty || fieldOrFieldArray.dirty.get())) {
// Set error to field or field array
fieldOrFieldArray.error.set(error);
// Focus element if set to "true"
if (error && 'value' in fieldOrFieldArray && shouldFocus) {
focus(form, name);
}
}
}
});
// Update invalid state of form
updateFormInvalid(form, !!error);
});
}
/**
* Clears the error of the specified field or field array.
*
* @param form The form of the field.
* @param name The name of the field.
* @param options The error options.
*/
function clearError(form, name, options) {
setError(form, name, '', options);
}
/**
* Clears the response of the form.
*
* @param form The form of the response.
*/
function clearResponse(form) {
form.internal.response.set({});
}
/**
* Value type of the get error options.
*/
/**
* Returns the error of the specified field or field array.
*
* @param form The form of the field or field array.
* @param name The name of the field or field array.
*
* @returns The error of the field or field array.
*/
function getError(form, name, {
shouldActive = true,
shouldTouched = false,
shouldDirty = false
} = {}) {
// Get store of specified field or field array
const field = getFieldStore(form, name);
const fieldArray = getFieldArrayStore(form, name);
// Return error if field or field array is present and corresponds to filter
// options
for (const fieldOrFieldArray of [field, fieldArray]) {
if (fieldOrFieldArray && (!shouldActive || fieldOrFieldArray.active.get()) && (!shouldTouched || fieldOrFieldArray.touched.get()) && (!shouldDirty || fieldOrFieldArray.dirty.get())) {
return fieldOrFieldArray.error.get();
}
}
// If field and field array is not present, set listeners to be notified when
// a new field or field array is added
if (!field && !fieldArray) {
form.internal.fieldNames.get();
form.internal.fieldArrayNames.get();
}
// Otherwise return undefined
return undefined;
}
/**
* Value type of the get errors options.
*/
/**
* Returns the current errors of the form fields.
*
* @param form The form of the fields.
* @param options The errors options.
*
* @returns The form errors.
*/
/**
* Returns the errors of the specified field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The errors options.
*
* @returns The form errors.
*/
/**
* Returns the current errors of the specified fields and field arrays.
*
* @param form The form of the fields.
* @param names The names of the fields and field arrays.
* @param options The errors options.
*
* @returns The form errors.
*/
function getErrors(form, arg2, arg3) {
// Get filtered field names to get error from
const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2);
// Destructure options and set default values
const {
shouldActive = true,
shouldTouched = false,
shouldDirty = false
} = getOptions(arg2, arg3);
// If no field or field array name is specified, set listener to be notified
// when a new field or field array is added
if (typeof arg2 !== 'string' && !Array.isArray(arg2)) {
form.internal.fieldNames.get();
form.internal.fieldArrayNames.get();
// Otherwise if a field array is included, set listener to be notified when
// a new field array items is added
} else {
fieldArrayNames.forEach(fieldArrayName => getFieldArrayStore(form, fieldArrayName).items.get());
}
// Create and return object with form errors
return [...fieldNames.map(name => [name, getFieldStore(form, name)]), ...fieldArrayNames.map(name => [name, getFieldArrayStore(form, name)])].reduce((formErrors, [name, fieldOrFieldArray]) => {
if (fieldOrFieldArray.error.get() && (!shouldActive || fieldOrFieldArray.active.get()) && (!shouldTouched || fieldOrFieldArray.touched.get()) && (!shouldDirty || fieldOrFieldArray.dirty.get())) {
formErrors[name] = fieldOrFieldArray.error.get();
}
return formErrors;
}, {});
}
/**
* Value type if the get value options.
*/
/**
* Returns the value of the specified field.
*
* @param form The form of the field.
* @param name The name of the field.
* @param options The value options.
*
* @returns The value of the field.
*/
function getValue(form, name, {
shouldActive = true,
shouldTouched = false,
shouldDirty = false,
shouldValid = false
} = {}) {
// Get store of specified field
const field = initializeFieldStore(form, name);
// Continue if field corresponds to filter options
if ((!shouldActive || field.active.get()) && (!shouldTouched || field.touched.get()) && (!shouldDirty || field.dirty.get()) && (!shouldValid || !field.error.get())) {
// Return value of field
return field.value.get();
}
// Otherwise return undefined
return undefined;
}
/**
* Value type of the get values options.
*/
/**
* Returns the current values of the form fields.
*
* @param form The form of the fields.
* @param options The values options.
*
* @returns The form field values.
*/
/**
* Returns the values of the specified field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The values options.
*
* @returns The field array values.
*/
/**
* Returns the current values of the specified fields and field arrays.
*
* @param form The form of the fields.
* @param names The names of the fields and field arrays.
* @param options The values options.
*
* @returns The form field values.
*/
function getValues(form, arg2, arg3) {
// Get filtered field names to get value from
const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2);
// Destructure options and set default values
const {
shouldActive = true,
shouldTouched = false,
shouldDirty = false,
shouldValid = false
} = getOptions(arg2, arg3);
// If no field or field array name is specified, set listener to be notified
// when a new field is added
if (typeof arg2 !== 'string' && !Array.isArray(arg2)) {
form.internal.fieldNames.get();
// Otherwise if a field array is included, set listener to be notified when
// a new field array items is added
} else {
fieldArrayNames.forEach(fieldArrayName => getFieldArrayStore(form, fieldArrayName).items.get());
}
// Create and return values of form or field array
return fieldNames.reduce((values, name) => {
// Get store of specified field
const field = getFieldStore(form, name);
// Add value if field corresponds to filter options
if ((!shouldActive || field.active.get()) && (!shouldTouched || field.touched.get()) && (!shouldDirty || field.dirty.get()) && (!shouldValid || !field.error.get())) {
// Split name into keys to be able to add values of nested fields
(typeof arg2 === 'string' ? name.replace(`${arg2}.`, '') : name).split('.').reduce((object, key, index, keys) => object[key] = index === keys.length - 1 ?
// If it is last key, add value
field.value.get() :
// Otherwise return object or array
typeof object[key] === 'object' && object[key] || (isNaN(+keys[index + 1]) ? {} : []), values);
}
// Return modified values object
return values;
}, typeof arg2 === 'string' ? [] : {});
}
/**
* Value type of the has field options.
*/
/**
* Checks if the specified field is included in the form.
*
* @param form The form of the field.
* @param name The name of the field.
* @param options The field options.
*
* @returns Whether the field is included.
*/
function hasField(form, name, {
shouldActive = true,
shouldTouched = false,
shouldDirty = false,
shouldValid = false
} = {}) {
// eslint-disable-next-line solid/reactivity
return solidJs.createMemo(() => {
// Get store of specified field
const field = getFieldStore(form, name);
// If field is not present, set listener to be notified when a new field is
// added
if (!field) {
form.internal.fieldNames.get();
}
// Return whether field is present and matches filter options
return !!field && (!shouldActive || field.active.get()) && (!shouldTouched || field.touched.get()) && (!shouldDirty || field.dirty.get()) && (!shouldValid || !field.error.get());
})();
}
/**
* Value type of the has field array options.
*/
/**
* Checks if the specified field array is included in the form.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The field array options.
*
* @returns Whether the field array is included.
*/
function hasFieldArray(form, name, {
shouldActive = true,
shouldTouched = false,
shouldDirty = false,
shouldValid = false
} = {}) {
// eslint-disable-next-line solid/reactivity
return solidJs.createMemo(() => {
// Get store of specified field array
const fieldArray = getFieldArrayStore(form, name);
// If field array is not present, set listener to be notified when a new
// field array is added
if (!fieldArray) {
form.internal.fieldArrayNames.get();
}
// Return whether field array is present and matches filter options
return !!fieldArray && (!shouldActive || fieldArray.active.get()) && (!shouldTouched || fieldArray.touched.get()) && (!shouldDirty || fieldArray.dirty.get()) && (!shouldValid || !fieldArray.error.get());
})();
}
/**
* Value type of the insert options.
*/
/**
* Inserts a new item into the field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The insert options.
*/
function insert(form, name, options) {
// Get store of specified field array
const fieldArray = getFieldArrayStore(form, name);
// Continue if specified field array exists
if (fieldArray) {
solidJs.untrack(() => {
// Get length of field array
const arrayLength = fieldArray.items.get().length;
// Destructure options
const {
at: index = arrayLength,
value
} = options;
// Continue if specified index is valid
if (index >= 0 && index <= arrayLength) {
solidJs.batch(() => {
// If item is not inserted at end, move fields and field arrays of items
// that come after new item one index further
if (index < arrayLength) {
// Create function to filter a name
const filterName = value => value.startsWith(`${name}.`) && getPathIndex(name, value) >= index;
// Create function to get next index name
const getNextIndexName = (fieldOrFieldArrayName, fieldOrFieldArrayIndex) => fieldOrFieldArrayName.replace(`${name}.${fieldOrFieldArrayIndex}`, `${name}.${fieldOrFieldArrayIndex + 1}`);
// Move fields that come after new item one index further
getFieldNames(form).filter(filterName).sort(sortArrayPathIndex(name)).reverse().forEach(fieldName => {
setFieldState(form, getNextIndexName(fieldName, getPathIndex(name, fieldName)), getFieldState(form, fieldName));
});
// Move field arrays that come after new item one index further
getFieldArrayNames(form).filter(filterName).sort(sortArrayPathIndex(name)).reverse().forEach(fieldArrayName => {
setFieldArrayState(form, getNextIndexName(fieldArrayName, getPathIndex(name, fieldArrayName)), getFieldArrayState(form, fieldArrayName));
});
}
// Set value of new field array item
setFieldArrayValue(form, name, {
at: index,
value
});
// Insert item into field array
fieldArray.items.set(prevItems => {
const nextItems = [...prevItems];
nextItems.splice(index, 0, getUniqueId());
return nextItems;
});
// Set touched at field array and form to true
fieldArray.touched.set(true);
form.internal.touched.set(true);
// Set dirty at field array and form to true
fieldArray.dirty.set(true);
form.internal.dirty.set(true);
});
}
});
// Validate field array if required with delay to allow new fields to be
// mounted beforehand
setTimeout(() => validateIfRequired(form, fieldArray, name, {
on: ['touched', 'input']
}), 250);
}
}
/**
* Value type of the move options.
*/
/**
* Moves a field of the field array to a new position and rearranges all fields
* in between.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The move options.
*/
function move(form, name, {
from: fromIndex,
to: toIndex
}) {
// Get store of specified field array
const fieldArray = getFieldArrayStore(form, name);
// Continue if specified field array exists
if (fieldArray) {
solidJs.untrack(() => {
// Get last index of field array
const lastIndex = fieldArray.items.get().length - 1;
// Continue if specified indexes are valid
if (fromIndex >= 0 && fromIndex <= lastIndex && toIndex >= 0 && toIndex <= lastIndex && fromIndex !== toIndex) {
// Create function to filter a name
const filterName = value => {
if (value.startsWith(name)) {
const fieldIndex = getPathIndex(name, value);
return fieldIndex >= fromIndex && fieldIndex <= toIndex || fieldIndex <= fromIndex && fieldIndex >= toIndex;
}
};
// Create function to get previous index name
const getPrevIndexName = (fieldOrFieldArrayName, fieldOrFieldArrayIndex) => fieldOrFieldArrayName.replace(`${name}.${fieldOrFieldArrayIndex}`, fromIndex < toIndex ? `${name}.${fieldOrFieldArrayIndex - 1}` : `${name}.${fieldOrFieldArrayIndex + 1}`);
// Create function to get "to" index name
const getToIndexName = fieldOrFieldArrayName => fieldOrFieldArrayName.replace(`${name}.${fromIndex}`, `${name}.${toIndex}`);
// Create list of all affected field and field array names
const fieldNames = getFieldNames(form).filter(filterName).sort(sortArrayPathIndex(name));
const fieldArrayNames = getFieldArrayNames(form).filter(filterName).sort(sortArrayPathIndex(name));
// Reverse names if "from" index is greater than "to" index
if (fromIndex > toIndex) {
fieldNames.reverse();
fieldArrayNames.reverse();
}
// Create field and field array state map
const fieldStateMap = new Map();
const fieldArrayStateMap = new Map();
solidJs.batch(() => {
// Add state of "from" fields to map and move all fields in between forward
// or backward
fieldNames.forEach(fieldName => {
// Get state of current field
const fieldState = getFieldState(form, fieldName);
// Get index of current field
const fieldIndex = getPathIndex(name, fieldName);
// Add state of field to map if it is "from" index
if (fieldIndex === fromIndex) {
fieldStateMap.set(fieldName, fieldState);
// Otherwise replace state of previous field with state of current
// field
} else {
setFieldState(form, getPrevIndexName(fieldName, fieldIndex), fieldState);
}
});
// Finally move fields with "from" index to "to" index
fieldStateMap.forEach((fieldState, fieldName) => {
setFieldState(form, getToIndexName(fieldName), fieldState);
});
// Add state of "from" field arrays to map and move all field arrays in
// between forward or backward
fieldArrayNames.forEach(fieldArrayName => {
// Get state of current field array
const fieldArrayState = getFieldArrayState(form, fieldArrayName);
// Get index of current field array
const fieldArrayIndex = getPathIndex(name, fieldArrayName);
// Add state of field to map if it is "from" index
if (fieldArrayIndex === fromIndex) {
fieldArrayStateMap.set(fieldArrayName, fieldArrayState);
// Otherwise replace state of previous field array with state of
// current field array
} else {
setFieldArrayState(form, getPrevIndexName(fieldArrayName, fieldArrayIndex), fieldArrayState);
}
});
// Finally move field arrays with "from" index to "to" index
fieldArrayStateMap.forEach((fieldArrayState, fieldArrayName) => {
setFieldArrayState(form, getToIndexName(fieldArrayName), fieldArrayState);
});
// Swap items of field array
fieldArray.items.set(prevItems => {
const nextItems = [...prevItems];
nextItems.splice(toIndex, 0, nextItems.splice(fromIndex, 1)[0]);
return nextItems;
});
// Set touched at field array and form to true
fieldArray.touched.set(true);
form.internal.touched.set(true);
// Update dirty state at field array and form
updateFieldArrayDirty(form, fieldArray);
});
}
});
}
}
/**
* Value type of the remove options.
*/
/**
* Removes a item of the field array.
*
* @param form The form of the field array.
* @param name The name of field array.
* @param options The remove options.
*/
function remove(form, name, {
at: index
}) {
// Get store of specified field array
const fieldArray = getFieldArrayStore(form, name);
// Continue if specified field array exists
if (fieldArray) {
solidJs.untrack(() => {
// Get last index of field array
const lastIndex = fieldArray.items.get().length - 1;
// Continue if specified index is valid
if (index >= 0 && index <= lastIndex) {
// Create function to filter a name
const filterName = value => value.startsWith(`${name}.`) && getPathIndex(name, value) > index;
// Create function to get previous index name
const getPrevIndexName = (fieldOrFieldArrayName, fieldOrFieldArrayIndex) => fieldOrFieldArrayName.replace(`${name}.${fieldOrFieldArrayIndex}`, `${name}.${fieldOrFieldArrayIndex - 1}`);
solidJs.batch(() => {
// Move state of each field after the removed index back by one index
getFieldNames(form).filter(filterName).sort(sortArrayPathIndex(name)).forEach(fieldName => {
setFieldState(form, getPrevIndexName(fieldName, getPathIndex(name, fieldName)), getFieldState(form, fieldName));
});
// Move state of each field array after the removed index back by one index
getFieldArrayNames(form).filter(filterName).sort(sortArrayPathIndex(name)).forEach(fieldArrayName => {
setFieldArrayState(form, getPrevIndexName(fieldArrayName, getPathIndex(name, fieldArrayName)), getFieldArrayState(form, fieldArrayName));
});
// Delete item from field array
fieldArray.items.set(prevItem => {
const nextItems = [...prevItem];
nextItems.splice(index, 1);
return nextItems;
});
// Set touched at field array and form to true
fieldArray.touched.set(true);
form.internal.touched.set(true);
// Update dirty state at field array and form
updateFieldArrayDirty(form, fieldArray);
// Validate field array if necessary
validateIfRequired(form, fieldArray, name, {
on: ['touched', 'input']
});
});
}
});
}
}
/**
* Value type of the replace options.
*/
/**
* Replaces a item of the field array.
*
* @param form The form of the field array.
* @param name The name of the field array.
* @param options The replace options.
*/
function replace(form, name, options) {
// Get store of specified field array
const fieldArray = getFieldArrayStore(form, name);
// Continue if specified field array exists
if (fieldArray) {
solidJs.untrack(() => {
// Destructure options
const {
at: index
} = options;
// Get last index of field array
const lastIndex = fieldArray.items.get().length - 1;
// Continue if specified index is valid
if (index >= 0 && index <= lastIndex) {
solidJs.batch(() => {
// Replace value of field array
setFieldArrayValue(form, name, options);
// Replace item at field array
fieldArray.items.set(prevItems => {
const nextItems = [...prevItems];
nextItems[index] = getUniqueId();
return nextItems;
});
// Set touched at field array and form to true
fieldArray.touched.set(true);
form.internal.touched.set(true);
// Set dirty at field array and form to true
fieldArray.dirty.set(true);
form.internal.dirty.set(true);
});
}
});
}
}
/**
* Value type of the reset options.
*/
/**
* Resets the entire form, several fields and field arrays or a single field or
* field array.
*
* @param form The form to be reset.
* @param options The reset options.
*/
/**
* Resets the entire form, several fields and field arrays or a single field or
* field array.
*
* @param form The form to be reset.
* @param name The field or field array to be reset.
* @param options The reset options.
*/
/**
* Resets the entire form, several fields and field arrays or a single field or
* field array.
*
* @param form The form to be reset.
* @param names The fields and field arrays to be reset.
* @param options The reset options.
*/
function reset(form, arg2, arg3) {
// Filter names between field and field arrays
const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2, false);
// Check if only a single field should be reset
const resetSingleField = typeof arg2 === 'string' && fieldNames.length === 1;
// Check if entire form should be reset
const resetEntireForm = !resetSingleField && !Array.isArray(arg2);
// Get options object
const options = getOptions(arg2, arg3);
// Destructure options and set default values
const {
initialValue,
initialValues,
keepResponse = false,
keepSubmitCount = false,
keepSubmitted = false,
keepValues = false,
keepDirtyValues = false,
keepItems = false,
keepDirtyItems = false,
keepErrors = false,
keepTouched = false,
keepDirty = false
} = options;
solidJs.batch(() => solidJs.untrack(() => {
// Reset state of each field
fieldNames.forEach(name => {
// Get store of specified field
const field = getFieldStore(form, name);
// Reset initial value if necessary
if (resetSingleField ? 'initialValue' in options : initialValues) {
field.initialValue.set(() => resetSingleField ? initialValue : getPathValue(name, initialValues));
}
// Check if dirty value should be kept
const keepDirtyValue = keepDirtyValues && field.dirty.get();
// Reset input if it is not to be kept
if (!keepValues && !keepDirtyValue) {
field.startValue.set(field.initialValue.get);
field.value.set(field.initialValue.get);
// Reset file inputs manually, as they can't be controlled
field.elements.get().forEach(element => {
if (element.type === 'file') {
element.value = '';
}
});
}
// Reset touched if it is not to be kept
if (!keepTouched) {
field.touched.set(false);
}
// Reset dirty if it is not to be kept
if (!keepDirty && !keepValues && !keepDirtyValue) {
field.dirty.set(false);
}
// Reset error if it is not to be kept
if (!keepErrors) {
field.error.set('');
}
});
// Reset state of each field array
fieldArrayNames.forEach(name => {
// Get store of specified field array
const fieldArray = getFieldArrayStore(form, name);
// Check if current dirty items should be kept
const keepCurrentDirtyItems = keepDirtyItems && fieldArray.dirty.get();
// Reset initial items and items if it is not to be kept
if (!keepItems && !keepCurrentDirtyItems) {
if (initialValues) {
fieldArray.initialItems.set(getPathValue(name, initialValues)?.map(() => getUniqueId()) || []);
}
fieldArray.startItems.set([...fieldArray.initialItems.get()]);
fieldArray.items.set([...fieldArray.initialItems.get()]);
}
// Reset touched if it is not to be kept
if (!keepTouched) {
fieldArray.touched.set(false);
}
// Reset dirty if it is not to be kept
if (!keepDirty && !keepItems && !keepCurrentDirtyItems) {
fieldArray.dirty.set(false);
}
// Reset error if it is not to be kept
if (!keepErrors) {
fieldArray.error.set('');
}
});
// Reset state of form if necessary
if (resetEntireForm) {
// Reset response if it is not to be kept
if (!keepResponse) {
form.internal.response.set({});
}
// Reset submit count if it is not to be kept
if (!keep