T
TanStack•4mo ago
fair-rose

onServerValidate example lacking other patterns.

I don't understand why we have formErrors.map but on onServerValidate but the example only returns 1 error message.
const serverValidate = createServerValidate({
...formOpts,
onServerValidate: ({ value }) => {
if (value.age < 12) {
return 'Server validation: You must be at least 12 to sign up'
}
},
})
const serverValidate = createServerValidate({
...formOpts,
onServerValidate: ({ value }) => {
if (value.age < 12) {
return 'Server validation: You must be at least 12 to sign up'
}
},
})
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
Is there a way to output it in a field? It would be great to have an example where we validate each field in server validation and then output the field errors. An example also with standard schema validate with custom db validation logic.
8 Replies
conscious-sapphire
conscious-sapphire•4mo ago
This is something I would like also. I can help a little, now. The server validation happens on submission. The return can be anything, including a simple string, but if you wanted to check several things (eg, a password fitting the rules) then you might return several issues. After that, the response should be merged to formErrors with the useActionState, which means client-side checks that might have left messages will also be present. The map allows you to show them all.
fair-rose
fair-roseOP•4mo ago
I have ditched the createServerValidate altogether and used next-safe-actions. create-form.tsx
import { create } from "./_action";

export const CreateForm = () => {
const form = useAppForm({
defaultValues: {
name: "",
completed: false,
category: "" as TodoCategory,
},
validators: {
onSubmitAsync: async ({ value }) => {
const result = await create(value);

if (result?.validationErrors) {
return {
fields: formatValidationErrors(result.validationErrors),
};
}

if (result?.serverError) {
return {
form: result.serverError,
fields: {},
};
}

if (result?.data?.success) {
toast.success(result?.data?.success);
}

return null;
},
},
onSubmit: () => {
redirect("/cms/todos");
},
});

return ()
}
import { create } from "./_action";

export const CreateForm = () => {
const form = useAppForm({
defaultValues: {
name: "",
completed: false,
category: "" as TodoCategory,
},
validators: {
onSubmitAsync: async ({ value }) => {
const result = await create(value);

if (result?.validationErrors) {
return {
fields: formatValidationErrors(result.validationErrors),
};
}

if (result?.serverError) {
return {
form: result.serverError,
fields: {},
};
}

if (result?.data?.success) {
toast.success(result?.data?.success);
}

return null;
},
},
onSubmit: () => {
redirect("/cms/todos");
},
});

return ()
}
_action.ts
"use server";

import { nanoid } from "nanoid";
import { actionClient } from "@/lib/safe-action";
import { db } from "@/server/db";
import { todo } from "@/server/db/schema";
import { createSchema } from "./_validation-schema";

export const create = actionClient
.schema(createSchema)
.action(async ({ parsedInput: { name, completed, category } }) => {
try {
await db.insert(todo).values({
id: `todo_${nanoid()}`,
name,
completed,
category: category,
});
} catch (error) {
throw error;
}

return { success: "Successfully created todo" };
});
"use server";

import { nanoid } from "nanoid";
import { actionClient } from "@/lib/safe-action";
import { db } from "@/server/db";
import { todo } from "@/server/db/schema";
import { createSchema } from "./_validation-schema";

export const create = actionClient
.schema(createSchema)
.action(async ({ parsedInput: { name, completed, category } }) => {
try {
await db.insert(todo).values({
id: `todo_${nanoid()}`,
name,
completed,
category: category,
});
} catch (error) {
throw error;
}

return { success: "Successfully created todo" };
});
_validation-schema.ts
import { z } from "zod";
import { TodoCategory } from "@/server/db/schema";

export const createSchema = z.object({
name: z
.string()
.min(1, "Name is required")
.max(36, "Maximum of 36 characters"),
completed: z.boolean(),
category: z.nativeEnum(TodoCategory, {
errorMap: () => ({ message: "Category is required" }),
}),
});
import { z } from "zod";
import { TodoCategory } from "@/server/db/schema";

export const createSchema = z.object({
name: z
.string()
.min(1, "Name is required")
.max(36, "Maximum of 36 characters"),
completed: z.boolean(),
category: z.nativeEnum(TodoCategory, {
errorMap: () => ({ message: "Category is required" }),
}),
});
magic-amber
magic-amber•4mo ago
https://github.com/TanStack/form/pull/1432 I'm just finishing some things up here
GitHub
feat (form-core): Merging Form-level server errors with Field-level...
Instead of the previous serverValidate function just returning a string, we now return an object: const serverValidate = createServerValidate({ ...formOpts, onServerValidate: ({ value }) =&...
conscious-sapphire
conscious-sapphire•4mo ago
This is also what I'm doing.
foreign-sapphire
foreign-sapphire•4mo ago
formatValidationErrors what is this function sr?
conscious-sapphire
conscious-sapphire•4mo ago
import { formatValidationErrors } from "next-safe-action";
fair-rose
fair-roseOP•3mo ago
It's a custom function I forgot to include in the initial message. I didn't use the one from next-safe-action. I'm not sure if they're identical. 😄
export function formatValidationErrors(
validationErrors: ValidationErrors | null | undefined,
): FormattedErrors {
if (!validationErrors) return {};

const formattedErrors: FormattedErrors = {};

// Process each field in the validation errors
Object.entries(validationErrors).forEach(([field, value]) => {
// Skip the root _errors field as it's not associated with a specific form field
if (field === "_errors") return;

// Handle the nested structure
if (
value &&
typeof value === "object" &&
"_errors" in value &&
Array.isArray(value._errors)
) {
if (value._errors.length > 0) {
formattedErrors[field] = value._errors.map((message) => ({ message }));
}
}
});

return formattedErrors;
}
export function formatValidationErrors(
validationErrors: ValidationErrors | null | undefined,
): FormattedErrors {
if (!validationErrors) return {};

const formattedErrors: FormattedErrors = {};

// Process each field in the validation errors
Object.entries(validationErrors).forEach(([field, value]) => {
// Skip the root _errors field as it's not associated with a specific form field
if (field === "_errors") return;

// Handle the nested structure
if (
value &&
typeof value === "object" &&
"_errors" in value &&
Array.isArray(value._errors)
) {
if (value._errors.length > 0) {
formattedErrors[field] = value._errors.map((message) => ({ message }));
}
}
});

return formattedErrors;
}
like-gold
like-gold•3mo ago
Oh this is huge - thank you! LMK if you need help getting it past the line 🙂

Did you find this page helpful?