React TypeScript Property Form Validation
export interface PropertyForm {
propertyTitle : string ;
description : string ;
amenities : string ;
monthlyRent : string ;
location : string ;
unitsAvailable : string ;
applicationDeadline : string ;
}
export interface PropertyFormErrors {
propertyTitle ?: string ;
description ?: string ;
amenities ?: string ;
monthlyRent ?: string ;
location ?: string ;
unitsAvailable ?: string ;
applicationDeadline ?: string ;
}
export const validatePropertyField = (
name : keyof PropertyForm ,
value : string
): string => {
switch ( name ) {
case " propertyTitle " :
if ( ! value . trim ()) {
return " Property title is required " ;
}
if ( value . trim (). length < 3 ) {
return " Property title must be at least 3 characters " ;
}
return "" ;
case " description " :
if ( ! value . trim ()) {
return " Description is required " ;
}
if ( value . trim (). length > 2000 ) {
return " Description cannot exceed 2000 characters " ;
}
return "" ;
case " amenities " :
if ( ! value . trim ()) {
return " Amenities are required " ;
}
return "" ;
case " monthlyRent " :
if ( ! value . trim ()) {
return " Monthly rent is required " ;
}
if ( Number ( value ) <= 0 ) {
return " Monthly rent must be greater than 0 " ;
}
return "" ;
case " location " :
if ( ! value . trim ()) {
return " Location is required " ;
}
return "" ;
case " unitsAvailable " :
if ( ! value . trim ()) {
return " Units available is required " ;
}
if ( ! Number . isInteger ( Number ( value ))) {
return " Units available must be a whole number " ;
}
if ( Number ( value ) < 1 ) {
return " At least 1 unit must be available " ;
}
return "" ;
case " applicationDeadline " :
if ( ! value ) {
return " Application deadline is required " ;
}
return "" ;
default :
return "" ;
}
};
export const validatePropertyForm = (
formData : PropertyForm
): PropertyFormErrors => {
const errors : PropertyFormErrors = {};
Object . entries ( formData ). forEach (([ name , value ]) => {
const error = validatePropertyField (
name as keyof PropertyForm ,
value
);
if ( error ) {
errors [ name as keyof PropertyFormErrors ] = error ;
}
});
return errors ;
};
Enter fullscreen mode
Exit fullscreen mode
Add Product Form **
import { useState } from " react " ;
import { useNavigate , useParams } from " react-router-dom " ;
import { toast } from " react-toastify " ;
import { useAddPropertyMutation } from " ../services/propertyApi " ;
import {
PropertyForm ,
PropertyFormErrors ,
validatePropertyField ,
validatePropertyForm ,
} from " ../validations/propertyValidation " ;
const AddProperty = () => {
const navigate = useNavigate ();
const { agencyId } = useParams < { agencyId : string } > ();
const [ addProperty , { isLoading : isAdding }] =
useAddPropertyMutation ();
const [ formData , setFormData ] = useState < PropertyForm > ({
propertyTitle : "" ,
description : "" ,
amenities : "" ,
monthlyRent : "" ,
location : "" ,
unitsAvailable : "" ,
applicationDeadline : "" ,
});
const [ errors , setErrors ] = useState < PropertyFormErrors > ({});
// ----------------------------------------
// Handle input change
// ----------------------------------------
const handleChange = (
e : React . ChangeEvent <
HTMLInputElement | HTMLTextAreaElement
>
) => {
const { name , value } = e . target ;
setFormData (( prev ) => ({
... prev ,
[ name ]: value ,
}));
// Clear field error while typing
if ( errors [ name as keyof PropertyFormErrors ]) {
setErrors (( prev ) => ({
... prev ,
[ name ]: "" ,
}));
}
};
// ----------------------------------------
// Blur validation
// ----------------------------------------
const handleBlur = (
e : React . FocusEvent <
HTMLInputElement | HTMLTextAreaElement
>
) => {
const { name , value } = e . target ;
const error = validatePropertyField (
name as keyof PropertyForm ,
value
);
setErrors (( prev ) => ({
... prev ,
[ name ]: error ,
}));
};
// ----------------------------------------
// Submit
// ----------------------------------------
const handleSubmit = async (
e : React . FormEvent < HTMLFormElement >
) => {
e . preventDefault ();
// Validate complete form
const validationErrors = validatePropertyForm ( formData );
setErrors ( validationErrors );
if ( Object . keys ( validationErrors ). length > 0 ) {
toast . error ( " Please fix the validation errors " );
return ;
}
// Validate Agency ID
const parsedAgencyId = Number ( agencyId );
if ( ! agencyId || Number . isNaN ( parsedAgencyId )) {
toast . error ( " Invalid agency ID " );
return ;
}
try {
const result = await addProperty ({
agencyId : parsedAgencyId ,
propertyTitle : formData . propertyTitle . trim (),
description : formData . description . trim (),
amenities : formData . amenities . trim (),
monthlyRent : Number ( formData . monthlyRent ),
location : formData . location . trim (),
unitsAvailable : Number ( formData . unitsAvailable ),
applicationDeadline : formData . applicationDeadline ,
}). unwrap ();
toast . success ( result . response );
// Navigate only after successful API call
navigate ( - 1 );
} catch ( error ) {
console . error ( " Add property error: " , error );
toast . error ( " Failed to add property " );
}
};
// ----------------------------------------
// Back
// ----------------------------------------
const handleBack = () => {
navigate ( - 1 );
};
return (
< div className = " min-h-screen bg-gray-50 px-4 py-6 sm:px-6 lg:px-8 " >
< div className = " mx-auto max-w-4xl " >
{ /* Header */ }
< div className = " mb-6 flex items-center gap-3 " >
< button
type = " button "
onClick = { handleBack }
disabled = { isAdding }
className = " flex h-10 w-10 items-center justify-center rounded-full border bg-white text-gray-700 shadow-sm transition hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 "
>
< span className = " text-xl " > ← < /span >
< /button >
< div >
< h1 className = " text-2xl font-bold text-gray-900 sm:text-3xl " >
Add Property
< /h1 >
< p className = " mt-1 text-sm text-gray-500 " >
Add a new property for Agency # { agencyId }
< /p >
< /div >
< /div >
{ /* Form */ }
< div className = " rounded-xl bg-white p-5 shadow-sm sm:p-8 " >
< form onSubmit = { handleSubmit } noValidate >
< div className = " grid grid-cols-1 gap-5 md:grid-cols-2 " >
{ /* Property Title */ }
< div className = " md:col-span-2 " >
< label
htmlFor = " propertyTitle "
className = " mb-2 block text-sm font-medium text-gray-700 "
>
Property Title
< span className = " text-red-500 " > *< /span >
< /label >
< input
id = " propertyTitle "
name = " propertyTitle "
type = " text "
value = { formData . propertyTitle }
onChange = { handleChange }
onBlur = { handleBlur }
placeholder = " Enter property title "
className = { `w-full rounded-lg border px-4 py-3 outline-none ${
errors . propertyTitle
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . propertyTitle && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . propertyTitle }
< /p >
)}
< /div >
{ /* Description */ }
< div className = " md:col-span-2 " >
< div className = " mb-2 flex justify-between " >
< label
htmlFor = " description "
className = " text-sm font-medium text-gray-700 "
>
Description
< span className = " text-red-500 " > *< /span >
< /label >
< span className = " text-xs text-gray-400 " >
{ formData . description . length } /200 0
< /span >
< /div >
< textarea
id = " description "
name = " description "
rows = { 5 }
maxLength = { 2000 }
value = { formData . description }
onChange = { handleChange }
onBlur = { handleBlur }
placeholder = " Describe the property... "
className = { `w-full resize-none rounded-lg border px-4 py-3 outline-none ${
errors . description
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . description && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . description }
< /p >
)}
< /div >
{ /* Amenities */ }
< div className = " md:col-span-2 " >
< label
htmlFor = " amenities "
className = " mb-2 block text-sm font-medium text-gray-700 "
>
Amenities
< span className = " text-red-500 " > *< /span >
< /label >
< input
id = " amenities "
name = " amenities "
type = " text "
value = { formData . amenities }
onChange = { handleChange }
onBlur = { handleBlur }
placeholder = " Parking, WiFi, Gym, Swimming Pool "
className = { `w-full rounded-lg border px-4 py-3 outline-none ${
errors . amenities
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . amenities && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . amenities }
< /p >
)}
< /div >
{ /* Monthly Rent */ }
< div >
< label
htmlFor = " monthlyRent "
className = " mb-2 block text-sm font-medium text-gray-700 "
>
Monthly Rent
< span className = " text-red-500 " > *< /span >
< /label >
< input
id = " monthlyRent "
name = " monthlyRent "
type = " number "
min = " 1 "
value = { formData . monthlyRent }
onChange = { handleChange }
onBlur = { handleBlur }
placeholder = " 25000 "
className = { `w-full rounded-lg border px-4 py-3 outline-none ${
errors . monthlyRent
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . monthlyRent && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . monthlyRent }
< /p >
)}
< /div >
{ /* Units Available */ }
< div >
< label
htmlFor = " unitsAvailable "
className = " mb-2 block text-sm font-medium text-gray-700 "
>
Units Available
< span className = " text-red-500 " > *< /span >
< /label >
< input
id = " unitsAvailable "
name = " unitsAvailable "
type = " number "
min = " 1 "
step = " 1 "
value = { formData . unitsAvailable }
onChange = { handleChange }
onBlur = { handleBlur }
placeholder = " 5 "
className = { `w-full rounded-lg border px-4 py-3 outline-none ${
errors . unitsAvailable
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . unitsAvailable && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . unitsAvailable }
< /p >
)}
< /div >
{ /* Location */ }
< div className = " md:col-span-2 " >
< label
htmlFor = " location "
className = " mb-2 block text-sm font-medium text-gray-700 "
>
Location
< span className = " text-red-500 " > *< /span >
< /label >
< input
id = " location "
name = " location "
type = " text "
value = { formData . location }
onChange = { handleChange }
onBlur = { handleBlur }
placeholder = " Enter property location "
className = { `w-full rounded-lg border px-4 py-3 outline-none ${
errors . location
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . location && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . location }
< /p >
)}
< /div >
{ /* Application Deadline */ }
< div >
< label
htmlFor = " applicationDeadline "
className = " mb-2 block text-sm font-medium text-gray-700 "
>
Application Deadline
< span className = " text-red-500 " > *< /span >
< /label >
< input
id = " applicationDeadline "
name = " applicationDeadline "
type = " date "
min = { new Date (). toISOString (). split ( " T " )[ 0 ]}
value = { formData . applicationDeadline }
onChange = { handleChange }
onBlur = { handleBlur }
className = { `w-full rounded-lg border px-4 py-3 outline-none ${
errors . applicationDeadline
? " border-red-500 "
: " border-gray-300 focus:border-blue-500 "
} ` }
/ >
{ errors . applicationDeadline && (
< p className = " mt-1 text-sm text-red-500 " >
{ errors . applicationDeadline }
< /p >
)}
< /div >
< /div >
{ /* Buttons */ }
< div className = " mt-8 flex flex-col-reverse gap-3 border-t pt-6 sm:flex-row sm:justify-end " >
< button
type = " button "
onClick = { handleBack }
disabled = { isAdding }
className = " rounded-lg border border-gray-300 px-6 py-3 font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 "
>
Cancel
< /button >
< button
type = " submit "
disabled = { isAdding }
className = " rounded-lg bg-blue-600 px-6 py-3 font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60 "
>
{ isAdding
? " Adding Property... "
: " Add Property " }
< /button >
< /div >
< /form >
< /div >
< /div >
< /div >
);
};
export default AddProperty ;
Enter fullscreen mode
Exit fullscreen mode
Top comments (0)