mobx-react-form
Version:
Reactive MobX Form State Management
210 lines (173 loc) • 5.75 kB
text/typescript
import { action, computed, observable, makeObservable, autorun, runInAction } from "mobx";
import _ from "lodash";
import Base from "./Base";
import Validator from "./Validator";
import State from "./State";
import Field from "./Field";
import ValidatorInterface from "./models/ValidatorInterface";
import { FieldInterface, FieldConstructor } from "./models/FieldInterface";
import { FormInterface, FieldsDefinitions, FormConfig } from "./models/FormInterface";
import { FieldPropsEnum } from "./models/FieldProps";
import { OptionsEnum } from "./models/OptionsModel";
export default class Form extends Base implements FormInterface {
name: string;
path = null;
validator: ValidatorInterface;
debouncedValidation: any = null;
constructor(
setup: FieldsDefinitions = {},
{
name = "",
options = {},
plugins = {},
bindings = {},
hooks = {},
handlers = {},
}: FormConfig = {}
) {
super();
makeObservable(this, {
fields: observable,
validatedValues: computed,
error: computed,
hasError: computed,
isValid: computed,
isPristine: computed,
isDirty: computed,
isDefault: computed,
isEmpty: computed,
focused: computed,
touched: computed,
disabled: computed,
// init: action,
invalidate: action,
clear: action,
reset: action,
resetValidation: action,
});
this.name = name;
runInAction(() => (this.$hooks = hooks));
runInAction(() => (this.$handlers = handlers));
// load data from initializers methods
const initial = _.each(
{
setup,
options,
plugins,
bindings,
},
(val, key) =>
(typeof (this as any)[key] === 'function')
? _.merge(val, (this as any)[key].apply(this, [this]))
: val
);
// setup hooks & handlers from initialization methods
runInAction(() => Object.assign(this.$hooks, (this as any).hooks?.apply(this, [this])));
runInAction(() => Object.assign(this.$handlers, (this as any).handlers?.apply(this, [this])));
this.state = new State({
form: this,
initial: initial.setup,
options: initial.options,
bindings: initial.bindings,
});
this.validator = new Validator({
form: this,
plugins: initial.plugins,
});
this.initFields(initial.setup);
this.debouncedValidation = _.debounce(
this.validate,
this.state.options.get(OptionsEnum.validationDebounceWait),
this.state.options.get(OptionsEnum.validationDebounceOptions)
);
// execute validation on form initialization
this.state.options.get(OptionsEnum.validateOnInit)
&& this.validator.validate({
showErrors: this.state.options.get(OptionsEnum.showErrorsOnInit),
});
this.execHook(FieldPropsEnum.onInit);
// handle Form onChange Hook
autorun(() => this.$changed && this.execHook(FieldPropsEnum.onChange));
}
/* ------------------------------------------------------------------ */
/* COMPUTED */
get validatedValues(): object {
const data: any = {};
this.each(($field: any) => (data[$field.path] = $field.validatedValue));
return data;
}
get error(): string | null {
return this.validator.error;
}
get hasError(): boolean {
return !!this.validator.error || this.check(FieldPropsEnum.hasError, true);
}
get isValid(): boolean {
return !this.validator.error && this.check(FieldPropsEnum.isValid, true);
}
get isPristine(): boolean {
return this.check(FieldPropsEnum.isPristine, true);
}
get isDirty(): boolean {
return this.check(FieldPropsEnum.isDirty, true);
}
get isDefault(): boolean {
return this.check(FieldPropsEnum.isDefault, true);
}
get isEmpty(): boolean {
return this.check(FieldPropsEnum.isEmpty, true);
}
get focused(): boolean {
return this.check(FieldPropsEnum.focused, true);
}
get touched(): boolean {
return this.check(FieldPropsEnum.touched, true);
}
get disabled(): boolean {
return this.check(FieldPropsEnum.disabled, true);
}
makeField(data: FieldConstructor, FieldClass: typeof Field = Field) {
return new FieldClass(data);
}
/** DEPRECATED
Init Form Fields and Nested Fields
init($fields: any = null): void {
_.set(this, "fields", observable.map({}));
this.state.initial.props.values = $fields; // eslint-disable-line
this.state.current.props.values = $fields; // eslint-disable-line
this.initFields({
fields: $fields || this.state.struct(),
});
}
*/
invalidate(message: string | null = null, deep: boolean = true): void {
this.debouncedValidation.cancel();
this.validator.error = message || this.state.options.get(OptionsEnum.defaultGenericError) || true;
deep && this.each((field: FieldInterface) => field.debouncedValidation.cancel());
}
showErrors(show: boolean = true): void {
this.each((field: FieldInterface) => field.showErrors(show));
}
resetValidation(deep: boolean = true): void {
this.validator.error = null;
deep && this.each((field: FieldInterface) => field.resetValidation(deep));
}
/**
Clear Form Fields
*/
clear(deep: boolean = true, execHook: boolean = true): void {
execHook && this.execHook(FieldPropsEnum.onClear);
this.$touched = false;
this.$changed = 0;
deep && this.each((field: FieldInterface) => field.clear(deep));
}
/**
Reset Form Fields
*/
reset(deep: boolean = true, execHook: boolean = true): void {
execHook && this.execHook(FieldPropsEnum.onReset);
this.$touched = false;
this.$changed = 0;
deep && this.each((field: FieldInterface) => field.reset(deep));
}
}