UNPKG

state-management-utilities

Version:
323 lines (322 loc) 11.3 kB
import React from "react"; import { ReactStoreManager } from "./store"; export class ReactFormManager { _config; _KEYS = []; _fields; get fields() { return this._fields; } get KEYS() { return [...this._KEYS]; } get meta() { // TODO: Clone? return (this._config.meta ?? {}); } get hooks() { return this._hooks; } _truthyValues; _falsyValues; _data; get data() { return this._data.entities; } _errors; get errors() { return this._errors.entities; } _touched; get touched() { return this._touched.entities; } _modified; get modified() { return this._modified.entities; } get value() { return { data: this._data.value, errors: this._errors.value, touched: this._touched.value, modified: this._modified.value, }; } set value(newValues) { const previousValues = this.value; if (newValues.data) { this._data.value = { ...previousValues.data, ...newValues.data }; } if (newValues.errors) { this._errors.value = { ...previousValues.errors, ...newValues.errors }; } if (newValues.touched) { this._touched.value = { ...previousValues.touched, ...newValues.touched }; } if (newValues.modified) { this._modified.value = { ...previousValues.modified, ...newValues.modified, }; } } update(updater) { const previousValues = this.value; const newValues = typeof updater === "function" ? updater(previousValues) : updater; if (newValues.data) { this._data.value = { ...previousValues.data, ...newValues.data }; } if (newValues.errors) { this._errors.value = { ...previousValues.errors, ...newValues.errors }; } if (newValues.touched) { this._touched.value = { ...previousValues.touched, ...newValues.touched }; } if (newValues.modified) { this._modified.value = { ...previousValues.modified, ...newValues.modified, }; } return this; } _validators; _hooks = Object.freeze({ useField: (name) => { const [touched, setTouched] = this._touched.entities[name].hooks.useState(); const [value, _setValue] = this._data.entities[name].hooks.useState(); const [error, setError] = this._errors.entities[name].hooks.useState(); const [modified, setModified] = this._modified.entities[name].hooks.useState(); const setValue = React.useCallback((newValue) => { // Error should be cleared first. The data change would trigger validation and subsequently result in new error state. If we clear it after assigning a new value, the validation would be cleared. this._errors.entities[name].value = undefined; this._data.entities[name].value = newValue; this._modified.entities[name].value = true; this._touched.entities[name].value = true; }, [name]); const setAsTouched = React.useCallback(() => { this._touched.entities[name].value = true; }, [name]); const setAsModified = React.useCallback(() => { this._modified.entities[name].value = true; }, [name]); return { value, setValue, error, setError, touched, setTouched, modified, setModified, setAsTouched, setAsModified, _setValue, }; }, useData: () => { return this._data.hooks.useState(); }, useErrors: () => { return this._errors.hooks.useState(); }, useTouched: () => { return this._touched.hooks.useState(); }, useModified: () => { return this._modified.hooks.useState(); }, useForm: () => { const [data, setData] = this._data.hooks.useState(); const [errors, setErrors] = this._errors.hooks.useState(); const [touched, setTouched] = this._touched.hooks.useState(); const [modified, setModified] = this._modified.hooks.useState(); return { data, setData, errors, setErrors, touched, setTouched, modified, setModified, }; }, useIsModified: () => { const [modified] = this._modified.hooks.useState(); const isModified = React.useMemo(() => Object.values(modified).some((value) => !!value), [modified]); return [isModified]; }, useIsTouched: () => { const [touched] = this._touched.hooks.useState(); const isTouched = React.useMemo(() => Object.values(touched).some((value) => !!value), [touched]); return [isTouched]; }, useHasErrors: () => { if (!this._config.hasError) throw new Error("hasError selector is not defined in the config"); const [errors] = this._errors.hooks.useState(); const hasErrors = React.useMemo(() => Object.values(errors).some(this._config.hasError), [errors]); return [hasErrors]; }, }); reset(resetValues = {}) { // Error should be cleared first. The data change would trigger validation and subsequently result in new error state. If we clear it after assigning a new value, the validation would be cleared. this._errors.value = { ...this._errors.initialValues, ...(resetValues.errors ?? {}), }; this._data.value = { ...this._data.initialValues, ...(resetValues.data ?? {}), }; this._touched.value = { ...this._touched.initialValues, ...(resetValues.touched ?? {}), }; this._modified.value = { ...this._modified.initialValues, ...(resetValues.modified ?? {}), }; this._config.onReset?.(); } setAllAsTouched() { this._touched.value = { ...this._truthyValues }; } setAllAsUntouched() { this._touched.value = { ...this._falsyValues }; } setAllAsModified() { this._modified.value = { ...this._truthyValues }; } setAllAsUnmodified() { this._modified.value = { ...this._falsyValues }; } getModifiedValues({ defaultFields, } = { defaultFields: [] }) { const items = this._modified.value; defaultFields.forEach((key) => { items[key] = true; }); const dataValues = this._data.value; const modifiedValues = {}; this.KEYS.forEach((key) => { if (items[key]) { modifiedValues[key] = dataValues[key]; } }); return modifiedValues; } get hasErrors() { if (!this._config.hasError) throw new Error("hasError selector is not defined in the config"); return Object.values(this._errors.value).some(this._config.hasError); } get isModified() { return Object.values(this._modified.value).some((value) => !!value); } get isTouched() { return Object.values(this._touched.value).some((value) => !!value); } hydrate(value) { const data = this._data.hydrate(value.data); const errors = this._errors.hydrate(value.errors ?? {}); const touched = this._touched.hydrate(value.touched ?? {}); const modified = this._modified.hydrate(value.modified ?? {}); return { update: (record) => { data.update(record); errors.update(record); touched.update(record); modified.update(record); }, value, }; } async fulfill() { await Promise.all([ this._data.fulfill(), this._errors.fulfill(), this._modified.fulfill(), this._touched.fulfill(), ]); return this; } constructor(initialValues, _config = { uid: `RSMF-#${++counter}`, }) { this._config = _config; const _KEYS = Object.keys(initialValues); this._KEYS = _KEYS; if (this._config.getValidator) { this._validators = this._KEYS.reduce((acc, key) => { acc[key] = this._config.getValidator(key, this); return acc; }, {}); } this._truthyValues = this._KEYS.reduce((acc, key) => { acc[key] = true; return acc; }, {}); this._falsyValues = this._KEYS.reduce((acc, key) => { acc[key] = false; return acc; }, {}); const undefinedValues = this._KEYS.reduce((acc, key) => { acc[key] = undefined; return acc; }, {}); this._data = new ReactStoreManager(initialValues, `${this._config.uid}/data`, this._KEYS.reduce((acc, key) => { acc[key] = { ...this._config.data?.[key], onChange: (value) => { this._validators?.[key]?.(value); this._config.data?.[key]?.onChange?.(value); }, }; return acc; }, {})); this._errors = new ReactStoreManager(undefinedValues, `${this._config.uid}/errors`, this._config.errors); this._touched = new ReactStoreManager(undefinedValues, `${this._config.uid}/touched`, this._config.touched); this._modified = new ReactStoreManager(undefinedValues, `${this._config.uid}/modified`, this._config.modified); this._fields = this.KEYS.reduce((acc, key) => { acc[key] = new Entities(this._data.entities[key], this._errors.entities[key], this._modified.entities[key], this._touched.entities[key]); return acc; }, {}); } } let counter = 0; export function form(initialValues, config) { return new ReactFormManager(initialValues, config); } class Entities { data; error; modified; touched; constructor(data, error, modified, touched) { this.data = data; this.error = error; this.modified = modified; this.touched = touched; } get values() { return { data: this.data.value, error: this.error.value, modified: this.modified.value, touched: this.touched.value, }; } set values(values) { this.data.value = values.data; this.error.value = values.error; this.modified.value = values.modified; this.touched.value = values.touched; } update(updater) { const newState = updater(this.values); this.values = newState; return this; } }