mobx-easy-form
Version:
Simple and performant form library built with MobX
186 lines (157 loc) • 4.76 kB
text/typescript
import { action, observable } from "mobx";
export type ValidationFn<ValueType, ParsedType = ValueType> = (
value: ValueType
) =>
| { error?: undefined; parsed: ParsedType }
| { error: Error | string; parsed?: undefined };
type ValidationSchema<ParsedType> = {
validateSync(value: any, options: { abortEarly: boolean }): ParsedType;
};
export type CreateFieldArgs<ValueType, ParsedType = ValueType> = {
id: string;
initialValue: ValueType;
initialError?: undefined | string;
form: {
actions: {
add(field: any): any;
submit(): any;
};
};
} & (
| {
validate?: undefined;
validationSchema?: undefined;
}
| {
validate: ValidationFn<ValueType, ParsedType>;
validationSchema?: undefined;
}
| {
validate?: undefined;
validationSchema: ValidationSchema<ParsedType> | undefined;
}
);
export function createField<ValueType = any, ParsedType = ValueType>({
id,
initialValue,
initialError,
form,
...validationProps
}: CreateFieldArgs<ValueType, ParsedType>): Field<ValueType, ParsedType> {
function getValidateFunction(): ValidationFn<ValueType, ParsedType> {
if ("validate" in validationProps && validationProps.validate) {
return validationProps.validate;
}
if (
"validationSchema" in validationProps &&
validationProps.validationSchema
) {
return function validate(value: ValueType) {
if (!validationProps.validationSchema)
throw new Error("Missing validation schema");
try {
const parsed = validationProps.validationSchema.validateSync(
(value as any) === "" ? undefined : value,
{ abortEarly: true }
);
return { parsed, error: undefined };
} catch (error) {
if (error instanceof Error && error.name === "ValidationError") {
return { parsed: undefined, error };
}
throw error;
}
};
}
return function validate(value: ValueType) {
return { parsed: value as unknown as ParsedType, error: undefined };
};
}
const runValidation = getValidateFunction();
const state = observable({
id,
errorOverride: initialError,
value: initialValue,
isFocused: false,
wasEverFocused: false,
wasEverBlurred: false,
});
const computed = observable({
get parsed(): ParsedType | undefined {
const result = runValidation(state.value);
if (result.error) return undefined;
return result.parsed;
},
get isDirty() {
// TODO: Add ability to provide custom equality function.
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 err = error as any;
return String(err.message?.value ?? err.message ?? error);
}
if (error instanceof Error) {
return error.message;
}
return error;
},
get ifWasEverFocusedThenError() {
if (!state.wasEverFocused) return undefined;
if (!computed.error) return undefined;
return String(computed.error);
},
get ifWasEverBlurredThenError() {
if (!state.wasEverBlurred) return undefined;
if (!computed.error) return undefined;
return String(computed.error);
},
});
const actions = {
onFocus: action(function onFocus() {
state.isFocused = true;
state.wasEverFocused = true;
}),
onBlur: action(function onBlur() {
state.isFocused = false;
state.wasEverBlurred = true;
}),
onChange: action(function onChange(value: ValueType) {
if (state.errorOverride) state.errorOverride = undefined;
state.value = value;
}),
setError: action(function setError(value: string | undefined) {
state.errorOverride = value;
}),
};
const field = { state, computed, actions };
form.actions.add(field);
return field;
}
export interface Field<ValueType, ParsedType = ValueType> {
state: {
id: string;
errorOverride: undefined | string;
value: ValueType;
isFocused: boolean;
wasEverFocused: boolean;
wasEverBlurred: boolean;
};
computed: {
readonly parsed: ParsedType | undefined;
readonly isDirty: boolean;
readonly error: undefined | string;
readonly ifWasEverFocusedThenError: undefined | string;
readonly ifWasEverBlurredThenError: undefined | string;
};
actions: {
onFocus(): void;
onChange(value: ValueType): void;
onBlur(): void;
setError(value: string | undefined): void;
};
}