UNPKG

state-management-utilities

Version:
263 lines (262 loc) 9.43 kB
import React from "react"; import { ReactStateManagerStore } from "./store"; export class ReactStateManagerForm { _config; _KEYS = []; fields = this.KEYS.reduce((acc, key) => { acc[key] = { data: this._data.entities[key], error: this._errors.entities[key], touched: this._touched.entities[key], modified: this._modified.entities[key], getValues() { return { data: this.data.value, error: this.error.value, touched: this.touched.value, modified: this.modified.value, }; }, }; return acc; }, {}); get KEYS() { return [...this._KEYS]; } get meta() { // TODO: Clone? return (this._config.meta ?? {}); } get hooks() { return this._hooks; } _truthyValues; _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) { if (newValues.data) { this._data.value = newValues.data; } if (newValues.errors) { this._errors.value = newValues.errors; } if (newValues.touched) { this._touched.value = newValues.touched; } if (newValues.modified) { this._modified.value = newValues.modified; } } _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; } setAllAsModified() { this._modified.value = this._truthyValues; } 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 fullFill() { 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; }, {}); const undefinedValues = this._KEYS.reduce((acc, key) => { acc[key] = undefined; return acc; }, {}); this._data = new ReactStateManagerStore(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 ReactStateManagerStore(undefinedValues, `${this._config.uid}/errors`, this._config.errors); this._touched = new ReactStateManagerStore(undefinedValues, `${this._config.uid}/touched`, this._config.touched); this._modified = new ReactStateManagerStore(undefinedValues, `${this._config.uid}/modified`, this._config.modified); } } let counter = 0; export function form(initialValues, config) { return new ReactStateManagerForm(initialValues, config); }