Anonymous
ZOD-Validierung von DTO – reagieren – fehlende Fehlermeldungen
Post
by Anonymous » 09 Nov 2025, 18:39
Ich versuche also nur, dieses Formular mit zod zu validieren, was tatsächlich funktioniert. Das
Problem besteht jedoch darin, dass die Fehlermeldungen nicht wie gewünscht angezeigt werden (es wird einfach nichts angezeigt, wenn ein Feld ungültig ist). Das Formular verwendet Tanstack-Formular in einem Typoskript-Setup. Ich habe keine Ahnung, warum es nicht funktioniert. Jede Eingabe ist willkommen
Schema
Code: Select all
import z from "zod";
import { zodErrorMessages } from "./../../../apps/web/src/shared/utils/zodErrorMessages";
// --- Sub-Schemas ---
export const createRecipeIngredientSchema = z.object({
ingredientId: z.number({ error: zodErrorMessages.requiredInput }).int().positive(),
quantity: z.string({ error: zodErrorMessages.requiredInput }).min(1).trim(),
unitId: z.number({ error: zodErrorMessages.requiredInput }).int().positive(),
note: z.string().trim().nullable(),
});
export const createRecipeStepSchema = z.object({
stepNumber: z.number({ error: zodErrorMessages.requiredInput }).int().positive(),
instruction: z.string({ error: zodErrorMessages.requiredInput }).min(1).trim(),
duration: z.number({ error: zodErrorMessages.requiredInput }).int().min(0),
});
export const createTagSchema = z.object({
tagId: z.number({ error: zodErrorMessages.requiredInput }).int().positive(),
});
// --- Haupt-Schema ---
export const createRecipeSchema = z.object({
title: z.string({ error: zodErrorMessages.requiredInput }).min(1).trim(),
description: z.string({ error: zodErrorMessages.requiredInput }).min(10).trim(),
prepTime: z.number({ error: zodErrorMessages.requiredInput }).int().min(0),
cookTime: z.number({ error: zodErrorMessages.requiredInput }).int().min(0),
servings: z.number({ error: zodErrorMessages.requiredInput }).int().min(1),
tags: z.array(createTagSchema),
ingredients: z.array(createRecipeIngredientSchema).min(1),
steps: z.array(createRecipeStepSchema).min(1),
});
// --- DTO-Typ ableiten ---
export type CreateRecipeDTO = z.infer;
// --- Sub-DTOs ---
export type CreateRecipeStepDTO = z.infer;
export type CreateRecipeIngredientDTO = z.infer;
export type CreateTagDTO = z.infer;
Formular
Code: Select all
import { useForm } from "@tanstack/react-form";
import { useNavigate, Link } from "@tanstack/react-router";
import type {
CreateRecipeDTO,
CreateRecipeStepDTO,
CreateRecipeIngredientDTO,
} from "@repo/types/recipe";
import { createRecipe } from "./recipeCreate.service";
import { LoadingOverlay } from "../../../shared/components/other/loadingoverlay";
import { useState } from "react";
import { createRecipeSchema } from '../../../../../../packages/types/src/createRecipe.schema';
export const RecipeCreate = () => {
const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false); // State für Lade-Overlay während des Submit
const form = useForm({
defaultValues: {
title: "",
description: "",
prepTime: undefined,
cookTime: undefined,
servings: undefined,
tags: [],
authors: [],
steps: [{ stepNumber: 1, instruction: "", duration: 0 }],
ingredients: [],
} as CreateRecipeDTO, // Typen für das Formular setzen
validators: {
onSubmit: createRecipeSchema, // Zod-Schema validiert die Formulardaten (mag kein optional wegen dem DTO)
},
onSubmit: async ({ value }) => {
try {
// Die Formularwerte wurden erfolgreich validiert
setIsSubmitting(true); // Ladeanimation aktivieren
await createRecipe(value); // API-Call zum Erstellen des Rezepts
navigate({ to: "/recipes" }); // nach Erfolg weiterleiten
} catch (err) {
setIsSubmitting(false); // Fehler => Ladeanimation deaktivieren
}
},
});
return (
{/* Lade-Animation */}
{isSubmitting && (
)}
New Recipe
{
e.preventDefault();
e.stopPropagation();
await form.handleSubmit();
}}
className="space-y-6"
>
{/* Basis-Informationen */}
Basics
{/* Title Field */}
{(field) => (
Title (min. 1 character)
field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{/* Description Field */}
{(field) => (
Description (min. 10 characters)
field.handleChange(e.target.value)}
onBlur={field.handleBlur}
rows={3}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
{/* Zeichen-Zähler */}
{field.state.value?.length ?? 0} / 10
)}
{/* Time and Servings Fields */}
{/* PREPERATION Field (Name korrigiert) */}
{(field) => (
Preparation (Min)
// Parsen in Zahl oder undefined
field.handleChange(e.target.value ? parseInt(e.target.value) : undefined)
}
onBlur={field.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{/* COOK Field (Name korrigiert) */}
{(field) => (
Cook (Min)
field.handleChange(e.target.value ? parseInt(e.target.value) : undefined)
}
onBlur={field.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{/* Servings Field */}
{(field) => (
Servings
field.handleChange(e.target.value ? parseInt(e.target.value) : undefined)
}
onBlur={field.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{/* Zutaten */}
{(field) => (
Ingredients
{/* Fehleranzeige für die Liste (z.B. Mindestanzahl) reaktiviert */}
field.pushValue({
ingredientId: 0,
quantity: "",
unitId: 0,
note: null,
} as CreateRecipeIngredientDTO)
}
className="rounded bg-secondary px-4 py-2 font-medium text-text-dark"
>
+ Add Ingredient
{field.state.value?.length === 0 && (
No ingredients added yet. Click "Add Ingredient" to start.
)}
{field.state.value?.map((_, index) => (
{/* Erste Zeile: Ingredient ID + Quantity */}
{(subField) => (
Ingredient ID
subField.handleChange(parseInt(e.target.value) || 0)
}
onBlur={subField.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{(subField) => (
Quantity
subField.handleChange(e.target.value)}
onBlur={subField.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{/* Zweite Zeile: Unit ID + Note */}
{(subField) => (
Unit ID
subField.handleChange(parseInt(e.target.value) || 0)
}
onBlur={subField.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{(subField) => (
Note (optional)
// Setzt null, wenn das Feld leer ist
subField.handleChange(e.target.value || null)
}
onBlur={subField.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{/* Remove Button */}
field.removeValue(index)}
className="rounded bg-red-100 px-4 py-2 text-sm text-red-700"
>
Remove Ingredient
))}
)}
{/* Schritte */}
{(field) => (
Steps
{
const newStepNumber = (field.state.value?.length || 0) + 1;
field.pushValue({
stepNumber: newStepNumber,
instruction: "",
duration: 0,
} as CreateRecipeStepDTO);
}}
className="rounded bg-secondary px-4 py-2 font-medium text-text-dark"
>
+ Add Step
{field.state.value?.map((step, index) => (
{step.stepNumber}
{(subField) => (
subField.handleChange(e.target.value)}
onBlur={subField.handleBlur}
rows={2}
placeholder="Add instruction details... *"
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{(subField) => (
subField.handleChange(parseInt(e.target.value) || 0)
}
onBlur={subField.handleBlur}
className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none"
/>
)}
{(field.state.value?.length || 0) > 1 && (
{
field.removeValue(index);
// Renumber remaining steps
const currentSteps = field.state.value || [];
const updatedSteps = currentSteps
.filter((_, i) => i !== index)
.map((step, i) => ({
...step,
stepNumber: i + 1,
}));
field.setValue(updatedSteps);
}}
className="rounded bg-red-100 px-4 py-2 text-red-700"
>
×
)}
))}
)}
{/* Buttons */}
Cancel
state.isSubmitting}>
{(isSubmitting) => (
{isSubmitting ? "Creating..." : "Create"}
)}
);
}
1762709952
Anonymous
Ich versuche also nur, dieses Formular mit zod zu validieren, was tatsächlich funktioniert. Das [url=viewtopic.php?t=26065]Problem[/url] besteht jedoch darin, dass die Fehlermeldungen nicht wie gewünscht angezeigt werden (es wird einfach nichts angezeigt, wenn ein Feld ungültig ist). Das Formular verwendet Tanstack-Formular in einem Typoskript-Setup. Ich habe keine Ahnung, warum es nicht funktioniert. Jede Eingabe ist willkommen Schema [code]import z from "zod"; import { zodErrorMessages } from "./../../../apps/web/src/shared/utils/zodErrorMessages"; // --- Sub-Schemas --- export const createRecipeIngredientSchema = z.object({ ingredientId: z.number({ error: zodErrorMessages.requiredInput }).int().positive(), quantity: z.string({ error: zodErrorMessages.requiredInput }).min(1).trim(), unitId: z.number({ error: zodErrorMessages.requiredInput }).int().positive(), note: z.string().trim().nullable(), }); export const createRecipeStepSchema = z.object({ stepNumber: z.number({ error: zodErrorMessages.requiredInput }).int().positive(), instruction: z.string({ error: zodErrorMessages.requiredInput }).min(1).trim(), duration: z.number({ error: zodErrorMessages.requiredInput }).int().min(0), }); export const createTagSchema = z.object({ tagId: z.number({ error: zodErrorMessages.requiredInput }).int().positive(), }); // --- Haupt-Schema --- export const createRecipeSchema = z.object({ title: z.string({ error: zodErrorMessages.requiredInput }).min(1).trim(), description: z.string({ error: zodErrorMessages.requiredInput }).min(10).trim(), prepTime: z.number({ error: zodErrorMessages.requiredInput }).int().min(0), cookTime: z.number({ error: zodErrorMessages.requiredInput }).int().min(0), servings: z.number({ error: zodErrorMessages.requiredInput }).int().min(1), tags: z.array(createTagSchema), ingredients: z.array(createRecipeIngredientSchema).min(1), steps: z.array(createRecipeStepSchema).min(1), }); // --- DTO-Typ ableiten --- export type CreateRecipeDTO = z.infer; // --- Sub-DTOs --- export type CreateRecipeStepDTO = z.infer; export type CreateRecipeIngredientDTO = z.infer; export type CreateTagDTO = z.infer; [/code] Formular [code]import { useForm } from "@tanstack/react-form"; import { useNavigate, Link } from "@tanstack/react-router"; import type { CreateRecipeDTO, CreateRecipeStepDTO, CreateRecipeIngredientDTO, } from "@repo/types/recipe"; import { createRecipe } from "./recipeCreate.service"; import { LoadingOverlay } from "../../../shared/components/other/loadingoverlay"; import { useState } from "react"; import { createRecipeSchema } from '../../../../../../packages/types/src/createRecipe.schema'; export const RecipeCreate = () => { const navigate = useNavigate(); const [isSubmitting, setIsSubmitting] = useState(false); // State für Lade-Overlay während des Submit const form = useForm({ defaultValues: { title: "", description: "", prepTime: undefined, cookTime: undefined, servings: undefined, tags: [], authors: [], steps: [{ stepNumber: 1, instruction: "", duration: 0 }], ingredients: [], } as CreateRecipeDTO, // Typen für das Formular setzen validators: { onSubmit: createRecipeSchema, // Zod-Schema validiert die Formulardaten (mag kein optional wegen dem DTO) }, onSubmit: async ({ value }) => { try { // Die Formularwerte wurden erfolgreich validiert setIsSubmitting(true); // Ladeanimation aktivieren await createRecipe(value); // API-Call zum Erstellen des Rezepts navigate({ to: "/recipes" }); // nach Erfolg weiterleiten } catch (err) { setIsSubmitting(false); // Fehler => Ladeanimation deaktivieren } }, }); return ( {/* Lade-Animation */} {isSubmitting && ( )} New Recipe { e.preventDefault(); e.stopPropagation(); await form.handleSubmit(); }} className="space-y-6" > {/* Basis-Informationen */} Basics {/* Title Field */} {(field) => ( Title (min. 1 character) field.handleChange(e.target.value)} onBlur={field.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {/* Description Field */} {(field) => ( Description (min. 10 characters) field.handleChange(e.target.value)} onBlur={field.handleBlur} rows={3} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> {/* Zeichen-Zähler */} {field.state.value?.length ?? 0} / 10 )} {/* Time and Servings Fields */} {/* PREPERATION Field (Name korrigiert) */} {(field) => ( Preparation (Min) // Parsen in Zahl oder undefined field.handleChange(e.target.value ? parseInt(e.target.value) : undefined) } onBlur={field.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {/* COOK Field (Name korrigiert) */} {(field) => ( Cook (Min) field.handleChange(e.target.value ? parseInt(e.target.value) : undefined) } onBlur={field.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {/* Servings Field */} {(field) => ( Servings field.handleChange(e.target.value ? parseInt(e.target.value) : undefined) } onBlur={field.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {/* Zutaten */} {(field) => ( Ingredients {/* Fehleranzeige für die Liste (z.B. Mindestanzahl) reaktiviert */} field.pushValue({ ingredientId: 0, quantity: "", unitId: 0, note: null, } as CreateRecipeIngredientDTO) } className="rounded bg-secondary px-4 py-2 font-medium text-text-dark" > + Add Ingredient {field.state.value?.length === 0 && ( No ingredients added yet. Click "Add Ingredient" to start. )} {field.state.value?.map((_, index) => ( {/* Erste Zeile: Ingredient ID + Quantity */} {(subField) => ( Ingredient ID subField.handleChange(parseInt(e.target.value) || 0) } onBlur={subField.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {(subField) => ( Quantity subField.handleChange(e.target.value)} onBlur={subField.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {/* Zweite Zeile: Unit ID + Note */} {(subField) => ( Unit ID subField.handleChange(parseInt(e.target.value) || 0) } onBlur={subField.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {(subField) => ( Note (optional) // Setzt null, wenn das Feld leer ist subField.handleChange(e.target.value || null) } onBlur={subField.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {/* Remove Button */} field.removeValue(index)} className="rounded bg-red-100 px-4 py-2 text-sm text-red-700" > Remove Ingredient ))} )} {/* Schritte */} {(field) => ( Steps { const newStepNumber = (field.state.value?.length || 0) + 1; field.pushValue({ stepNumber: newStepNumber, instruction: "", duration: 0, } as CreateRecipeStepDTO); }} className="rounded bg-secondary px-4 py-2 font-medium text-text-dark" > + Add Step {field.state.value?.map((step, index) => ( {step.stepNumber} {(subField) => ( subField.handleChange(e.target.value)} onBlur={subField.handleBlur} rows={2} placeholder="Add instruction details... *" className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {(subField) => ( subField.handleChange(parseInt(e.target.value) || 0) } onBlur={subField.handleBlur} className="w-full rounded border border-border px-3 py-2 focus:border-primary focus:outline-none" /> )} {(field.state.value?.length || 0) > 1 && ( { field.removeValue(index); // Renumber remaining steps const currentSteps = field.state.value || []; const updatedSteps = currentSteps .filter((_, i) => i !== index) .map((step, i) => ({ ...step, stepNumber: i + 1, })); field.setValue(updatedSteps); }} className="rounded bg-red-100 px-4 py-2 text-red-700" > × )} ))} )} {/* Buttons */} Cancel state.isSubmitting}> {(isSubmitting) => ( {isSubmitting ? "Creating..." : "Create"} )} ); } [/code]