mobx-easy-form
Version:
Simple and performant form library built with MobX
247 lines (246 loc) • 7.24 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let mobx = require("mobx");
let react = require("react");
//#region src/isPromise.ts
function isPromise(value) {
return !!(value && typeof value === "object" && "then" in value && typeof value.then === "function");
}
//#endregion
//#region src/mapValues.ts
function mapValues(object, callbackFn) {
return Object.fromEntries(Object.entries(object).map(([key, value]) => {
return [key, callbackFn(value)];
}));
}
//#endregion
//#region src/createForm.ts
function createForm({ onSubmit }) {
const fields = (0, mobx.observable)({});
const state = (0, mobx.observable)({
isSubmitting: false,
valuesAtLastSubmit: void 0,
submitCount: 0
});
const computed = (0, mobx.observable)({
get isDirty() {
return Object.values(fields).some((field) => field.computed.isDirty);
},
get errorList() {
return Object.values(fields).map((field) => field.computed.error).filter((error) => error !== void 0);
},
get isError() {
return Object.values(fields).some((field) => !!field.computed.error);
},
get isValid() {
return !this.isError;
},
get valueList() {
return String(Object.values(fields).map((field) => field.state.value));
},
get isChangedSinceLastSubmit() {
if (state.submitCount === 0) return this.isDirty;
return this.valueList !== state.valuesAtLastSubmit;
}
});
return {
fields,
state,
computed,
actions: {
add(field) {
fields[field.state.id] = field;
},
submit: (0, mobx.action)(function submit() {
state.isSubmitting = true;
state.submitCount++;
state.valuesAtLastSubmit = computed.valueList;
for (const fieldId in fields) {
const field = fields[fieldId];
field.state.wasEverFocused = true;
field.state.wasEverBlurred = true;
}
if (computed.isError) {
state.isSubmitting = false;
return;
}
const maybePromise = onSubmit({
fields,
rawValues: mapValues(fields, (field) => field.state.value),
values: mapValues(fields, (field) => field.computed.parsed)
});
if (isPromise(maybePromise)) return Promise.resolve(maybePromise).finally(() => {
(0, mobx.runInAction)(() => {
state.isSubmitting = false;
});
});
(0, mobx.runInAction)(() => {
state.isSubmitting = false;
});
return maybePromise;
})
}
};
}
//#endregion
//#region src/createField.ts
function isStandardSchema(schema) {
return typeof schema === "object" && schema !== null && "~standard" in schema;
}
function isYupLikeSchema(schema) {
return typeof schema === "object" && schema !== null && "validateSync" in schema && typeof schema.validateSync === "function";
}
function createField(args) {
const { id, initialValue, initialError, form } = args;
const validate = args.validate;
const validationSchema = args.validationSchema;
const runValidation = getValidateFunction(validate, validationSchema);
const state = (0, mobx.observable)({
id,
errorOverride: initialError,
value: initialValue,
isFocused: false,
wasEverFocused: false,
wasEverBlurred: false
});
const computed = (0, mobx.observable)({
get parsed() {
const result = runValidation(state.value);
if (result.error) return void 0;
return result.parsed;
},
get isDirty() {
return JSON.stringify(state.value) !== JSON.stringify(initialValue);
},
get error() {
const { error } = runValidation(state.value);
if (state.errorOverride) return state.errorOverride;
if (error instanceof Error && error.name === "ValidationError") {
const msg = error.message;
if (msg !== null && typeof msg === "object" && "value" in msg && msg.value !== void 0) return String(msg.value);
if (typeof msg === "string") return msg;
return String(error);
}
if (error instanceof Error) return error.message;
return error;
},
get ifWasEverFocusedThenError() {
if (!state.wasEverFocused) return void 0;
if (!computed.error) return void 0;
return String(computed.error);
},
get ifWasEverBlurredThenError() {
if (!state.wasEverBlurred) return void 0;
if (!computed.error) return void 0;
return String(computed.error);
}
});
const field = {
state,
computed,
actions: {
onFocus: (0, mobx.action)(function onFocus() {
state.isFocused = true;
state.wasEverFocused = true;
}),
onBlur: (0, mobx.action)(function onBlur() {
state.isFocused = false;
state.wasEverBlurred = true;
}),
onChange: (0, mobx.action)(function onChange(value) {
if (state.errorOverride) state.errorOverride = void 0;
state.value = value;
}),
setError: (0, mobx.action)(function setError(value) {
state.errorOverride = value;
})
}
};
form.actions.add(field);
return field;
}
function getValidateFunction(validate, validationSchema) {
if (validate) return validate;
if (validationSchema) {
if (isYupLikeSchema(validationSchema)) {
const schema = validationSchema;
return function validateWithYup(value) {
try {
return {
parsed: schema.validateSync(value, { abortEarly: true }),
error: void 0
};
} catch (error) {
if (error instanceof Error && error.name === "ValidationError") return {
parsed: void 0,
error
};
throw error;
}
};
}
if (isStandardSchema(validationSchema)) {
const schema = validationSchema;
return function validateWithStandardSchema(value) {
const result = schema["~standard"].validate(value);
if (result instanceof Promise) throw new TypeError("mobx-easy-form: Standard Schema async validation is not supported. Use a synchronous schema.");
if (result.issues) return {
parsed: void 0,
error: result.issues[0]?.message ?? "Invalid"
};
return {
parsed: result.value,
error: void 0
};
};
}
throw new TypeError("mobx-easy-form: validationSchema must implement Standard Schema or expose a `validateSync` method (Yup-like).");
}
return function passthrough(value) {
return {
parsed: value,
error: void 0
};
};
}
//#endregion
//#region src/useMemoOne.ts
function areInputsEqual(next, prev) {
if (next.length !== prev.length) return false;
for (let i = 0; i < next.length; i++) if (next[i] !== prev[i]) return false;
return true;
}
function useMemoOne(factory, inputs = []) {
const cache = (0, react.useRef)(null);
if (cache.current === null || !areInputsEqual(inputs, cache.current.inputs)) cache.current = {
inputs,
value: factory()
};
return cache.current.value;
}
//#endregion
//#region src/useForm.ts
function useEvent(handler) {
const handlerRef = (0, react.useRef)(handler);
(0, react.useLayoutEffect)(() => {
handlerRef.current = handler;
});
return (0, react.useCallback)((...args) => handlerRef.current(...args), []);
}
function useForm(args, deps = []) {
const onSubmit = useEvent(args.onSubmit);
return useMemoOne(() => createForm({
...args,
onSubmit
}), deps);
}
//#endregion
//#region src/useField.ts
function useField(args, deps = []) {
return useMemoOne(() => createField(args), deps);
}
//#endregion
exports.createField = createField;
exports.createForm = createForm;
exports.useField = useField;
exports.useForm = useForm;
//# sourceMappingURL=index.cjs.map