[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.

niceform / niceform.ts
5.6 kB 183 lines
1import { fail, type RequestEvent } from '@sveltejs/kit' 2import { 3 setError, 4 superValidate, 5 type FormPathLeavesWithErrors, 6 type Infer, 7 type ValidationErrors, 8} from 'sveltekit-superforms' 9import { zod as zod_adapter } from 'sveltekit-superforms/adapters' 10 11export type FormInput = RequestEvent | Request | FormData | URLSearchParams | null | undefined 12export type ZodSchema = Parameters<typeof zod_adapter>[0] 13 14// For AddFormProps to get name attribute type validation, and to read errors 15export type PartialNiceForm = { 16 data?: Record<string, unknown> 17 errors?: Record<string, string[]> 18} | null 19 20export 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 40let id_n = 0 41export function sequential_num() { 42 return id_n++ 43} 44 45export 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} 52type 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 69let error_map: Record<string, () => string> = {} 70 71export 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 76async 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), { strict: true ) 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 136export const nice_form = { 137 zod, 138} 139 140export function auto_snapshot() { 141 let container_el: HTMLElement | undefined 142 const selector = 'input[data-snapshot],textarea[data-snapshot],select[data-snapshot]' 143 return { 144 capture() { 145 if (!container_el?.querySelectorAll) { 146 throw new Error('auto_snapshot: container element not set') 147 } 148 const elements = container_el.querySelectorAll(selector) 149 const values: Record<string, string> = {} 150 for (const element of elements) { 151 if ( 152 element instanceof HTMLInputElement || 153 element instanceof HTMLSelectElement || 154 element instanceof HTMLTextAreaElement 155 ) { 156 values[element.name] = element.value 157 } 158 } 159 return values 160 }, 161 restore(value: Record<string, string>) { 162 if (!container_el?.querySelectorAll) { 163 throw new Error('auto_snapshot: container element not set') 164 } 165 const elements = container_el.querySelectorAll(selector) 166 for (const element of elements) { 167 if ( 168 (element instanceof HTMLInputElement || 169 element instanceof HTMLSelectElement || 170 element instanceof HTMLTextAreaElement) && 171 value[element.name] 172 ) { 173 element.value = value[element.name] 174 // Trigger Svelte state to updates 175 element.dispatchEvent(new Event('input')) 176 } 177 } 178 }, 179 container(node: HTMLElement) { 180 container_el = node 181 }, 182 } 183}