[READ-ONLY] Mirror of https://github.com/probablykasper/niceform. Convenient form validation and typing for SvelteKit
0

Configure Feed

Select the types of activity you want to include in your feed.

Create niceform.ts

authored by

Kasper and committed by
GitHub
(Feb 1, 2025, 8:38 AM +0100) 3abd7cf6 f9b44249

+138
+138
niceform.ts
··· 1 + import { fail, type RequestEvent } from '@sveltejs/kit' 2 + import { 3 + setError, 4 + superValidate, 5 + type FormPathLeavesWithErrors, 6 + type Infer, 7 + type ValidationErrors, 8 + } from 'sveltekit-superforms' 9 + import { zod as zod_adapter } from 'sveltekit-superforms/adapters' 10 + 11 + export type FormInput = RequestEvent | Request | FormData | URLSearchParams | null | undefined 12 + export type ZodSchema = Parameters<typeof zod_adapter>[0] 13 + 14 + // For AddFormProps to get name attribute type validation, and to read errors 15 + export type PartialNiceForm = { 16 + data?: Record<string, unknown> 17 + errors?: Record<string, string[]> 18 + } | null 19 + 20 + export type AddFormProps< 21 + // The unrelated props 22 + T extends object, 23 + // Partially defined form property. Allow null because SvelteKit's form prop is nullable 24 + F extends PartialNiceForm | null, 25 + > = 26 + // Props without form 27 + | (Omit<T, 'form'> & { 28 + form?: undefined 29 + label?: string 30 + }) 31 + // Props with form 32 + | (Omit<T, 'form'> & { 33 + // Allow null because SvelteKit's form prop is nullable 34 + form: F 35 + // Get the full combined `data` property of the form. This also gets around cases where a form action returns its own custom data, in which case `data` would not be defined 36 + name: F extends { data: Record<string, unknown> } ? keyof F['data'] : never 37 + label?: string 38 + }) 39 + 40 + let id_n = 0 41 + export function sequential_num() { 42 + return id_n++ 43 + } 44 + 45 + export type NiceForm<Z extends ZodSchema> = { 46 + valid: boolean 47 + data: Infer<Z> 48 + errors: ValidationErrors<Infer<Z>> 49 + message?: string 50 + single_error_message?: string 51 + } 52 + type NiceFormServer<Z extends ZodSchema> = NiceForm<Z> & { 53 + form: NiceForm<Z> 54 + /** Adds the form as a property to the data object */ 55 + fail: <D extends Record<string, unknown> | string>( 56 + status: number, 57 + data?: D, 58 + ) => D extends string ? NiceForm<Z> & { message: D } : NiceForm<Z> & D 59 + success: <D extends Record<string, unknown> | string>( 60 + data?: D, 61 + ) => D extends string ? NiceForm<Z> & { message: D } : NiceForm<Z> & D 62 + set_error: ( 63 + name: FormPathLeavesWithErrors<Infer<Z>>, 64 + message: string, 65 + options?: Parameters<typeof setError>[3], 66 + ) => NiceForm<Z> 67 + } 68 + 69 + let error_map: Record<string, () => string> = {} 70 + 71 + export function set_error_map<K extends string>(map: Record<K, () => string>) { 72 + error_map = map 73 + return Object.fromEntries(Object.entries(map).map(([key]) => [key, key])) as Record<K, K> 74 + } 75 + 76 + async function zod<Z extends ZodSchema, D extends FormInput | undefined>( 77 + schema: Z, 78 + data?: D, 79 + ): Promise<D extends object ? NiceFormServer<Z> : NiceForm<Z>> { 80 + if (data) { 81 + const sf_form = await superValidate(data, zod_adapter(schema)) 82 + let translated_single_error_message = '' 83 + let single_error_message = '' 84 + for (const [key, field_errors] of Object.entries(sf_form.errors)) { 85 + for (let i = 0; i < field_errors.length; i++) { 86 + const msg = field_errors[i] 87 + if (error_map[msg]) { 88 + field_errors[i] = error_map[msg]() 89 + translated_single_error_message += `${key}: ${field_errors[i]}\n` 90 + } 91 + single_error_message += `${key}: ${field_errors[i]}\n` 92 + } 93 + } 94 + const form: NiceForm<Z> = { 95 + valid: sf_form.valid, 96 + data: sf_form.data, 97 + errors: sf_form.errors, 98 + single_error_message: translated_single_error_message || single_error_message || undefined, 99 + } 100 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 101 + return <any>{ 102 + ...form, 103 + form, 104 + fail(status: number, data: Record<string, unknown> | string = {}) { 105 + if (typeof data === 'string') { 106 + return fail(status, { ...form, message: data }) 107 + } 108 + return fail(status, { ...form, ...data }) 109 + }, 110 + success(data: Record<string, unknown> | string = {}) { 111 + if (typeof data === 'string') { 112 + return { ...form, message: data } 113 + } 114 + return { ...form, ...data } 115 + }, 116 + set_error( 117 + name: FormPathLeavesWithErrors<Infer<Z, 'zod'>>, 118 + message: string, 119 + options?: Parameters<typeof setError>[3], 120 + ) { 121 + const sf_form2 = setError(sf_form, name, message, options) 122 + return fail(sf_form2.status, { ...form }) 123 + }, 124 + } 125 + } else { 126 + const sf_form = await superValidate(zod_adapter(schema), { strict: true }) 127 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 128 + return <any>{ 129 + valid: sf_form.valid, 130 + data: sf_form.data, 131 + errors: sf_form.errors, 132 + } 133 + } 134 + } 135 + 136 + export const nice_form = { 137 + zod, 138 + }