L'état des champs des Signal Forms vous permet de réagir aux interactions de l'utilisateur en fournissant des signals réactifs pour le statut de validation (comme valid, invalid, errors), le suivi des interactions (comme touched, dirty) et la disponibilité (comme disabled, hidden).
Comprendre l'état des champs
Lorsque vous créez un formulaire avec la fonction form(), elle renvoie un arbre de champs (field tree) : une structure d'objets qui reproduit votre model de formulaire. Chaque champ de l'arbre est accessible avec la notation pointée (comme form.email).
Accéder à l'état d'un champ
Lorsque vous appelez un champ de l'arbre comme une fonction (comme form.email()), il renvoie un objet FieldState contenant des signals réactifs qui suivent l'état de validation, d'interaction et de disponibilité du champ. Par exemple, le signal invalid() vous indique si le champ comporte des erreurs de validation :
import {Component, signal} from '@angular/core';
import {form, FormField, required, email} from '@angular/forms/signals';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `
<input type="email" [formField]="registrationForm.email" />
@if (registrationForm.email().invalid()) {
<p class="error">Email has validation errors:</p>
<ul>
@for (error of registrationForm.email().errors(); track error) {
<li>{{ error.message }}</li>
}
</ul>
}
`,
})
export class Registration {
registrationModel = signal({
email: '',
password: '',
});
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Enter a valid email address'});
});
}
Dans cet exemple, le template vérifie registrationForm.email().invalid() pour déterminer s'il faut afficher un message d'erreur.
Signals d'état des champs
Le signal le plus utilisé est value(), un WritableSignal qui donne accès à la valeur courante du champ :
const emailValue = registrationForm.email().value();
console.log(emailValue); // Current email string
Au-delà de value(), l'état du champ comprend des signals pour la validation, le suivi des interactions et le contrôle de la disponibilité :
| Catégorie | Signal | Description |
|---|---|---|
| Validation | valid() |
Le champ respecte toutes les règles de validation et n'a aucun validateur en attente |
invalid() |
Le champ comporte des erreurs de validation | |
errors() |
Tableau d'objets d'erreur de validation | |
pending() |
Validation asynchrone en cours | |
| Interaction | touched() |
L'utilisateur a donné puis retiré le focus au champ (s'il est interactif) |
dirty() |
L'utilisateur a modifié le champ (s'il est interactif), même si la valeur correspond à l'état initial | |
| Disponibilité | disabled() |
Le champ est désactivé et n'affecte pas l'état du formulaire parent |
hidden() |
Indique que le champ doit être masqué ; la visibilité dans le template se contrôle avec @if |
|
readonly() |
Le champ est en lecture seule et n'affecte pas l'état du formulaire parent |
Ces signals vous permettent de créer des expériences de formulaire réactives qui s'adaptent au comportement de l'utilisateur. Les sections suivantes détaillent chaque catégorie.
État de validation
Les signals d'état de validation vous indiquent si un champ est valide et quelles erreurs il contient.
Note : Ce guide se concentre sur l'utilisation de l'état de validation dans vos templates et votre logique (comme la lecture de valid(), invalid(), errors() pour afficher un retour). Pour savoir comment définir des règles de validation et créer des validateurs personnalisés, consultez le guide de validation.
Vérifier la validité
Utilisez valid() et invalid() pour vérifier le statut de validation :
@Component({
template: `
<input type="email" [formField]="loginForm.email" />
@if (loginForm.email().invalid()) {
<p class="error">Email is invalid</p>
}
@if (loginForm.email().valid()) {
<p class="success">Email looks good</p>
}
`,
})
export class Login {
loginModel = signal({email: '', password: ''});
loginForm = form(this.loginModel);
}
| Signal | Renvoie true quand |
|---|---|
valid() |
Le champ respecte toutes les règles de validation et n'a aucun validateur en attente |
invalid() |
Le champ comporte des erreurs de validation |
Pour vérifier la validité dans le code, utilisez invalid() plutôt que !valid() si vous voulez distinguer « a des erreurs » de « validation en attente ». En effet, valid() et invalid() peuvent tous deux valoir false en même temps lorsqu'une validation asynchrone est en attente : le champ n'est pas encore valide puisque la validation n'est pas terminée, et il n'est pas non plus invalide puisqu'aucune erreur n'a encore été trouvée.
Lire les erreurs de validation
Accédez au tableau des erreurs de validation avec errors(), ou à une erreur précise avec getError(). Chaque objet d'erreur contient :
| Propriété | Description |
|---|---|
kind |
La règle de validation qui a échoué (comme « required » ou « email ») |
message |
Message d'erreur lisible, facultatif |
fieldTree |
Référence au FieldTree où l'erreur s'est produite |
Note : La propriété message est facultative. Les validateurs peuvent fournir des messages d'erreur personnalisés, mais si aucun n'est précisé, vous devrez peut-être associer les valeurs kind des erreurs à vos propres messages.
Voici un exemple qui affiche dans votre template les erreurs à partir d'un tableau d'erreurs de validation :
@Component({
template: `
<input type="email" [formField]="loginForm.email" />
@if (loginForm.email().errors().length > 0) {
<div class="errors">
@for (error of loginForm.email().errors(); track error) {
<p>{{ error.message }}</p>
}
</div>
}
`
})
Cette approche parcourt toutes les erreurs d'un champ et affiche chaque message d'erreur à l'utilisateur.
Voici un exemple qui affiche une erreur dans votre template avec getError() :
@Component({
template: `
<input type="email" [formField]="loginForm.email" />
@if (loginForm.email().getError('required'); as error) {
<div class="errors">
<p>{{ error.message }}</p>
</div>
}
`
})
getError() accepte n'importe quelle chaîne comme kind d'erreur, et reconnaît les types d'erreur des validations intégrées (par exemple « required », « email », « minLength »).
Validation en attente
Le signal pending() indique qu'une validation asynchrone est en cours :
@Component({
template: `
<input type="email" [formField]="signupForm.email" />
@if (signupForm.email().pending()) {
<p>Checking if email is available...</p>
}
@if (signupForm.email().invalid() && !signupForm.email().pending()) {
<p>Email is already taken</p>
}
`
})
Ce signal vous permet d'afficher des états de chargement pendant l'exécution de la validation asynchrone.
État d'interaction
L'état d'interaction indique si l'utilisateur a interagi avec les champs, ce qui permet des patterns comme « afficher les erreurs seulement après que l'utilisateur a touché un champ ».
État touché
Le signal touched() indique si l'utilisateur a donné puis retiré le focus à un champ, ou si le champ a été marqué comme touché par programmation. Seuls les champs interactifs peuvent devenir touchés ; les champs masqués, désactivés et en lecture seule ne deviennent pas touchés à la suite d'interactions de l'utilisateur ni de markAsTouched().
Lorsqu'une action au niveau d'une section doit révéler les erreurs de validation de cette section, appelez markAsTouched() sur le champ de la section. La valeur par défaut de skipDescendants est false : l'appel marque donc comme touchés le champ de la section et chacun de ses champs descendants.
Par exemple, un parcours de paiement peut valider la section de livraison avant de permettre à l'utilisateur de passer à l'étape suivante :
import {Component, signal} from '@angular/core';
import {form, FormField, required} from '@angular/forms/signals';
@Component({
selector: 'app-checkout-shipping',
imports: [FormField],
template: `
<label>
Name
<input [formField]="checkoutForm.shipping.name" />
</label>
@if (checkoutForm.shipping.name().touched() && checkoutForm.shipping.name().invalid()) {
<p>{{ checkoutForm.shipping.name().errors()[0].message }}</p>
}
<label>
Address
<input [formField]="checkoutForm.shipping.address" />
</label>
@if (checkoutForm.shipping.address().touched() && checkoutForm.shipping.address().invalid()) {
<p>{{ checkoutForm.shipping.address().errors()[0].message }}</p>
}
<button type="button" (click)="continueToPayment()">Continue</button>
@if (showPayment() && checkoutForm.shipping().valid()) {
<p>Ready for payment.</p>
}
`,
})
export class CheckoutShipping {
checkoutModel = signal({
shipping: {
name: '',
address: '',
},
});
showPayment = signal(false);
checkoutForm = form(this.checkoutModel, (schemaPath) => {
required(schemaPath.shipping.name, {message: 'Enter a name'});
required(schemaPath.shipping.address, {message: 'Enter an address'});
});
continueToPayment() {
this.checkoutForm.shipping().markAsTouched();
if (this.checkoutForm.shipping().invalid()) {
return;
}
this.showPayment.set(true);
}
}
Lorsque continueToPayment() appelle markAsTouched() sur checkoutForm.shipping(), il utilise le comportement par défaut skipDescendants: false. Angular marque shipping, shipping.name et shipping.address comme touchés : les messages d'erreur conditionnés par touched() && invalid() sur les champs enfants deviennent donc visibles avant la soumission du formulaire complet.
Note : Passez {skipDescendants: true} uniquement lorsque le champ qui reçoit l'appel doit devenir touché sans modifier l'état touché de ses descendants.
État modifié (dirty)
Les formulaires doivent souvent détecter si les données ont réellement changé, par exemple pour avertir l'utilisateur de modifications non enregistrées ou pour n'activer un bouton d'enregistrement que lorsque c'est nécessaire. Le signal dirty() indique si l'utilisateur a modifié le champ.
Le signal dirty() passe à true lorsque l'utilisateur modifie la valeur d'un champ interactif, et reste à true même si la valeur est ramenée à sa valeur initiale :
@Component({
template: `
<form novalidate>
<input [formField]="profileForm.name" />
<input [formField]="profileForm.bio" />
@if (profileForm().dirty()) {
<p class="warning">You have unsaved changes</p>
}
</form>
`,
})
export class Profile {
profileModel = signal({name: 'Alice', bio: 'Developer'});
profileForm = form(this.profileModel);
}
Utilisez dirty() pour les avertissements de « modifications non enregistrées » ou pour n'activer les boutons d'enregistrement que lorsque les données ont changé.
Touché ou modifié (dirty)
Ces signals suivent différents types d'état d'interaction :
| Signal | Quand il passe à true |
|---|---|
touched() |
L'utilisateur a donné puis retiré le focus à un champ interactif, ou le champ a été marqué comme touché par programmation |
dirty() |
L'utilisateur a modifié un champ interactif (même s'il n'en a jamais retiré le focus, et même si la valeur courante correspond à la valeur initiale) |
Un champ peut se trouver dans différentes combinaisons :
| État | Scénario |
|---|---|
| Touché mais non modifié | L'utilisateur a donné puis retiré le focus au champ sans rien modifier |
| Touché et modifié | L'utilisateur a donné le focus au champ, modifié la valeur, puis retiré le focus |
Note : Les champs masqués, désactivés et en lecture seule ne sont pas interactifs : ils ne deviennent ni touchés ni modifiés à la suite d'interactions de l'utilisateur.
État de disponibilité
Les signals d'état de disponibilité déterminent si les champs sont interactifs, modifiables ou visibles. Les champs désactivés, masqués et en lecture seule ne sont pas interactifs. Ils n'influencent pas le fait que leur formulaire parent soit valide, touché ou modifié.
Champs désactivés
Le signal disabled() indique si un champ accepte les saisies de l'utilisateur. Les champs désactivés apparaissent dans l'interface, mais l'utilisateur ne peut pas interagir avec eux.
import {Component, signal} from '@angular/core';
import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-order',
imports: [FormField],
// TIP: The `[formField]` directive automatically binds the `disabled` attribute based
// on the field's `disabled()` state, so you don't need to manually add `[disabled]="field().disabled()"`
template: `
<input [formField]="orderForm.couponCode" />
@if (orderForm.couponCode().disabled()) {
<p class="info">Coupon code is only available for orders over $50</p>
}
`,
})
export class Order {
orderModel = signal({
total: 25,
couponCode: '',
});
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.couponCode, {when: ({valueOf}) => valueOf(schemaPath.total) < 50});
});
}
Dans cet exemple, nous utilisons valueOf(schemaPath.total) pour lire la valeur du champ total et déterminer si couponCode doit être désactivé.
Note : Le paramètre du callback de schéma (schemaPath dans ces exemples) est un objet SchemaPathTree qui fournit les chemins vers tous les champs de votre formulaire. Vous pouvez nommer ce paramètre comme vous le souhaitez.
Lorsque vous définissez des règles comme disabled(), hidden() ou readonly(), la fonction when reçoit un objet FieldContext, généralement déstructuré (comme ({valueOf})). Deux méthodes couramment utilisées dans les règles de validation sont :
valueOf(schemaPath.otherField)- Lit la valeur d'un autre champ du formulairevalue()- Un signal contenant la valeur du champ auquel la règle s'applique
Les champs désactivés ne contribuent pas à l'état de validation du formulaire parent. Même si un champ désactivé était invalide, le formulaire parent peut quand même être valide. L'état disabled() affecte l'interactivité et la validation, mais ne modifie pas la valeur du champ.
Champs masqués
Le signal hidden() indique si un champ est masqué selon une condition. Utilisez hidden() avec @if pour afficher ou masquer des champs selon des conditions :
import {Component, signal} from '@angular/core';
import {form, FormField, hidden} from '@angular/forms/signals';
@Component({
selector: 'app-profile',
imports: [FormField],
template: `
<label>
<input type="checkbox" [formField]="profileForm.isPublic" />
Make profile public
</label>
@if (!profileForm.publicUrl().hidden()) {
<label>
Public URL
<input [formField]="profileForm.publicUrl" />
</label>
}
`,
})
export class Profile {
profileModel = signal({
isPublic: false,
publicUrl: '',
});
profileForm = form(this.profileModel, (schemaPath) => {
hidden(schemaPath.publicUrl, {when: ({valueOf}) => !valueOf(schemaPath.isPublic)});
});
}
Les champs masqués ne participent pas à la validation. Si un champ obligatoire est masqué, il n'empêche pas la soumission du formulaire. L'état hidden() affecte la disponibilité et la validation, mais ne modifie pas la valeur du champ.
Champs en lecture seule
Le signal readonly() indique si un champ est en lecture seule. Les champs en lecture seule affichent leur valeur, mais l'utilisateur ne peut pas les modifier :
import {Component, signal} from '@angular/core';
import {form, FormField, readonly} from '@angular/forms/signals';
@Component({
selector: 'app-account',
imports: [FormField],
template: `
<label>
Username (cannot be changed)
<input [formField]="accountForm.username" />
</label>
<label>
Email
<input [formField]="accountForm.email" />
</label>
`,
})
export class Account {
accountModel = signal({
username: 'johndoe',
email: 'john@example.com',
});
accountForm = form(this.accountModel, (schemaPath) => {
readonly(schemaPath.username);
});
}
Note : La directive [formField] lie automatiquement l'attribut readonly en fonction de l'état readonly() du champ : vous n'avez donc pas besoin d'ajouter manuellement [readonly]="field().readonly()".
Comme les champs désactivés et masqués, les champs en lecture seule ne sont pas interactifs et n'affectent pas l'état du formulaire parent. L'état readonly() affecte la possibilité de modification et la validation, mais ne modifie pas la valeur du champ.
Quand utiliser chaque état
| État | À utiliser quand | Visible par l'utilisateur | Interaction possible | Pris en compte dans la validation |
|---|---|---|---|---|
disabled() |
Le champ est temporairement indisponible (par exemple en fonction des valeurs d'autres champs) | Oui | Non | Non |
hidden() |
Le champ n'est pas pertinent dans le contexte actuel | Non (avec @if) | Non | Non |
readonly() |
La valeur doit être visible mais non modifiable | Oui | Non | Non |
État au niveau du formulaire
Le formulaire racine est lui aussi un champ de l'arbre de champs. Lorsque vous l'appelez comme une fonction, il renvoie lui aussi un objet FieldState qui agrège l'état de tous les champs enfants.
Accéder à l'état du formulaire
@Component({
template: `
<form novalidate>
<input [formField]="loginForm.email" />
<input [formField]="loginForm.password" />
<button [disabled]="!loginForm().valid()">Sign In</button>
</form>
`,
})
export class Login {
loginModel = signal({email: '', password: ''});
loginForm = form(this.loginModel);
}
Dans cet exemple, le formulaire n'est valide que lorsque tous les champs enfants sont valides. Cela vous permet d'activer ou de désactiver les boutons de soumission en fonction de la validité globale du formulaire.
Signals au niveau du formulaire
Comme le formulaire racine est un champ, il possède les mêmes signals (comme valid(), invalid(), touched(), dirty(), etc.).
| Signal | Comportement au niveau du formulaire |
|---|---|
valid() |
Tous les champs interactifs sont valides et aucun validateur n'est en attente |
invalid() |
Au moins un champ interactif comporte des erreurs de validation |
pending() |
Au moins un champ interactif a une validation asynchrone en attente |
touched() |
Le formulaire, ou au moins un descendant interactif, est touché |
dirty() |
L'utilisateur a modifié au moins un champ interactif |
Quand utiliser le niveau formulaire ou le niveau champ
Utilisez l'état au niveau du formulaire pour :
- L'état activé/désactivé du bouton de soumission
- L'état du bouton « Enregistrer »
- Les vérifications de validité globale du formulaire
- Les avertissements de modifications non enregistrées
Utilisez l'état au niveau du champ pour :
- Les messages d'erreur propres à chaque champ
- Les styles spécifiques à un champ
- Le retour de validation champ par champ
- La disponibilité conditionnelle des champs
Propagation de l'état
L'état des champs se propage des champs enfants vers le formulaire racine, en passant par les groupes de champs parents.
Comment l'état des enfants affecte les formulaires parents
Lorsqu'un champ enfant devient invalide, son groupe de champs parent devient invalide, tout comme le formulaire racine. Lorsqu'un enfant devient touché ou modifié, le groupe de champs parent et le formulaire racine reflètent ce changement. Cette agrégation vous permet de vérifier la validité à n'importe quel niveau : un champ ou le formulaire entier.
const userModel = signal({
profile: {
firstName: '',
lastName: '',
},
address: {
street: '',
city: '',
},
});
const userForm = form(userModel);
// If firstName is invalid, profile is invalid
userForm.profile.firstName().invalid() === true;
// → userForm.profile().invalid() === true
// → userForm().invalid() === true
Champs masqués, désactivés et en lecture seule
Les champs masqués, désactivés et en lecture seule ne sont pas interactifs et n'affectent pas l'état du formulaire parent :
const orderModel = signal({
customerName: '',
requiresShipping: false,
shippingAddress: '',
});
const orderForm = form(orderModel, (schemaPath) => {
hidden(schemaPath.shippingAddress, {when: ({valueOf}) => !valueOf(schemaPath.requiresShipping)});
});
Dans cet exemple, lorsque shippingAddress est masqué, il n'affecte pas la validité du formulaire. Ainsi, même si shippingAddress est vide et obligatoire, le formulaire peut être valide.
Ce comportement empêche les champs masqués, désactivés ou en lecture seule de bloquer la soumission du formulaire ou d'affecter la validation et les états touché et modifié.
Utiliser l'état dans les templates
Les signals d'état des champs s'intègrent naturellement aux templates Angular et permettent des expériences de formulaire réactives sans gestion manuelle des événements.
Affichage conditionnel des erreurs
Affichez les erreurs uniquement après que l'utilisateur a interagi avec un champ :
import {Component, signal} from '@angular/core';
import {form, FormField, email} from '@angular/forms/signals';
@Component({
selector: 'app-signup',
imports: [FormField],
template: `
<label>
Email
<input type="email" [formField]="signupForm.email" />
</label>
@if (signupForm.email().touched() && signupForm.email().invalid()) {
<p class="error">{{ signupForm.email().errors()[0].message }}</p>
}
`,
})
export class Signup {
signupModel = signal({email: '', password: ''});
signupForm = form(this.signupModel, (schemaPath) => {
email(schemaPath.email);
});
}
Ce pattern évite d'afficher des erreurs avant que l'utilisateur ait eu l'occasion d'interagir avec le champ. Les erreurs n'apparaissent qu'une fois que l'utilisateur a donné le focus au champ puis l'a quitté.
Disponibilité conditionnelle des champs
Utilisez le signal hidden() avec @if pour afficher ou masquer des champs de façon conditionnelle :
import {Component, signal} from '@angular/core';
import {form, FormField, hidden} from '@angular/forms/signals';
@Component({
selector: 'app-order',
imports: [FormField],
template: `
<label>
<input type="checkbox" [formField]="orderForm.requiresShipping" />
Requires shipping
</label>
@if (!orderForm.shippingAddress().hidden()) {
<label>
Shipping Address
<input [formField]="orderForm.shippingAddress" />
</label>
}
`,
})
export class Order {
orderModel = signal({
requiresShipping: false,
shippingAddress: '',
});
orderForm = form(this.orderModel, (schemaPath) => {
hidden(schemaPath.shippingAddress, {
when: ({valueOf}) => !valueOf(schemaPath.requiresShipping),
});
});
}
Les champs masqués ne participent pas à la validation, ce qui permet de soumettre le formulaire même si le champ masqué serait autrement invalide.
Suivre les valeurs des champs de tableau
Dans les Signal Forms, un bloc @for qui parcourt un ensemble de champs doit être suivi par l'identité des champs.
@Component({
imports: [FormField],
template: `
@for (field of form.emails; track field) {
<input [formField]="field" />
}
`,
})
export class App {
formModel = signal({emails: ['john.doe@mail.com', 'max.musterman@mail.com']});
form = form(this.formModel);
}
Le système de formulaires suit déjà les valeurs du model dans le tableau et maintient automatiquement une identité stable pour les champs qu'il crée.
Lorsqu'un élément change, il peut représenter une nouvelle entité logique même si certaines de ses propriétés semblent identiques. Le suivi par identité garantit que le framework le traite comme un élément distinct au lieu de réutiliser des éléments d'interface existants. Cela évite que des éléments avec état, comme les champs de formulaire, soient partagés à tort et garde les bindings alignés sur la bonne partie du model.
Utiliser l'état des champs dans la logique du composant
Les signals d'état des champs fonctionnent avec les primitives réactives d'Angular comme computed() et effect() pour une logique de formulaire avancée.
Vérifications de validation avant la soumission
Vérifiez la validité du formulaire dans les méthodes du composant :
export class Registration {
registrationModel = signal({
username: '',
email: '',
password: '',
});
registrationForm = form(this.registrationModel);
async onSubmit() {
// Wait for any pending async validation
if (this.registrationForm().pending()) {
console.log('Waiting for validation...');
return;
}
// Guard against invalid submissions
if (this.registrationForm().invalid()) {
console.error('Form is invalid');
return;
}
const data = this.registrationModel();
await this.api.register(data);
}
}
Cela garantit que seules des données valides et entièrement validées atteignent votre API.
État dérivé avec computed
Créez des signals computed basés sur l'état des champs pour qu'ils se mettent à jour automatiquement quand l'état sous-jacent des champs change :
export class Password {
passwordModel = signal({password: '', confirmPassword: ''});
passwordForm = form(this.passwordModel);
// Compute password strength indicator
passwordStrength = computed(() => {
const password = this.passwordForm.password().value();
if (password.length < 8) return 'weak';
if (password.length < 12) return 'medium';
return 'strong';
});
// Check if all required fields are filled
allFieldsFilled = computed(() => {
return (
this.passwordForm.password().value().length > 0 &&
this.passwordForm.confirmPassword().value().length > 0
);
});
}
Modifier l'état par programmation
Si l'état des champs se met généralement à jour via les interactions de l'utilisateur (saisie, prise et perte du focus), vous devez parfois le contrôler par programmation. Les cas courants incluent la soumission et la réinitialisation des formulaires.
Soumission du formulaire
Les Signal Forms fournissent une directive FormRoot qui simplifie la soumission des formulaires. Elle empêche automatiquement le comportement de soumission par défaut du navigateur et définit l'attribut novalidate sur l'élément <form>.
import {FormField, FormRoot, form, required, email} from '@angular/forms/signals';
import {Component, signal} from '@angular/core';
@Component({
imports: [FormRoot, FormField],
template: `
<form [formRoot]="registrationForm">
<input [formField]="registrationForm.username" />
<input type="email" [formField]="registrationForm.email" />
<input type="password" [formField]="registrationForm.password" />
<button type="submit">Register</button>
</form>
`,
})
export class Registration {
registrationModel = signal({username: '', email: '', password: ''});
registrationForm = form(
this.registrationModel,
(schemaPath) => {
required(schemaPath.username);
email(schemaPath.email);
required(schemaPath.password);
},
{
submission: {
action: async () => this.submitToServer(),
},
},
);
private submitToServer() {
// Send data to server
}
}
Lorsque vous utilisez FormRoot, soumettre le formulaire appelle automatiquement la fonction submit(), qui marque tous les champs comme touchés (ce qui révèle les erreurs de validation) et exécute votre callback action si le formulaire est valide.
Vous pouvez aussi soumettre un formulaire manuellement, sans utiliser la directive, en appelant submit(this.registrationForm). Lorsque vous appelez explicitement la fonction submit de cette façon, vous pouvez passer un FormSubmitOptions pour remplacer la logique de submission par défaut du formulaire : submit(this.registrationForm, {action: () => /* ... */ }).
Réinitialiser les formulaires après la soumission
Après avoir soumis un formulaire avec succès, vous pouvez vouloir le ramener à son état initial, en effaçant à la fois l'historique des interactions de l'utilisateur et les valeurs des champs. La méthode reset() réinitialise les indicateurs touché et modifié. Vous pouvez aussi passer une valeur facultative à reset() pour mettre à jour les données du model :
export class Contact {
private readonly INITIAL_MODEL = {name: '', email: '', message: ''};
contactModel = signal({...this.INITIAL_MODEL});
contactForm = form(this.contactModel, {
submission: {
action: async (f) => {
await this.api.sendMessage(this.contactModel());
// Clear interaction state (touched, dirty) and reset to initial values
f().reset({...this.INITIAL_MODEL});
},
},
});
}
Le formulaire est ainsi prêt pour une nouvelle saisie, sans afficher de messages d'erreur obsolètes ni d'indicateurs d'état modifié.
Appliquer des styles selon l'état de validation
Vous pouvez appliquer des styles personnalisés à votre formulaire en liant des classes CSS en fonction de l'état de validation :
import {Component, signal} from '@angular/core';
import {form, FormField, email} from '@angular/forms/signals';
@Component({
imports: [FormField],
template: `
<input
type="email"
[formField]="form.email"
[class.is-invalid]="form.email().touched() && form.email().invalid()"
[class.is-valid]="form.email().touched() && form.email().valid()"
/>
`,
styles: `
input.is-invalid {
border: 2px solid red;
background-color: white;
}
input.is-valid {
border: 2px solid green;
}
`,
})
export class StyleExample {
model = signal({email: ''});
form = form(this.model, (schemaPath) => {
email(schemaPath.email);
});
}
Vérifier à la fois touched() et l'état de validation garantit que les styles n'apparaissent qu'après que l'utilisateur a interagi avec le champ.
Donner le focus au contrôle de formulaire lié à un champ
Les Signal Forms d'Angular fournissent sur l'état du champ une méthode focusBoundControl() qui vous permet de déplacer par programmation le focus vers le contrôle de formulaire associé à un champ donné.
Un cas d'usage courant consiste à améliorer l'accessibilité lors de la soumission d'un formulaire : lorsque le formulaire est invalide, affichez les messages d'erreur et déplacez automatiquement le focus vers le premier champ invalide pour guider l'utilisateur dans sa correction.
Utilisation de base
Soit un formulaire d'inscription :
@Component({/* ... */})
export class Registration {
registrationModel = signal({username: '', email: '', password: ''});
registrationForm = form(this.registrationModel, (schemaPath) => {
required(schemaPath.username);
email(schemaPath.email);
required(schemaPath.password);
});
}
Pour déplacer le focus vers le contrôle lié au champ email :
registrationForm.email().focusBoundControl();
Empêcher le défilement
Si le contrôle ciblé est en dehors du viewport et que vous voulez lui donner le focus sans déclencher de défilement, vous pouvez définir l'option preventScroll à true lors de l'appel à la méthode focusBoundControl().
registrationForm.email().focusBoundControl({preventScroll: true});
Donner le focus au premier champ invalide lors de la soumission
Utilisez errorSummary() pour trouver le premier champ invalide et lui donner le focus lorsque l'utilisateur soumet le formulaire avec des erreurs :
onSubmit() {
const firstError = this.registrationForm().errorSummary()[0];
if (firstError?.fieldTree) {
firstError.fieldTree().focusBoundControl();
} else {
// proceed with submission
}
}
Contrôles personnalisés
Par défaut, appeler focusBoundControl() sur un contrôle personnalisé n'a aucun effet, car un contrôle personnalisé peut contenir plusieurs champs natifs. Par exemple, un sélecteur de date peut contenir des champs distincts pour le jour, le mois et l'année. Angular ne peut donc pas déterminer quel élément doit recevoir le focus ni quelle action effectuer.
Pour prendre en charge le focus par programmation dans un contrôle personnalisé, implémentez une méthode focus(). Lorsque focusBoundControl() est appelée sur l'état du champ associé à un contrôle personnalisé, Angular appelle la méthode focus() du contrôle si elle existe.
Prenons un champ de mot de passe personnalisé :
<div class="password-block">
<input type="password" #passwordCtrl [value]="value()" (input)="value.set($event.target.value)" />
</div>
@Component({/* ... */})
export class PasswordInput implements FormValueControl<string> {
readonly value = model<string>('');
readonly passwordCtrl = viewChild.required<ElementRef<HTMLInputElement>>('passwordCtrl');
// Called automatically when focusBoundControl() is invoked
// on the field state associated with this custom control
focus(): void {
this.passwordCtrl().nativeElement.focus();
}
}
Prochaines étapes
Ce guide a couvert la gestion des statuts de validation et de disponibilité, le suivi des interactions et la propagation de l'état des champs. D'autres guides explorent d'autres aspects des Signal Forms :