UNPKG

@modular-forms/solid

Version:

The modular and type-safe form library for SolidJS

1,708 lines (1,505 loc) 78.4 kB
'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