mobx-easy-form
Version:
Simple and performant form library built with MobX
1 lines • 17.3 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":[],"sources":["../src/isPromise.ts","../src/mapValues.ts","../src/createForm.ts","../src/createField.ts","../src/useMemoOne.ts","../src/useForm.ts","../src/useField.ts"],"sourcesContent":["export function isPromise<T, S>(\n value: PromiseLike<T> | S\n): value is PromiseLike<T> {\n return !!(\n value &&\n typeof value === \"object\" &&\n \"then\" in value &&\n typeof value.then === \"function\"\n );\n}\n","export function mapValues<T, R>(\n object: Record<string, T>,\n callbackFn: (value: T) => R\n) {\n return Object.fromEntries(\n Object.entries(object).map(([key, value]) => {\n return [key, callbackFn(value)];\n })\n );\n}\n","import { action, observable, runInAction } from \"mobx\";\nimport { isPromise } from \"./isPromise\";\nimport type { Field } from \"./createField\";\nimport { mapValues } from \"./mapValues\";\n\nexport type OnSubmitArg = {\n fields: Record<string, Field<unknown>>;\n rawValues: Record<string, unknown>;\n values: Record<string, unknown>;\n};\n\nexport type OnSubmitFn = (props: OnSubmitArg) => unknown;\n\nexport type CreateFormArgs = {\n onSubmit: OnSubmitFn;\n};\n\nexport type Form = ReturnType<typeof createForm>;\n\nexport function createForm({ onSubmit }: CreateFormArgs) {\n const fields = observable({}) as Record<string, Field<unknown>>;\n\n const state = observable({\n isSubmitting: false,\n valuesAtLastSubmit: undefined as undefined | string,\n submitCount: 0,\n });\n\n const computed = observable({\n get isDirty() {\n return Object.values(fields).some((field) => field.computed.isDirty);\n },\n get errorList() {\n return Object.values(fields)\n .map((field) => field.computed.error)\n .filter((error) => error !== undefined);\n },\n get isError() {\n return Object.values(fields).some((field) => !!field.computed.error);\n },\n get isValid() {\n return !this.isError;\n },\n get valueList() {\n return String(Object.values(fields).map((field) => field.state.value));\n },\n\n get isChangedSinceLastSubmit() {\n if (state.submitCount === 0) return this.isDirty;\n return this.valueList !== state.valuesAtLastSubmit;\n },\n });\n\n const actions = {\n add(field: Field<unknown>) {\n fields[field.state.id] = field;\n },\n\n submit: action(function submit() {\n state.isSubmitting = true;\n state.submitCount++;\n state.valuesAtLastSubmit = computed.valueList;\n\n for (const fieldId in fields) {\n const field = fields[fieldId];\n field.state.wasEverFocused = true;\n field.state.wasEverBlurred = true;\n }\n\n if (computed.isError) {\n state.isSubmitting = false;\n return;\n }\n\n const maybePromise = onSubmit({\n fields,\n rawValues: mapValues(fields, (field) => field.state.value),\n values: mapValues(fields, (field) => field.computed.parsed),\n });\n\n if (isPromise(maybePromise)) {\n return Promise.resolve(maybePromise).finally(() => {\n runInAction(() => {\n state.isSubmitting = false;\n });\n });\n }\n\n runInAction(() => {\n state.isSubmitting = false;\n });\n\n return maybePromise;\n }),\n };\n\n return { fields, state, computed, actions };\n}\n","import { action, observable } from \"mobx\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\nexport type ValidationFn<ValueType, ParsedType = ValueType> = (\n value: ValueType\n) =>\n | { error?: undefined; parsed: ParsedType }\n | { error: Error | string; parsed?: undefined };\n\ntype YupLikeSchema<ParsedType> = {\n validateSync(\n value: unknown,\n options?: { abortEarly?: boolean }\n ): ParsedType;\n};\n\nexport type ValidationSchema<ParsedType = unknown> =\n | StandardSchemaV1<unknown, ParsedType>\n | YupLikeSchema<ParsedType>;\n\nexport type InferParsed<Schema, Fallback> =\n Schema extends StandardSchemaV1<unknown, infer Out>\n ? Out\n : Schema extends YupLikeSchema<infer P>\n ? P\n : Fallback;\n\nexport type CreateFieldArgs<\n ValueType,\n ParsedType = ValueType,\n Schema extends ValidationSchema<ParsedType> | undefined =\n | ValidationSchema<ParsedType>\n | undefined,\n> = {\n id: string;\n initialValue: ValueType;\n initialError?: string | undefined;\n form: {\n actions: {\n add(field: Field<any, any>): void;\n submit(): unknown;\n };\n };\n} & (\n | { validate?: undefined; validationSchema?: undefined }\n | {\n validate: ValidationFn<ValueType, ParsedType>;\n validationSchema?: undefined;\n }\n | { validate?: undefined; validationSchema: Schema }\n);\n\nfunction isStandardSchema(schema: unknown): schema is StandardSchemaV1 {\n return (\n typeof schema === \"object\" && schema !== null && \"~standard\" in schema\n );\n}\n\nfunction isYupLikeSchema<T>(schema: unknown): schema is YupLikeSchema<T> {\n return (\n typeof schema === \"object\" &&\n schema !== null &&\n \"validateSync\" in schema &&\n typeof (schema as { validateSync: unknown }).validateSync === \"function\"\n );\n}\n\nexport function createField<\n ValueType,\n Schema extends ValidationSchema<unknown> | undefined = undefined,\n ParsedType = InferParsed<Schema, ValueType>,\n>(\n args: CreateFieldArgs<\n ValueType,\n ParsedType,\n Schema extends ValidationSchema<ParsedType> ? Schema : undefined\n >\n): Field<ValueType, ParsedType> {\n const { id, initialValue, initialError, form } = args;\n const validate = args.validate;\n const validationSchema = args.validationSchema as\n | ValidationSchema<ParsedType>\n | undefined;\n\n const runValidation = getValidateFunction<ValueType, ParsedType>(\n validate,\n validationSchema\n );\n\n const state = observable({\n id,\n errorOverride: initialError,\n value: initialValue,\n isFocused: false,\n wasEverFocused: false,\n wasEverBlurred: false,\n });\n\n const computed = observable({\n get parsed(): ParsedType | undefined {\n const result = runValidation(state.value);\n if (result.error) return undefined;\n return result.parsed;\n },\n\n get isDirty() {\n return JSON.stringify(state.value) !== JSON.stringify(initialValue);\n },\n\n get error(): string | undefined {\n const { error } = runValidation(state.value);\n\n if (state.errorOverride) {\n return state.errorOverride;\n }\n\n if (error instanceof Error && error.name === \"ValidationError\") {\n const msg: unknown = (error as { message: unknown }).message;\n if (\n msg !== null &&\n typeof msg === \"object\" &&\n \"value\" in msg &&\n (msg as { value: unknown }).value !== undefined\n ) {\n return String((msg as { value: unknown }).value);\n }\n if (typeof msg === \"string\") return msg;\n return String(error);\n }\n\n if (error instanceof Error) {\n return error.message;\n }\n\n return error;\n },\n\n get ifWasEverFocusedThenError(): string | undefined {\n if (!state.wasEverFocused) return undefined;\n if (!computed.error) return undefined;\n return String(computed.error);\n },\n\n get ifWasEverBlurredThenError(): string | undefined {\n if (!state.wasEverBlurred) return undefined;\n if (!computed.error) return undefined;\n return String(computed.error);\n },\n });\n\n const actions = {\n onFocus: action(function onFocus() {\n state.isFocused = true;\n state.wasEverFocused = true;\n }),\n\n onBlur: action(function onBlur() {\n state.isFocused = false;\n state.wasEverBlurred = true;\n }),\n\n onChange: action(function onChange(value: ValueType) {\n if (state.errorOverride) state.errorOverride = undefined;\n state.value = value;\n }),\n\n setError: action(function setError(value: string | undefined) {\n state.errorOverride = value;\n }),\n };\n\n const field: Field<ValueType, ParsedType> = { state, computed, actions };\n\n form.actions.add(field);\n\n return field;\n}\n\nfunction getValidateFunction<ValueType, ParsedType>(\n validate: ValidationFn<ValueType, ParsedType> | undefined,\n validationSchema: ValidationSchema<ParsedType> | undefined\n): ValidationFn<ValueType, ParsedType> {\n if (validate) return validate;\n\n if (validationSchema) {\n // Prefer Yup's `validateSync` when available — it's guaranteed sync,\n // while Standard Schema's `validate` may return a Promise (Yup itself\n // implements Standard Schema in async mode).\n if (isYupLikeSchema<ParsedType>(validationSchema)) {\n const schema = validationSchema;\n return function validateWithYup(value: ValueType) {\n try {\n const parsed = schema.validateSync(value, { abortEarly: true });\n return { parsed, error: undefined };\n } catch (error) {\n if (error instanceof Error && error.name === \"ValidationError\") {\n return { parsed: undefined, error };\n }\n throw error;\n }\n };\n }\n\n if (isStandardSchema(validationSchema)) {\n const schema = validationSchema;\n return function validateWithStandardSchema(value: ValueType) {\n const result = schema[\"~standard\"].validate(value);\n if (result instanceof Promise) {\n throw new TypeError(\n \"mobx-easy-form: Standard Schema async validation is not supported. Use a synchronous schema.\"\n );\n }\n if (result.issues) {\n const message = result.issues[0]?.message ?? \"Invalid\";\n return { parsed: undefined, error: message };\n }\n return { parsed: result.value as ParsedType, error: undefined };\n };\n }\n\n throw new TypeError(\n \"mobx-easy-form: validationSchema must implement Standard Schema or expose a `validateSync` method (Yup-like).\"\n );\n }\n\n return function passthrough(value: ValueType) {\n return { parsed: value as unknown as ParsedType, error: undefined };\n };\n}\n\nexport interface Field<ValueType, ParsedType = ValueType> {\n state: {\n id: string;\n errorOverride: undefined | string;\n value: ValueType;\n isFocused: boolean;\n wasEverFocused: boolean;\n wasEverBlurred: boolean;\n };\n computed: {\n readonly parsed: ParsedType | undefined;\n readonly isDirty: boolean;\n readonly error: undefined | string;\n readonly ifWasEverFocusedThenError: undefined | string;\n readonly ifWasEverBlurredThenError: undefined | string;\n };\n actions: {\n onFocus(): void;\n onChange(value: ValueType): void;\n onBlur(): void;\n setError(value: string | undefined): void;\n };\n}\n","import { useRef } from \"react\";\n\nfunction areInputsEqual(\n next: ReadonlyArray<unknown>,\n prev: ReadonlyArray<unknown>\n) {\n if (next.length !== prev.length) return false;\n for (let i = 0; i < next.length; i++) {\n if (next[i] !== prev[i]) return false;\n }\n return true;\n}\n\nexport function useMemoOne<T>(\n factory: () => T,\n inputs: ReadonlyArray<unknown> = []\n): T {\n const cache = useRef<{ inputs: ReadonlyArray<unknown>; value: T } | null>(\n null\n );\n\n if (cache.current === null || !areInputsEqual(inputs, cache.current.inputs)) {\n cache.current = { inputs, value: factory() };\n }\n\n return cache.current.value;\n}\n","import { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { useMemoOne } from \"./useMemoOne\";\nimport { createForm, type CreateFormArgs, type Form } from \"./createForm\";\n\nfunction useEvent<Args extends unknown[], ReturnValue>(\n handler: (...args: Args) => ReturnValue,\n) {\n const handlerRef = useRef<(...args: Args) => ReturnValue>(handler);\n\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n return useCallback((...args: Args) => handlerRef.current(...args), []);\n}\n\nexport function useForm(\n args: CreateFormArgs,\n deps: ReadonlyArray<unknown> = [],\n): Form {\n const onSubmit = useEvent(args.onSubmit);\n return useMemoOne(() => createForm({ ...args, onSubmit }), deps);\n}\n","import { useMemoOne } from \"./useMemoOne\";\nimport {\n createField,\n type CreateFieldArgs,\n type Field,\n type InferParsed,\n type ValidationSchema,\n} from \"./createField\";\n\nexport function useField<\n ValueType,\n Schema extends ValidationSchema<unknown> | undefined = undefined,\n ParsedType = InferParsed<Schema, ValueType>,\n>(\n args: CreateFieldArgs<\n ValueType,\n ParsedType,\n Schema extends ValidationSchema<ParsedType> ? Schema : undefined\n >,\n deps: ReadonlyArray<unknown> = [],\n): Field<ValueType, ParsedType> {\n return useMemoOne(() => createField(args), deps) as Field<\n ValueType,\n ParsedType\n >;\n}\n"],"mappings":"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"}