@@ -30,14 +30,7 @@ import {
3030 type UnwrapEffects ,
3131 type ZodValidation
3232} from '../index.js' ;
33- import type {
34- z ,
35- AnyZodObject ,
36- ZodEffects ,
37- ZodArray ,
38- ZodTypeAny ,
39- ZodError
40- } from 'zod' ;
33+ import type { z , AnyZodObject , ZodEffects } from 'zod' ;
4134import { stringify } from 'devalue' ;
4235import type { FormFields } from '../index.js' ;
4336import {
@@ -48,15 +41,17 @@ import {
4841 comparePaths ,
4942 setPaths ,
5043 pathExists ,
51- type ZodTypeInfo ,
52- traversePaths ,
5344 isInvalidPath
5445} from '../traversal.js' ;
5546import { fieldProxy } from './proxies.js' ;
56- import { clone } from '../utils.js' ;
57- import { hasEffects , type Entity } from '../schemaEntity.js' ;
58- import { unwrapZodType } from '../schemaEntity.js' ;
59- import { splitPath , type StringPath } from '../stringPath.js' ;
47+ import { clearErrors , clone } from '../utils.js' ;
48+ import type { Entity } from '../schemaEntity.js' ;
49+ import {
50+ splitPath ,
51+ type StringPath ,
52+ type StringPathLeaves
53+ } from '../stringPath.js' ;
54+ import { validateField , type Validate } from './validateField.js' ;
6055
6156enum FetchStatus {
6257 Idle = 0 ,
@@ -222,19 +217,7 @@ type SuperFormEventList<T extends AnyZodObject, M> = {
222217 > [ ] ;
223218} ;
224219
225- type TaintOption = boolean | 'untaint' | 'untaint-all' ;
226-
227- type ValidateOptions < V > = Partial < {
228- value : V ;
229- update : boolean | 'errors' | 'value' ;
230- taint : TaintOption ;
231- errors : string | string [ ] ;
232- } > ;
233-
234- type Validate < T extends AnyZodObject , P extends StringPath < z . infer < T > > > = (
235- path : P ,
236- opts ?: ValidateOptions < unknown >
237- ) => Promise < string [ ] | undefined > ;
220+ export type TaintOption = boolean | 'untaint' | 'untaint-all' ;
238221
239222// eslint-disable-next-line @typescript-eslint/no-explicit-any
240223export type SuperForm < T extends ZodValidation < AnyZodObject > , M = any > = {
@@ -281,7 +264,7 @@ export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
281264 capture : ( ) => SuperFormSnapshot < UnwrapEffects < T > , M > ;
282265 restore : ( snapshot : SuperFormSnapshot < UnwrapEffects < T > , M > ) => void ;
283266
284- validate : Validate < UnwrapEffects < T > , StringPath < z . infer < T > > > ;
267+ validate : Validate < UnwrapEffects < T > , StringPathLeaves < z . infer < T > > > ;
285268} ;
286269
287270/**
@@ -978,322 +961,6 @@ function shouldSyncFlash<T extends AnyZodObject, M>(
978961 return options . syncFlashMessage ;
979962}
980963
981- function clearErrors < T extends AnyZodObject > (
982- Errors : Writable < ValidationErrors < T > > ,
983- options : {
984- undefinePath : string [ ] | null ;
985- clearFormLevelErrors : boolean ;
986- }
987- ) {
988- Errors . update ( ( $errors ) => {
989- traversePaths ( $errors , ( pathData ) => {
990- if (
991- pathData . path . length == 1 &&
992- pathData . path [ 0 ] == '_errors' &&
993- ! options . clearFormLevelErrors
994- ) {
995- return ;
996- }
997- if ( Array . isArray ( pathData . value ) ) {
998- return pathData . set ( undefined ) ;
999- }
1000- } ) ;
1001-
1002- if ( options . undefinePath )
1003- setPaths ( $errors , [ options . undefinePath ] , undefined ) ;
1004-
1005- return $errors ;
1006- } ) ;
1007- }
1008-
1009- const effectMapCache = new WeakMap < object , boolean > ( ) ;
1010-
1011- // @DCI -context
1012- async function validateField < T extends AnyZodObject , M > (
1013- path : string [ ] ,
1014- validators : FormOptions < T , M > [ 'validators' ] ,
1015- defaultValidator : FormOptions < T , M > [ 'defaultValidator' ] ,
1016- data : SuperForm < T , M > [ 'form' ] ,
1017- Errors : SuperForm < T , M > [ 'errors' ] ,
1018- tainted : SuperForm < T , M > [ 'tainted' ] ,
1019- options : ValidateOptions < unknown > = { }
1020- ) : Promise < string [ ] | undefined > {
1021- if ( options . update === undefined ) options . update = true ;
1022- if ( options . taint === undefined ) options . taint = false ;
1023-
1024- //let value = options.value;
1025- //let shouldUpdate = true;
1026- //let currentData: z.infer<T> | undefined = undefined;
1027-
1028- const Context = {
1029- value : options . value ,
1030- shouldUpdate : true ,
1031- currentData : undefined as z . infer < T > | undefined ,
1032- // Remove numeric indices, they're not used for validators.
1033- validationPath : path . filter ( ( p ) => isNaN ( parseInt ( p ) ) )
1034- } ;
1035-
1036- async function defaultValidate ( ) {
1037- if ( defaultValidator == 'clear' ) {
1038- Errors_update ( undefined ) ;
1039- }
1040- return undefined ;
1041- }
1042-
1043- function isPathTainted (
1044- path : string [ ] ,
1045- tainted : TaintedFields < AnyZodObject > | undefined
1046- ) {
1047- if ( tainted === undefined ) return false ;
1048- const leaf = traversePath ( tainted , path as FieldPath < typeof tainted > ) ;
1049- if ( ! leaf ) return false ;
1050- return leaf . value === true ;
1051- }
1052-
1053- function extractValidator (
1054- data : ZodTypeInfo ,
1055- key : string
1056- ) : ZodTypeAny | undefined {
1057- if ( data . effects ) return undefined ;
1058-
1059- // No effects, check if ZodObject or ZodArray, which are the
1060- // "allowed" objects in the path above the leaf.
1061- const type = data . zodType ;
1062-
1063- if ( type . _def . typeName == 'ZodObject' ) {
1064- const nextType = ( type as AnyZodObject ) . _def . shape ( ) [ key ] ;
1065- const unwrapped = unwrapZodType ( nextType ) ;
1066- return unwrapped . effects ? undefined : unwrapped . zodType ;
1067- } else if ( type . _def . typeName == 'ZodArray' ) {
1068- const array = type as ZodArray < ZodTypeAny > ;
1069- const unwrapped = unwrapZodType ( array . element ) ;
1070- if ( unwrapped . effects ) return undefined ;
1071- return extractValidator ( unwrapped , key ) ;
1072- } else {
1073- throw new SuperFormError ( 'Invalid validator' ) ;
1074- }
1075- }
1076-
1077- ///// Roles ///////////////////////////////////////////////////////
1078-
1079- function Errors_get ( ) {
1080- return get ( Errors ) ;
1081- }
1082-
1083- function Errors_clear (
1084- options : NonNullable < Parameters < typeof clearErrors > [ 1 ] >
1085- ) {
1086- return clearErrors ( Errors , options ) ;
1087- }
1088-
1089- function Errors_set ( newErrors : ValidationErrors < UnwrapEffects < T > > ) {
1090- Errors . set ( newErrors ) ;
1091- }
1092-
1093- function Errors_fromZod ( errors : ZodError < unknown > ) {
1094- return mapErrors ( errors . format ( ) ) ;
1095- }
1096-
1097- function Errors_update ( errorMsgs : null | undefined | string | string [ ] ) {
1098- if ( typeof errorMsgs === 'string' ) errorMsgs = [ errorMsgs ] ;
1099-
1100- if ( options . update === true || options . update == 'errors' ) {
1101- Errors . update ( ( errors ) => {
1102- const error = traversePath (
1103- errors ,
1104- path as FieldPath < typeof errors > ,
1105- ( node ) => {
1106- if ( isInvalidPath ( path , node ) ) {
1107- throw new SuperFormError (
1108- 'Errors can only be added to form fields, not to arrays or objects in the schema. Path: ' +
1109- node . path . slice ( 0 , - 1 )
1110- ) ;
1111- } else if ( node . value === undefined ) {
1112- node . parent [ node . key ] = { } ;
1113- return node . parent [ node . key ] ;
1114- } else {
1115- return node . value ;
1116- }
1117- }
1118- ) ;
1119-
1120- if ( ! error )
1121- throw new SuperFormError (
1122- 'Error path could not be created: ' + path
1123- ) ;
1124-
1125- error . parent [ error . key ] = errorMsgs ?? undefined ;
1126- return errors ;
1127- } ) ;
1128- }
1129- return errorMsgs ?? undefined ;
1130- }
1131-
1132- if ( ! ( 'value' in options ) ) {
1133- // Use value from data
1134- Context . currentData = get ( data ) ;
1135-
1136- const dataToValidate = traversePath (
1137- Context . currentData ,
1138- path as FieldPath < typeof Context . currentData >
1139- ) ;
1140-
1141- Context . value = dataToValidate ?. value ;
1142- } else if ( options . update === true || options . update === 'value' ) {
1143- // Value should be updating the data
1144- data . update (
1145- ( $data ) => {
1146- setPaths ( $data , [ path ] , Context . value ) ;
1147- return ( Context . currentData = $data ) ;
1148- } ,
1149- { taint : options . taint }
1150- ) ;
1151- } else {
1152- Context . shouldUpdate = false ;
1153- }
1154-
1155- //console.log('🚀 ~ file: index.ts:871 ~ validate:', path, value);
1156-
1157- if ( typeof validators !== 'object' ) {
1158- return defaultValidate ( ) ;
1159- }
1160-
1161- if ( 'safeParseAsync' in validators ) {
1162- // Zod validator
1163- // Check if any effects exist for the path, then parse the entire schema.
1164- if ( ! effectMapCache . has ( validators ) ) {
1165- effectMapCache . set ( validators , hasEffects ( validators as ZodTypeAny ) ) ;
1166- }
1167-
1168- const effects = effectMapCache . get ( validators ) ;
1169-
1170- const perFieldValidator = effects
1171- ? undefined
1172- : traversePath (
1173- validators ,
1174- Context . validationPath as FieldPath < typeof validators > ,
1175- ( pathData ) => {
1176- return extractValidator (
1177- unwrapZodType ( pathData . parent ) ,
1178- pathData . key
1179- ) ;
1180- }
1181- ) ;
1182-
1183- if ( perFieldValidator ) {
1184- const validator = extractValidator (
1185- unwrapZodType ( perFieldValidator . parent ) ,
1186- perFieldValidator . key
1187- ) ;
1188- if ( validator ) {
1189- // Check if validator is ZodArray and the path is an array access
1190- // in that case validate the whole array.
1191- if (
1192- Context . currentData &&
1193- validator . _def . typeName == 'ZodArray' &&
1194- ! isNaN ( parseInt ( path [ path . length - 1 ] ) )
1195- ) {
1196- const validateArray = traversePath (
1197- Context . currentData ,
1198- path . slice ( 0 , - 1 ) as FieldPath < typeof Context . currentData >
1199- ) ;
1200- Context . value = validateArray ?. value ;
1201- }
1202-
1203- //console.log('🚀 ~ file: index.ts:972 ~ no effects:', validator);
1204- const result = await validator . safeParseAsync ( Context . value ) ;
1205- if ( ! result . success ) {
1206- const errors = result . error . format ( ) ;
1207- return Errors_update ( errors . _errors ) ;
1208- } else {
1209- return Errors_update ( undefined ) ;
1210- }
1211- }
1212- }
1213-
1214- //console.log('🚀 ~ file: index.ts:983 ~ Effects found, validating all');
1215-
1216- // Effects are found, validate entire data, unfortunately
1217- if ( ! Context . shouldUpdate ) {
1218- // If value shouldn't update, clone and set the new value
1219- Context . currentData = clone ( Context . currentData ?? get ( data ) ) ;
1220- setPaths ( Context . currentData , [ path ] , Context . value ) ;
1221- }
1222-
1223- const result = await ( validators as ZodTypeAny ) . safeParseAsync (
1224- Context . currentData
1225- ) ;
1226-
1227- if ( ! result . success ) {
1228- const newErrors = Errors_fromZod ( result . error ) ;
1229-
1230- if ( options . update === true || options . update == 'errors' ) {
1231- // Set errors for other (tainted) fields, that may have been changed
1232- const taintedFields = get ( tainted ) ;
1233- const currentErrors = Errors_get ( ) ;
1234- let updated = false ;
1235-
1236- // Special check for form level errors
1237- if ( currentErrors . _errors !== newErrors . _errors ) {
1238- if (
1239- ! currentErrors . _errors ||
1240- ! newErrors . _errors ||
1241- currentErrors . _errors . join ( '' ) != newErrors . _errors . join ( '' )
1242- ) {
1243- currentErrors . _errors = newErrors . _errors ;
1244- updated = true ;
1245- }
1246- }
1247-
1248- traversePaths ( newErrors , ( pathData ) => {
1249- if ( ! Array . isArray ( pathData . value ) ) return ;
1250- if ( isPathTainted ( pathData . path , taintedFields ) ) {
1251- setPaths ( currentErrors , [ pathData . path ] , pathData . value ) ;
1252- updated = true ;
1253- }
1254- return 'skip' ;
1255- } ) ;
1256-
1257- if ( updated ) Errors_set ( currentErrors ) ;
1258- }
1259-
1260- // Finally, set errors for the specific field
1261- // it will be set to undefined if no errors, so the tainted+error check
1262- // in oninput can determine if errors should be displayed or not.
1263- const current = traversePath (
1264- newErrors ,
1265- path as FieldPath < typeof newErrors >
1266- ) ;
1267-
1268- return Errors_update ( options . errors ?? current ?. value ) ;
1269- } else {
1270- // We validated the whole data structure, so clear all errors on success
1271- // but also set the current path to undefined, so it will be used in the tainted+error
1272- // check in oninput.
1273- Errors_clear ( { undefinePath : path , clearFormLevelErrors : true } ) ;
1274- return undefined ;
1275- }
1276- } else {
1277- // SuperForms validator
1278-
1279- const validator = traversePath (
1280- validators as Validators < UnwrapEffects < T > > ,
1281- Context . validationPath as FieldPath < typeof validators >
1282- ) ;
1283-
1284- if ( ! validator ) {
1285- // Path didn't exist
1286- throw new SuperFormError ( 'No Superforms validator found: ' + path ) ;
1287- } else if ( validator . value === undefined ) {
1288- // No validator, use default
1289- return defaultValidate ( ) ;
1290- } else {
1291- const result = validator . value ( Context . value ) ;
1292- return Errors_update ( result ? options . errors ?? result : result ) ;
1293- }
1294- }
1295- }
1296-
1297964/**
1298965 * Custom use:enhance version. Flash message support, friendly error messages, for usage with initializeForm.
1299966 * @param formEl Form element from the use:formEnhance default parameter.
0 commit comments