Skip to content

Commit a18e68d

Browse files
committed
Factorized out validateField context.
1 parent 90bbe8a commit a18e68d

File tree

3 files changed

+386
-344
lines changed

3 files changed

+386
-344
lines changed

src/lib/client/index.ts

Lines changed: 11 additions & 344 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
4134
import { stringify } from 'devalue';
4235
import type { FormFields } from '../index.js';
4336
import {
@@ -48,15 +41,17 @@ import {
4841
comparePaths,
4942
setPaths,
5043
pathExists,
51-
type ZodTypeInfo,
52-
traversePaths,
5344
isInvalidPath
5445
} from '../traversal.js';
5546
import { 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

6156
enum 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
240223
export 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

Comments
 (0)