Aller au contenu principal

Frontend

Traductions

Voir la section traduction

Angular

  • Bien penser à Unsubscribe les souscriptions lors de la destruction d'un composant, pour cela utiliser SubSinkAdapter (étendre la classe et utiliser this.subs.add(ma_souscription) )
    Les souscriptions suivantes sont fermées automatiquement :
    - appel HttpClient
    - async Pipe dans un template
  • Utilisation d'un Resolver lors de l'accès à une ressource unitaire afin d'éviter une navigation arrière ou une page vide si la ressource n'existe pas (ex: détail d'un évènement).

Formulaire

Utilisation des Reactive Forms (docs) plutôt que les template-driven forms.

Structure

Voici une idée de la forme globale d'un formulaire. Des explications plus précises sont donnés sur les parties suivantes.

<form [formGroup]="form">
<!-- legende lorsqu'il y a au moins un champ obligatoire -->
<shared-required-fields-legend />
<!-- bloc d'affichage des erreurs -->
<div class="mb-3" *ngIf="error">
<shared-simple-alert type="error" [message]="error"></shared-simple-alert>
</div>
<!-- Champs du formulaire -->
<div class="mb-3">
<!-- appFormLabel indique que le champ est obligatoire -->
<label
for="modal-cancel-ouverture-reason"
class="form-label"
appFormLabel
>...</label
>
<!-- is-invalid permet d'avoir les styles bootstrap lorsque le champ est en erreur-->
<!-- (accessibilité) aria-invalid permet de connaitre la validité du champ -->
<!-- (accessibilité) aria-describedby permet d'ajouter un lien vers le conteneur des messages d'erreur -->
<textarea
class="form-control"
id="modal-cancel-ouverture-reason"
formControlName="reason"
[ngClass]="{
'is-invalid':
form.controls.reason.touched && form.controls.reason.invalid
}"
[attr.aria-invalid]="
form.controls.reason.touched && form.controls.reason.invalid
"
aria-describedby="modal-cancel-ouverture-reason-errors"
required
></textarea>
<!-- conteneur des erreurs de ce champ -->
<shared-errors id="modal-cancel-ouverture-reason-errors">
<!-- si on test la présence obligatoire d'une valeur on peut utiliser ce composant -->
<shared-required-error
*ngIf="form.controls.reason.touched && form.controls.reason.errors?.['required']"
></shared-required-error>
<!-- ici on peut rajouter d'autres types d'erreurs -->
<shared-error *ngIf="monTest">Mon message d'erreur spécifique</shared-error>
</shared-errors>
</div>
</form>

Id des champs de formulaire des modal

Les ids utilisés pour ces champs doivent être préfixés par le nom du modal. Cela permet d'éviter d'avoir des doublons d'id (HTML non valide) si la page appelant le modal contient deja un formulaire avec cet id.
Ici on voit que l'id est préfixé, par contre le formControlName est name puisqu'il est dans le scope du formulaire du composant. L'attribut aria-describedby (présenté plus bas) utilise aussi le préfixe.

<input
type="text"
class="form-control"
id="modal-description-edition-name"
formControlName="name"
[ngClass]="{
'is-invalid':
form.controls.name.touched && form.controls.name.invalid
}"
[attr.aria-invalid]="
form.controls.name.touched && form.controls.name.invalid
"
aria-describedby="modal-description-edition-name-errors"
required
/>

Affichage champs obligatoires / optionnels

Nous avons choisi de marquer les champs obligatoires et optionnel d'un repère visuel :

  • une asterisque * pour les champs obligatoires
  • un texte - Optionnel pour les champs optionnels

Pour marquer qu'un champ est obligatoire, il faut rajouter à son <label> l'attribut de la directive appFormLabel ou appFormLabel=required.
Pour marquer qu'un champ est optionnel, il faut rajouter à son <label> l'attribut de la directive appFormLabel=optional.
Un champ peut être conditionnellement obligatoire, dans ce cas, il faut "binder" l'attribut, ex : [appFormLabel]="isEdition ? 'required' : 'optional'" .

Une légende doit être indiqué en haut du formulaire lorsqu'il y a au moins un champ obligatoire.
Il faut utiliser le composant <shared-required-fields-legend />.

Composants

  • Radio group en ligne (shared)
<shared-inline-radio-group
[formGroup]="form"
[name]="'avenantAutorise'"
[label]="monLabel"
[required]="true"
></shared-inline-radio-group>
  • Date picker (librairie ngbootstrap)

Exemple minimal de configuration d'un datepicker.

<div class="input-group">
<input
formControlName="date"
ngbDatepicker
#modalOuvertureDatepicker="ngbDatepicker"
[minDate]="minDate"
/>
<button
class="btn btn-outline-secondary"
(click)="modalOuvertureDatepicker.toggle()"
type="button"
>
<fa-icon [icon]="icons.calendar"></fa-icon>
</button>
</div>

Accessibilité (A11y) avec ARIA

Voici les attributs minimums sont à utiliser :

  • required est suffisant pour la plupart des input, pour les radio-group il faut le placer sur l'element ayant le rôle radiogroup (il faut utiliser le composant shared-inline-radio-group qui gère ça).
  • aria-invalid permet d'indiquer que le champ est valide ou pas, il est fortement lié à la classe css d'invalidité qu'on utilise.
  • aria-describedby permet de rajouter des indications sur le champ, notamment les messages d'erreur et d'aide sur un champ (voir section plus bas)
  • (peu utilisé pour le moment) aria-labelledby permet de lier un champ à un label si aucun label n'est déjà relié avec l'attribut for .
  • (peu utilisé pour le moment) tabindex=0 permet de rajouter un element non dynamique au defilement avec la touche TAB , il est utilisé dans les radio group et sur la légende des champs obligatoires afin que ça soit focusable et annoncé au Screen Reader.

Dans Angular pour binder dynamiquement une valeur d'un attribut html il faut le prefixer par [attr.] , ex: [attr.aria-invalid] , si la valeur est statique alors on met directement aria-invalid.

Elément <input/> et <textarea/>

<!-- un id, ngClass + aria-invalid pour binder la validité du champ, aria-describedby pour pointer sur le container d'erreur (si présent), attribut required si nécessaire -->
<input
type="text"
class="form-control"
id="modal-description-edition-name"
formControlName="name"
[ngClass]="{
'is-invalid':
form.controls.name.touched && form.controls.name.invalid
}"
[attr.aria-invalid]="
form.controls.name.touched && form.controls.name.invalid
"
aria-describedby="modal-description-edition-name-errors"
required
/>

Element <radio/> - l'accessibilité n'est pas encore parfaite car plus complexe

<!-- sur le div ayant le role radiogroup : aria-required, aria-labelledby pointant sur le label, ng-class + aria-invalid, aria-describedby pointant sur le container d'erreur, tabindex=0 -->
<!-- sur le label à l'intérieur : un id -->
<!-- sur les inputs : un id + required ou non -->
<!-- sur les labels : l'attribut for pointant sur l'id de l'input -->
<div
[formGroup]="this.formGroup"
class="row align-items-center"
role="radiogroup"
[attr.aria-labelledby]="this.groupName"
[ngClass]="{
'is-invalid': this.control.touched && this.control.invalid
}"
aria-required="true"
[attr.aria-describedby]="this.errorContainerId"
[attr.aria-invalid]="this.control.touched && this.control.invalid"
tabindex="0"
>
<div
class="col-12 col-md-6 form-label"
[attr.id]="this.groupName"
[appFormLabel]="this.required ? 'required' : 'optional'"
>
{{ this.label }}
</div>
<div class="col">
<!-- input 1 -->
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
[formControlName]="this.name"
[attr.id]="trueValueId"
[value]="true"
[required]="this.required"
/>
<label class="form-check-label" [attr.for]="trueValueId">
{{ 'COMMON.FORM.RADIO_BUTTON.OUI' | translate }}
</label>
</div>
<!--... input 2 -->
</div>
</div>

Element <select/>

<!-- un id, ngclass + aria-invalid, aria-describedby, required (si nécessaire) -->
<select
id="modal-assign-role-role"
formControlName="role"
class="form-select"
[ngClass]="{
'is-invalid':
form.controls.role.touched && form.controls.role.invalid
}"
[attr.aria-invalid]="
form.controls.role.touched && form.controls.role.invalid
"
aria-describedby="modal-assign-role-role-errors"
required
>

Element <ng-select/> , accessibilité limitée par l'utilisation d'une librairie (état aria-invalid à améliorer)

<!-- labelForId (correspond à l'id qui sera donné à l'input), ngClass, [inputAttrs] permet de passer des attributs à l'input -> on y ajoute required et aria-describedby. -->
<ng-select
[items]="domainesList"
labelForId="domainesConvention"
bindLabel="libelle"
formControlName="domainesConvention"
class="form-control"
[ngClass]="{
'is-invalid':
form.controls.domainesConvention.touched &&
form.controls.domainesConvention.invalid
}"
[inputAttrs]="{
required: 'true',
'aria-describedby': 'domainesConvention-errors'
}"
>
</ng-select>

Lier les messages d'erreur

Lorsqu'un champ de formulaire contient des erreurs, il faut lier le container d'erreur au champ.
La convention pour l'id du container est : idchamp-errors.

<textarea id="monchamp" aria-describedby="monchamp-errors"></textarea>
<shared-errors id="monchamp-errors">
//... erreurs
</shared-errors>

Lier les messages d'aide

Lorsqu'un champ de formulaire est décrit par un champ d'aide il faut lier le champ de formulaire a son texte d'aide (ex: formulaire recherche utilisateur, on indique a l'utilisateur quoi renseigner dans le champ).
Ici on a également un champ d'erreur, l'attribut aria-describedby étant une liste d'id séparés par des espaces.
La convention pour l'id du message est : idchamp-help.

<input id="monchamp" aria-describedby="monchamp-help monchamp-errors"/>
<p id="monchamp-help" class="form-text">blabla</p>
<shared-errors id="monchamp-errors">
//... erreurs
</shared-errors>

Chargement et bouton submit

Pour éviter l'appel en doublon d'un service (double clic sur un bouton submit), il vaut mieux bloquer les boutons de formulaires avec un booléen de chargement.

Dans le composant

onSubmit() {
// reset des erreurs
this.error = '';
// on démarre le chargement
this.loading = true;
this.detailService
.updateDescription(this.form.getRawValue())
.pipe(finalize(() => (this.loading = false))) // on enleve le chargement
.subscribe({
next: () => {
// si succès
},
error: (error: ApiError) => {
// affichage du message d'erreur dans la page ou appel au service de notification selon le cas
this.error = error.message;
},
});
}

Dans le template

<button
[disabled]="!form.valid || isLoading"
type="button"
class="btn btn-primary"
(click)="onPrimaryAction()"
>