@formulier/core
Version:
Simple, performant form library
132 lines (131 loc) • 4.6 kB
JavaScript
import { getPath, isEqual, removeKey, setKey, setPath } from './state-utils.js';
class Formulier {
store;
instances = {};
hasMounted = false;
constructor({ initialValues }) {
this.store = new Store({
values: initialValues,
validators: {},
errors: {},
touched: {},
submitCount: 0,
});
}
setValues = (values) => {
this.store.setState(state => ({ ...state, values }));
};
setFieldErrors = (fieldErrors) => {
this.store.batch(() => {
this.store.setState(state => ({ ...state, errors: {} }));
for (const [name, error] of Object.entries(fieldErrors)) {
this.store.setState(state => ({ ...state, errors: setKey(state.errors, name, error) }));
}
});
};
validateFields = () => {
const { validators, values } = this.store.getState();
const fieldErrors = Object.fromEntries(Object.entries(validators).map(([name, validate]) => {
const value = getPath(values, name, null);
const error = validate?.(value) || null;
return [name, error];
}));
this.setFieldErrors(fieldErrors);
const noErrors = Object.values(this.store.getState().errors).every(value => value == null);
return noErrors;
};
validateField = (name) => {
const { validators, values } = this.store.getState();
const validate = validators[name];
const value = getPath(values, name, null);
const error = validate?.(value) || null;
this.store.setState(state => ({ ...state, errors: setKey(state.errors, name, error) }));
return !!validate && !error;
};
registerField = (name, validate) => {
this.store.setState(state => ({
...state,
values: setPath(state.values, name, getPath(state.values, name, null)),
validators: setKey(state.validators, name, validate || null),
errors: state.errors[name] === undefined ? setKey(state.errors, name, null) : state.errors,
touched: state.touched[name] === undefined ? setKey(state.touched, name, false) : state.touched,
}));
return () => {
this.store.setState(state => ({
...state,
values: setPath(state.values, name, undefined),
validators: removeKey(state.validators, name),
errors: removeKey(state.errors, name),
touched: removeKey(state.touched, name),
}));
};
};
addInstance = (name, instanceId) => {
this.hasMounted = true;
this.instances[name] ||= new Set();
this.instances[name].add(instanceId);
return () => {
this.instances[name]?.delete(instanceId);
};
};
hasInstance = (name) => {
return !!this.instances[name]?.size;
};
setFieldValue = (name, value) => {
const { values } = this.store.getState();
if (isEqual(getPath(values, name), value))
return;
this.store.setState(state => ({ ...state, values: setPath(state.values, name, value) }));
};
touchField = (name, value = true) => {
const { touched } = this.store.getState();
if (touched[name] === value)
return;
this.store.setState(state => ({ ...state, touched: setKey(state.touched, name, value) }));
};
incrementSubmitCount = () => {
this.store.setState(state => ({ ...state, submitCount: state.submitCount + 1 }));
};
}
class Store {
batching = false;
flushing = 0;
listeners = new Set();
state;
constructor(state) {
this.state = state;
}
getState = () => {
return this.state;
};
setState = (updater) => {
this.state = updater(this.state);
this.flush();
};
subscribe = (listener) => {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
};
batch = (callback) => {
if (this.batching === true)
return void callback();
this.batching = true;
callback();
this.batching = false;
this.flush();
};
flush = () => {
if (this.batching === true)
return;
const state = this.getState();
const flushId = ++this.flushing;
for (const listener of this.listeners) {
if (this.flushing !== flushId)
continue;
listener(state);
}
};
}
export { Formulier };