sveltekit-superforms
Advanced tools
Comparing version 0.6.0-rc.2 to 0.6.0-rc.3
@@ -77,3 +77,3 @@ import { type MaybePromise, type SubmitFunction } from '$app/forms'; | ||
timeout: Readable<boolean>; | ||
fields: Readable<FormFields<T>>; | ||
fields: FormFields<T>; | ||
firstError: Readable<{ | ||
@@ -80,0 +80,0 @@ path: string[]; |
@@ -10,3 +10,5 @@ import { enhance, applyAction } from '$app/forms'; | ||
import { stringify } from 'devalue'; | ||
import { mapErrors, unwrapZodType, traversePath, findErrors } from '../entity'; | ||
import { mapErrors, traversePath, findErrors } from '../entity'; | ||
import { unwrapZodType } from './entity'; | ||
unwrapZodType; | ||
var FetchStatus; | ||
@@ -147,8 +149,17 @@ (function (FetchStatus) { | ||
options.taintedMessage = undefined; | ||
// Check client validation on data change | ||
let previousForm; | ||
function enableTaintedMessage() { | ||
options.taintedMessage = _taintedMessage; | ||
if (options.taintedMessage) { | ||
previousForm = structuredClone(initialForm.data); | ||
} | ||
} | ||
function rebind(form, untaint, message) { | ||
if (untaint) | ||
if (untaint) { | ||
if (options.taintedMessage) { | ||
previousForm = structuredClone(form.data); | ||
} | ||
Tainted.set(undefined); | ||
} | ||
Data.set(form.data); | ||
@@ -339,2 +350,13 @@ Message.set(message ?? form.message); | ||
} | ||
const unsubscriptions = []; | ||
onDestroy(() => { | ||
/* | ||
console.log( | ||
'🚀 ~ file: index.ts:565 ~ onDestroy ~ removing', | ||
unsubscriptions.length, | ||
'subscriptions' | ||
); | ||
*/ | ||
unsubscriptions.forEach((unsub) => unsub()); | ||
}); | ||
if (browser) { | ||
@@ -344,5 +366,3 @@ beforeNavigate((nav) => { | ||
const tainted = get(Tainted); | ||
if (tainted && | ||
tainted.length && | ||
!window.confirm(options.taintedMessage)) { | ||
if (tainted && !window.confirm(options.taintedMessage)) { | ||
nav.cancel(); | ||
@@ -352,8 +372,9 @@ } | ||
}); | ||
// Check client validation on data change | ||
let previousForm = structuredClone(initialForm.data); | ||
let loaded = false; | ||
Data.subscribe(async (data) => { | ||
if (!loaded) { | ||
loaded = true; | ||
// Prevent client validation on first page load | ||
// (when it recieives data from the server) | ||
let hasNewData = true; | ||
unsubscriptions.push(Data.subscribe(async (data) => { | ||
//console.log('🚀 ~ Data.subscribe ~ hasNewData:', formId, hasNewData); | ||
if (hasNewData) { | ||
hasNewData = false; | ||
return; | ||
@@ -364,30 +385,34 @@ } | ||
} | ||
// Whether submitted or not, it will be returned here. | ||
// Clone the data so it won't be marked as tainted. | ||
previousForm = structuredClone(data); | ||
}); | ||
})); | ||
// Need to subscribe to catch page invalidation. | ||
if (options.applyAction) { | ||
onDestroy(page.subscribe(async (p) => { | ||
unsubscriptions.push(page.subscribe(async (newValidation) => { | ||
function error(type) { | ||
throw new SuperFormError(`No form data found in ${type}. Make sure you return { form } in the form actions and load function.`); | ||
throw new SuperFormError(`No form data found in ${type}. Make sure you return { form } in form actions and load functions.`); | ||
} | ||
if (p.form && typeof p.form === 'object') { | ||
const forms = findForms(p.form); | ||
const untaint = newValidation.status >= 200 && newValidation.status < 300; | ||
if (newValidation.form && typeof newValidation.form === 'object') { | ||
const forms = findForms(newValidation.form); | ||
if (!forms.length) | ||
error('$page.form (ActionData)'); | ||
for (const newForm of forms) { | ||
//console.log('🚀~ ActionData ~ newForm:', newForm.id); | ||
if (newForm === form || newForm.id !== formId) | ||
continue; | ||
await _update(newForm, p.status >= 200 && p.status < 300); | ||
// Prevent client validation from overriding the new server errors. | ||
hasNewData = true; | ||
await _update(newForm, untaint); | ||
} | ||
} | ||
else if (p.data && typeof p.data === 'object') { | ||
const forms = findForms(p.data); | ||
else if (newValidation.data && | ||
typeof newValidation.data === 'object') { | ||
const forms = findForms(newValidation.data); | ||
// It's a page reload, redirect or error/failure, | ||
// so don't trigger any events, just update the data. | ||
for (const newForm of forms) { | ||
//console.log('🚀 ~ PageData ~ newForm:', newForm.id); | ||
if (newForm === form || newForm.id !== formId) | ||
continue; | ||
rebind(newForm, p.status >= 200 && p.status < 300); | ||
hasNewData = true; | ||
rebind(newForm, untaint); | ||
} | ||
@@ -402,14 +427,30 @@ } | ||
} | ||
function fieldStore(fieldName) { | ||
const store = writable(); | ||
Data.subscribe((data) => { | ||
function fieldStore(parentStore, fieldName, value) { | ||
if (!browser) | ||
return writable(value); | ||
const store = writable(value); | ||
const unsub2 = parentStore.subscribe((data) => { | ||
store.set(data[fieldName]); | ||
}); | ||
return { | ||
subscribe: store.subscribe, | ||
subscribe(...params) { | ||
//console.log('~ fieldStore ~ subscribe', fieldName); | ||
const unsub = store.subscribe(...params); | ||
return () => { | ||
//console.log('~ fieldStore ~ unsubscribe', fieldName); | ||
unsub(); | ||
unsub2(); | ||
}; | ||
}, | ||
//subscribe: store.subscribe, | ||
set: (value) => { | ||
Data.set({ ...get(Data), [fieldName]: value }); | ||
//console.log('~ fieldStore ~ set value for', fieldName, value); | ||
parentStore.update((form) => ({ | ||
...form, | ||
[fieldName]: value | ||
})); | ||
}, | ||
update: (cb) => { | ||
Data.set((form) => ({ | ||
//console.log('~ fieldStore ~ update value for', fieldName); | ||
parentStore.update((form) => ({ | ||
...form, | ||
@@ -421,2 +462,15 @@ [fieldName]: cb(form[fieldName]) | ||
} | ||
const Fields = Object.fromEntries(Object.keys(initialForm.data).map((key) => [ | ||
key, | ||
Fields_create(key, initialForm) | ||
])); | ||
function Fields_create(key, validation) { | ||
return { | ||
name: key, | ||
value: fieldStore(Data, key, validation.data[key]), | ||
errors: fieldStore(Errors, key, validation.errors[key]), | ||
constraints: fieldStore(Constraints, key, validation.constraints[key]), | ||
type: validation.meta?.types[key] | ||
}; | ||
} | ||
return { | ||
@@ -428,14 +482,3 @@ form: Data, | ||
meta: derived(Meta, ($m) => $m), | ||
fields: derived([Errors, Constraints, Meta], ([$E, $C, $M], set) => { | ||
set(Object.fromEntries(Object.keys(get(Data)).map((key) => [ | ||
key, | ||
{ | ||
name: key, | ||
value: fieldStore(key), | ||
errors: $E[key], | ||
constraints: $C[key], | ||
type: $M?.types[key] | ||
} | ||
]))); | ||
}), | ||
fields: Fields, | ||
tainted: derived(Tainted, ($t) => $t), | ||
@@ -442,0 +485,0 @@ valid: derived(Valid, ($s) => $s), |
import type { ValidationErrors } from '.'; | ||
import { type ZodTypeAny, type AnyZodObject, type ZodFormattedError } from 'zod'; | ||
import type { ZodTypeAny, AnyZodObject, ZodFormattedError } from 'zod'; | ||
export type ZodTypeInfo = { | ||
@@ -10,3 +10,2 @@ zodType: ZodTypeAny; | ||
}; | ||
export declare function unwrapZodType(zodType: ZodTypeAny): ZodTypeInfo; | ||
export declare function mapErrors<T extends AnyZodObject>(obj: ZodFormattedError<unknown>): ValidationErrors<T>; | ||
@@ -13,0 +12,0 @@ export declare function findErrors(errors: ValidationErrors<AnyZodObject>, path?: string[]): { |
@@ -1,39 +0,1 @@ | ||
import { ZodDefault, ZodNullable, ZodOptional, ZodEffects } from 'zod'; | ||
export function unwrapZodType(zodType) { | ||
let _wrapped = true; | ||
let isNullable = false; | ||
let isOptional = false; | ||
let hasDefault = false; | ||
let defaultValue = undefined; | ||
//let i = 0; | ||
while (_wrapped) { | ||
//console.log(' '.repeat(++i * 2) + zodType.constructor.name); | ||
if (zodType instanceof ZodNullable) { | ||
isNullable = true; | ||
zodType = zodType.unwrap(); | ||
} | ||
else if (zodType instanceof ZodDefault) { | ||
hasDefault = true; | ||
defaultValue = zodType._def.defaultValue(); | ||
zodType = zodType._def.innerType; | ||
} | ||
else if (zodType instanceof ZodOptional) { | ||
isOptional = true; | ||
zodType = zodType.unwrap(); | ||
} | ||
else if (zodType instanceof ZodEffects) { | ||
zodType = zodType._def.schema; | ||
} | ||
else { | ||
_wrapped = false; | ||
} | ||
} | ||
return { | ||
zodType, | ||
isNullable, | ||
isOptional, | ||
hasDefault, | ||
defaultValue | ||
}; | ||
} | ||
export function mapErrors(obj) { | ||
@@ -40,0 +2,0 @@ const output = {}; |
@@ -44,7 +44,7 @@ import type { z, AnyZodObject, ZodArray, ZodObject } from 'zod'; | ||
export type FormField<T extends AnyZodObject, Property extends keyof z.infer<T>> = { | ||
name: string; | ||
readonly name: string; | ||
value: Writable<z.infer<T>[Property]>; | ||
errors?: ValidationErrors<T>[Property]; | ||
constraints?: InputConstraints<T>[Property]; | ||
type?: string; | ||
errors?: Writable<ValidationErrors<T>[Property]>; | ||
constraints?: Writable<InputConstraints<T>[Property]>; | ||
readonly type?: string; | ||
}; | ||
@@ -51,0 +51,0 @@ export type FormFields<T extends AnyZodObject> = { |
import { type InputConstraints } from '..'; | ||
import { type ZodTypeInfo } from '../entity'; | ||
import { z, type AnyZodObject, ZodDefault, ZodNullable, ZodOptional, ZodEffects } from 'zod'; | ||
import type { ZodTypeInfo } from '../entity'; | ||
import { z, type ZodTypeAny, type AnyZodObject, ZodDefault, ZodNullable, ZodOptional, ZodEffects } from 'zod'; | ||
export type UnwrappedEntity<T> = T extends ZodOptional<infer U> ? UnwrappedEntity<U> : T extends ZodDefault<infer U> ? UnwrappedEntity<U> : T extends ZodNullable<infer U> ? UnwrappedEntity<U> : T extends ZodEffects<infer U> ? UnwrappedEntity<U> : T; | ||
@@ -17,2 +17,3 @@ type EntityRecord<T extends AnyZodObject, K> = Record<keyof z.infer<T>, K>; | ||
}; | ||
export declare function unwrapZodType(zodType: ZodTypeAny): ZodTypeInfo; | ||
export declare function entityHash<T extends AnyZodObject>(meta: EntityMetaData<T>): string; | ||
@@ -19,0 +20,0 @@ export declare function entityData<T extends AnyZodObject>(schema: T): Entity<T>; |
import { SuperFormError } from '..'; | ||
import { unwrapZodType } from '../entity'; | ||
import { z, ZodDefault, ZodNullable, ZodOptional, ZodString, ZodNumber, ZodBoolean, ZodDate, ZodArray, ZodEffects, ZodBigInt, ZodObject, ZodSymbol } from 'zod'; | ||
export function unwrapZodType(zodType) { | ||
let _wrapped = true; | ||
let isNullable = false; | ||
let isOptional = false; | ||
let hasDefault = false; | ||
let defaultValue = undefined; | ||
//let i = 0; | ||
while (_wrapped) { | ||
//console.log(' '.repeat(++i * 2) + zodType.constructor.name); | ||
if (zodType instanceof ZodNullable) { | ||
isNullable = true; | ||
zodType = zodType.unwrap(); | ||
} | ||
else if (zodType instanceof ZodDefault) { | ||
hasDefault = true; | ||
defaultValue = zodType._def.defaultValue(); | ||
zodType = zodType._def.innerType; | ||
} | ||
else if (zodType instanceof ZodOptional) { | ||
isOptional = true; | ||
zodType = zodType.unwrap(); | ||
} | ||
else if (zodType instanceof ZodEffects) { | ||
zodType = zodType._def.schema; | ||
} | ||
else { | ||
_wrapped = false; | ||
} | ||
} | ||
return { | ||
zodType, | ||
isNullable, | ||
isOptional, | ||
hasDefault, | ||
defaultValue | ||
}; | ||
} | ||
// https://stackoverflow.com/a/8831937/70894 | ||
@@ -5,0 +41,0 @@ function hashCode(str) { |
import { fail, json } from '@sveltejs/kit'; | ||
import { parse, stringify } from 'devalue'; | ||
import { SuperFormError } from '..'; | ||
import { entityData, valueOrDefault } from './entity'; | ||
import { traversePath, unwrapZodType } from '../entity'; | ||
import { entityData, unwrapZodType, valueOrDefault } from './entity'; | ||
import { traversePath } from '../entity'; | ||
import { z, ZodObject, ZodAny, ZodString, ZodNumber, ZodBoolean, ZodDate, ZodLiteral, ZodUnion, ZodArray, ZodBigInt, ZodEnum, ZodNativeEnum, ZodSymbol, ZodEffects } from 'zod'; | ||
@@ -7,0 +7,0 @@ import { mapErrors } from '../entity'; |
{ | ||
"name": "sveltekit-superforms", | ||
"version": "0.6.0-rc.2", | ||
"version": "0.6.0-rc.3", | ||
"author": "Andreas Söderlund <ciscoheat@gmail.com> (https://blog.encodeart.dev)", | ||
@@ -5,0 +5,0 @@ "description": "Supercharge your SvelteKit forms with this powerhouse of a library!", |
100608
21
1770