Référence des Composants de Formulaire
Ce guide fournit une référence complète des composants de formulaire utilisés dans le système EMTB.
🎯 Vue d'ensemble
Le système EMTB utilise une collection de composants de formulaire réutilisables basés sur Material-UI et React Hook Form pour assurer une expérience utilisateur cohérente.
📋 Composants de base
TextField
import { TextField } from '@mui/material';
import { Controller } from 'react-hook-form';
interface FormTextFieldProps {
name: string;
label: string;
control: any;
rules?: any;
required?: boolean;
disabled?: boolean;
multiline?: boolean;
rows?: number;
}
const FormTextField: React.FC<FormTextFieldProps> = ({
name,
label,
control,
rules,
required = false,
disabled = false,
multiline = false,
rows = 1,
}) => {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label={label}
required={required}
disabled={disabled}
multiline={multiline}
rows={rows}
error={!!error}
helperText={error?.message}
fullWidth
margin="normal"
/>
)}
/>
);
};
SelectField
import { FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import { Controller } from 'react-hook-form';
interface FormSelectProps {
name: string;
label: string;
control: any;
options: Array<{ value: string | number; label: string }>;
rules?: any;
required?: boolean;
disabled?: boolean;
}
const FormSelect: React.FC<FormSelectProps> = ({
name,
label,
control,
options,
rules,
required = false,
disabled = false,
}) => {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState: { error } }) => (
<FormControl
fullWidth
margin="normal"
required={required}
disabled={disabled}
error={!!error}
>
<InputLabel>{label}</InputLabel>
<Select {...field} label={label}>
{options.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
{error && (
<FormHelperText error>{error.message}</FormHelperText>
)}
</FormControl>
)}
/>
);
};
DatePicker
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { Controller } from 'react-hook-form';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { fr } from 'date-fns/locale';
interface FormDatePickerProps {
name: string;
label: string;
control: any;
rules?: any;
required?: boolean;
disabled?: boolean;
}
const FormDatePicker: React.FC<FormDatePickerProps> = ({
name,
label,
control,
rules,
required = false,
disabled = false,
}) => {
return (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={fr}>
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState: { error } }) => (
<DatePicker
{...field}
label={label}
disabled={disabled}
slotProps={{
textField: {
required,
error: !!error,
helperText: error?.message,
fullWidth: true,
margin: 'normal',
},
}}
/>
)}
/>
</LocalizationProvider>
);
};
🏗️ Composants composés
ClientForm
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { Box, Button, Paper, Typography } from '@mui/material';
const clientSchema = yup.object().shape({
nom: yup.string().required('Le nom est obligatoire'),
adresse: yup.string().optional(),
telephone: yup.string().optional(),
email: yup.string().email('Email invalide').optional(),
});
interface ClientFormProps {
initialData?: any;
onSubmit: (data: any) => void;
loading?: boolean;
}
const ClientForm: React.FC<ClientFormProps> = ({
initialData,
onSubmit,
loading = false,
}) => {
const {
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: yupResolver(clientSchema),
defaultValues: initialData || {
nom: '',
adresse: '',
telephone: '',
email: '',
},
});
return (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Informations Client
</Typography>
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
<FormTextField
name="nom"
label="Nom de l'entreprise"
control={control}
required
/>
<FormTextField
name="adresse"
label="Adresse"
control={control}
multiline
rows={3}
/>
<FormTextField
name="telephone"
label="Téléphone"
control={control}
/>
<FormTextField
name="email"
label="Email"
control={control}
/>
<Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
<Button
type="submit"
variant="contained"
disabled={isSubmitting || loading}
>
{isSubmitting ? 'Enregistrement...' : 'Enregistrer'}
</Button>
<Button
type="button"
variant="outlined"
disabled={isSubmitting || loading}
>
Annuler
</Button>
</Box>
</Box>
</Paper>
);
};
ReclamationForm
const reclamationSchema = yup.object().shape({
siteId: yup.number().required('Le site est obligatoire'),
type: yup.string().required('Le type est obligatoire'),
montant: yup.number().positive('Le montant doit être positif').required('Le montant est obligatoire'),
dateReclamation: yup.date().required('La date est obligatoire'),
description: yup.string().optional(),
});
const ReclamationForm: React.FC<ReclamationFormProps> = ({
sites,
onSubmit,
loading = false,
}) => {
const {
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: yupResolver(reclamationSchema),
defaultValues: {
siteId: '',
type: '',
montant: '',
dateReclamation: new Date(),
description: '',
},
});
const typeOptions = [
{ value: 'TAXE_FONCIERE', label: 'Taxe Foncière' },
{ value: 'TAXE_HABITATION', label: 'Taxe d\'Habitation' },
{ value: 'CFE', label: 'CFE' },
];
return (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Nouvelle Réclamation
</Typography>
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
<FormSelect
name="siteId"
label="Site"
control={control}
options={sites.map(site => ({
value: site.id,
label: site.nom,
}))}
required
/>
<FormSelect
name="type"
label="Type de réclamation"
control={control}
options={typeOptions}
required
/>
<FormTextField
name="montant"
label="Montant (€)"
control={control}
type="number"
required
/>
<FormDatePicker
name="dateReclamation"
label="Date de réclamation"
control={control}
required
/>
<FormTextField
name="description"
label="Description"
control={control}
multiline
rows={4}
/>
<Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
<Button
type="submit"
variant="contained"
disabled={isSubmitting || loading}
>
{isSubmitting ? 'Création...' : 'Créer la réclamation'}
</Button>
</Box>
</Box>
</Paper>
);
};
🎨 Composants de validation
FormErrorDisplay
import { Alert, AlertTitle } from '@mui/material';
interface FormErrorDisplayProps {
errors: any;
title?: string;
}
const FormErrorDisplay: React.FC<FormErrorDisplayProps> = ({
errors,
title = 'Erreurs de validation',
}) => {
if (!errors || Object.keys(errors).length === 0) {
return null;
}
return (
<Alert severity="error" sx={{ mb: 2 }}>
<AlertTitle>{title}</AlertTitle>
<ul style={{ margin: 0, paddingLeft: 20 }}>
{Object.entries(errors).map(([field, error]: [string, any]) => (
<li key={field}>
<strong>{field}:</strong> {error.message}
</li>
))}
</ul>
</Alert>
);
};
FormSuccessMessage
import { Alert, AlertTitle } from '@mui/material';
interface FormSuccessMessageProps {
message: string;
onClose?: () => void;
}
const FormSuccessMessage: React.FC<FormSuccessMessageProps> = ({
message,
onClose,
}) => {
return (
<Alert severity="success" onClose={onClose} sx={{ mb: 2 }}>
<AlertTitle>Succès</AlertTitle>
{message}
</Alert>
);
};
🧪 Tests des composants
Tests unitaires
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ClientForm } from './ClientForm';
describe('ClientForm', () => {
it('should render all form fields', () => {
render(<ClientForm onSubmit={jest.fn()} />);
expect(screen.getByLabelText(/nom de l'entreprise/i)).toBeInTheDocument();
expect(screen.getByLabelText(/adresse/i)).toBeInTheDocument();
expect(screen.getByLabelText(/téléphone/i)).toBeInTheDocument();
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
});
it('should validate required fields', async () => {
const user = userEvent.setup();
render(<ClientForm onSubmit={jest.fn()} />);
const submitButton = screen.getByRole('button', { name: /enregistrer/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText('Le nom est obligatoire')).toBeInTheDocument();
});
});
it('should submit form with valid data', async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<ClientForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/nom de l'entreprise/i), 'Test Client');
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.click(screen.getByRole('button', { name: /enregistrer/i }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
nom: 'Test Client',
adresse: '',
telephone: '',
email: 'test@example.com',
});
});
});
});
📚 Bonnes pratiques
Structure des formulaires
- Validation : Utilisez Yup pour la validation côté client
- Gestion d'état : Utilisez React Hook Form pour la gestion d'état
- Accessibilité : Assurez-vous que tous les champs ont des labels appropriés
- Feedback utilisateur : Affichez des messages d'erreur clairs et utiles
Performance
- Memoization : Utilisez React.memo pour les composants de formulaire
- Lazy loading : Chargez les options de sélection de manière asynchrone
- Debouncing : Implémentez le debouncing pour les champs de recherche
Sécurité
- Validation côté serveur : Toujours valider les données côté serveur
- Sanitisation : Nettoyez les entrées utilisateur
- CSRF Protection : Utilisez des tokens CSRF pour les formulaires
Dernière mise à jour : Décembre 2024