UNPKG

@tuentyfaiv/svelte-form

Version:

A form library for Svelte. It is built on top of Svelte and Typescript. Inspired by Formik and React Hook Form.

345 lines (344 loc) 15.4 kB
import { SchemaError, SchemaErrorList } from "./errors.js"; import { Adapter } from "../typing/stores/form.js"; const fieldsSchema = { string: "", number: 0, boolean: false, date: new Date(), file: null, }; const primitives = ["string", "number", "boolean", "date", "file"]; export function bridge(schema) { function resolveSchema(field, required = false) { if (required) { return fieldsSchema[field instanceof RegExp ? "string" : field]; } return null; } function isFieldProps(three) { const hasFieldProps = (Object.hasOwn(three, "required") && typeof three.required === "boolean") || (Object.hasOwn(three, "min") && typeof three.min === "number") || (Object.hasOwn(three, "max") && typeof three.max === "number"); return Object.hasOwn(three, "type") && ((typeof three.type === "string" && three.type !== "array") || three.type instanceof RegExp) && (primitives.includes(three.type instanceof RegExp ? "string" : three.type) || hasFieldProps); } function isArraySchema(three) { return Object.hasOwn(three, "type") && three.type === "array" && Object.hasOwn(three, "item"); } function resolveFieldProps({ type, required }) { return resolveSchema(type, required); } function resolveArray({ item, required }) { if (typeof item === "string" || item instanceof RegExp) { return required ? [] : null; } if (typeof item === "object" && !(item instanceof RegExp) && isArraySchema(item)) { return required ? [resolveArray(item)] : null; } if (typeof item === "object" && !(item instanceof RegExp) && isFieldProps(item)) { return required ? [] : null; } return required ? [] : null; } function resolveDeep(three) { if (typeof three === "string" || three instanceof RegExp) { return resolveSchema(three); } if (isFieldProps(three)) { return resolveFieldProps(three); } if (isArraySchema(three)) { return resolveArray(three); } const resolveSelf = Object.keys(three).reduce((acc, key) => ({ ...acc, [key]: resolveDeep(three[key]), }), {}); return resolveSelf; } function typeError({ type, ...config }) { return new SchemaError({ ...config, message: `The value is not a ${type}.`, reason: `The type is a ${type}, but the value is ${config.value}.`, }); } function invalidError(config) { return new SchemaError({ ...config, message: "The type is invalid.", reason: "The type is invalid, check the schema.", }); } function requiredError(config) { return new SchemaError({ ...config, message: "The field is required.", reason: "The field is required, but the value is empty.", }); } function rangeError({ label, range, ...config }) { const compare = label === "min" ? "less" : "more"; let legend = typeof config.value === "string" ? " characters" : ""; let rangeLegend = range; if (config.value instanceof Date) { legend = " date"; } if (config.value instanceof File || config.value instanceof Blob) { legend = " bytes"; } if (range instanceof Date) { rangeLegend = ` ${range.toISOString()}`; } return new SchemaError({ ...config, message: `The value is ${compare} than ${rangeLegend}${legend}.`, reason: `The value is ${compare} than ${rangeLegend}${legend}. Check the ${label} property in the schema.`, }); } class InternalAdapter extends Adapter { #schema; constructor() { super(); this.#schema = schema; Object.freeze(this); } #validateSchema = (fieldSchema, field, value) => { const correctSchema = fieldSchema instanceof RegExp || fieldSchema === "string" || fieldSchema === "number" || fieldSchema === "boolean" || fieldSchema === "date" || fieldSchema === "file"; const error = { field: String(field), schema: fieldSchema, value }; if (correctSchema) { if (typeof value === "string") { if (fieldSchema instanceof RegExp) { if (fieldSchema.test(value)) return value; return new SchemaError({ ...error, schema: fieldSchema.source, message: "The value is not match with the pattern.", reason: `Value: "${value}" is not match with the pattern: ${fieldSchema.source}`, }); } return value; } const correct = value === null || typeof value === "undefined" || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date || value instanceof File || value instanceof Blob; if (correct) return value; const dateType = fieldSchema === "date" ? "Date" : fieldSchema; const fileType = dateType === "file" ? "File | Blob" : dateType; const errorType = fileType instanceof RegExp ? "RegExp" : fileType; return typeError({ ...error, type: errorType }); } return invalidError(error); }; #validateFieldPropsSchema = (fieldSchema, field, value) => { const { type, required, min, max } = fieldSchema; const validated = this.#validateSchema(type, field, value); const isEmpty = typeof validated === "undefined" || validated === null; const correctSchema = type instanceof RegExp || type === "string" || type === "number" || type === "boolean" || type === "date" || type === "file"; const invalid = { field: String(field), schema: fieldSchema, value }; if (correctSchema) { const error = { ...invalid, value: validated }; const errorMin = { ...error, label: "min", range: min, value: validated }; const errorMax = { ...error, label: "max", range: max, value: validated }; if (required && ((typeof validated === "string" && validated.length === 0) || isEmpty)) { return requiredError(error); } if (typeof validated === "string") { if (min && validated.length < min) { return rangeError(errorMin); } if (max && validated.length > max) { return rangeError(errorMax); } } if (typeof validated === "number") { if (min && validated < min) { return rangeError(errorMin); } if (max && validated > max) { return rangeError(errorMax); } } if (validated instanceof Date) { if (min && validated.getTime() < min) { return rangeError({ ...errorMin, range: new Date(min) }); } if (max && validated.getTime() > max) { return rangeError({ ...errorMax, range: new Date(max) }); } } if ((validated instanceof File || validated instanceof Blob)) { if (min && validated.size < min) { return rangeError(errorMin); } if (max && validated.size > max) { return rangeError(errorMax); } } return validated; } return invalidError(invalid); }; #validateArraySchema = (fieldSchema, field, value) => { const { item, required, min, max } = fieldSchema; const isEmpty = typeof value === "undefined" || value === null || (Array.isArray(value) && value.length === 0); const error = { field: String(field), schema: fieldSchema, value }; if (!Array.isArray(value) && typeof value !== "undefined" && value !== null) { return invalidError(error); } if (required && isEmpty) { return requiredError(error); } if (value && min && value.length < min) { return rangeError({ ...error, label: "min", range: min, value }); } if (value && max && value.length > max) { return rangeError({ ...error, label: "max", range: max, value }); } if ((typeof item === "string" || item instanceof RegExp)) { const incorrect = value?.find((itemValue) => { const validated = this.#validateSchema(item, field, itemValue); return validated instanceof SchemaError; }); if (incorrect) { const isRegExp = item instanceof RegExp ? "string" : item; const isFile = isRegExp === "file" ? "File | Blob" : isRegExp; const isDate = isFile === "date" ? "Date" : isFile; return typeError({ ...error, type: `${isDate}[]`, value: incorrect }); } return value; } if (typeof item === "object" && !(item instanceof RegExp) && isArraySchema(item)) { const incorrect = value?.find((subArray) => { const validated = this.#validateArraySchema(item, field, subArray); return validated instanceof SchemaError; }); if (incorrect) { return typeError({ ...error, type: "array", value: incorrect }); } return value; } if (typeof item === "object" && !(item instanceof RegExp) && isFieldProps(item)) { const incorrect = value?.find((itemValue) => { const validated = this.#validateFieldPropsSchema(item, field, itemValue); return validated instanceof SchemaError; }); if (incorrect) { return typeError({ ...error, type: "array", value: incorrect }); } return value; } if (typeof item === "object" && !(item instanceof RegExp)) { const incorrect = value?.find((itemValue) => { const validated = this.#validateDeepSchema(item, field, itemValue); return validated instanceof SchemaError; }); if (incorrect) { return typeError({ ...error, type: "array", value: incorrect }); } return value; } return invalidError(error); }; #validateDeepSchema = (fieldSchema, field, value) => { if (typeof fieldSchema === "string" || fieldSchema instanceof RegExp) { return this.#validateSchema(fieldSchema, field, value); } if (isArraySchema(fieldSchema)) { return this.#validateArraySchema(fieldSchema, field, value); } if (isFieldProps(fieldSchema)) { return this.#validateFieldPropsSchema(fieldSchema, field, value); } const validateSelf = Object.keys(fieldSchema).reduce((acc, key) => ({ ...acc, [key]: this.#validateDeepSchema(fieldSchema[key], key, value[key]), }), {}); if (Object.values(validateSelf).some((item) => (item instanceof SchemaError || item instanceof SchemaErrorList))) { const errors = Object.values(validateSelf) .filter((item) => (item instanceof SchemaError || item instanceof SchemaErrorList)) .flatMap((item) => (item instanceof SchemaErrorList ? item.errors : item)); return new SchemaErrorList(errors); } return validateSelf; }; #setError = async (field, errors, error) => { let message = null; if (error instanceof SchemaError) { message = error.reason; } if (error instanceof SchemaErrorList) { message = error.errors.reduce((acc, current) => (`${acc}${current.reason}. `), ""); } await errors.update((prev) => ({ ...prev, [field]: message })); }; initial = () => { const start = Object.entries(this.#schema).reduce((acc, [key, three]) => ({ fields: { ...acc.fields, [key]: resolveDeep(three), }, errors: { ...acc.errors, [key]: null, }, }), { fields: {}, errors: {}, }); return start; }; validate = async (data) => { if (typeof data === "object" && !Array.isArray(data) && data !== null) { const guard = Object.keys(data).map((key) => (this.#validateDeepSchema(this.#schema[key], key, data[key]))); if (guard.some((item) => item instanceof SchemaError || item instanceof SchemaErrorList)) { const errors = guard .filter((item) => item instanceof SchemaError || item instanceof SchemaErrorList) .flatMap((item) => (item instanceof SchemaErrorList ? item.errors : item)); throw new SchemaErrorList(errors); } } }; field = async (field, value, errors) => { try { const guard = await this.#validateDeepSchema(this.#schema[field], field, value); if (guard instanceof SchemaError || guard instanceof SchemaErrorList) throw guard; await this.#setError(field, errors); } catch (error) { await this.#setError(field, errors, error); } }; errors = async (error, errors, handle) => { await handle?.(error); if (error instanceof SchemaErrorList) { const newErrors = error.errors.reduce((acc, err) => ({ ...acc, [err.field]: err.reason, }), {}); await errors.update((prev) => ({ ...prev, ...newErrors })); } }; } return new InternalAdapter(); }