ZOD-Validierung von DTO – reagieren – fehlende FehlermeldungenJavaScript

Javascript-Forum
Anonymous
 ZOD-Validierung von DTO – reagieren – fehlende Fehlermeldungen

Post by Anonymous »

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"}

)}





);
}

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post