state-management-utilities
Version:
State management utilities
323 lines (322 loc) • 11.3 kB
JavaScript
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;
}
}