Aller au contenu principal

Components

Interne

Les composants partagés sont définis dans le dossier shared/components voir code

Editable Bloc

Ce composant sert à gérer l'édition de données en tant que bloc de la page de détail d'une ressource.

Après mise à jour avec succès du back-end, il faut mettre à jour le composant pour que les nouvelles valeurs par défaut du formulaire correspondent aux nouvelles valeurs sauvegardées. Si on omet cela, la fonction reset du formulaire appelée lors du clic sur le bouton "Annuler" va réinitialiser le formulaire à sa valeur initiale lors du premier chargement de la page.

this.editableBloc.resetState(this.form.getRawValue()); // ici on utilise form.getRawValue qui est la valeur actuelle du formulaire qui a été envoyé au back-end

Implémentation existantes

Il existe des modals pré-configuré à utiliser pour des cas récurrents dans le dossier src/app/shared/components/modal

Liste non exhaustive :

  • ModalReasonComponent : modal avec un champ de saisie de raison (ex : pour justifier une suppression)
  • ModalConfirmComponent : modal de confirmation simple avec message et bouton de confirmation
  • ModalConfirmBeforeDeactivateComponent : modal de confirmation avec message et bouton de confirmation, utilisée spécifiquement pour confirmer la fermeture d'une page

Implémenter un modal

Chaque modal est basé sur le composant ModalComponent pour le layout (src/app/shared/components/modal/modal.component.ts) et s'utilise avec le ModalService (src/app/core/components/modal/modal.service.ts). La classe BaseModalComponent (src/app/shared/components/modal/base-modal.component.ts) expose les méthodes de gestion du modal (dismiss, close) et permet d'homogénéiser l'implémentation des modals personnalisés. Le service ModalService rejette la promesse d'ouverture lorsque la modal est fermée (i.e: ce n'est pas une erreur).

Principes à suivre :

  • Créer un composant dédié pour chaque modal dans le dossier de la fonctionnalité concernée. Ce composant étend la classe mère BaseModalComponent.
  • Si des données d'entrée sont nécessaires, définir une interface TypeScript dans un fichier à côté du composant (ex : ModalFeatureData) et passer ce type en tant que paramètre générique à BaseModalComponent. La nouvelle devient accessible via la propriété data du composant.
  • Si la modal doit retourner un résultat, définir aussi un type ModalFeatureResult.
  • Pour charger des données asynchrones avant affichage, utiliser un observable suivant la convention de nommage firstContent$ et le pipe asDataLoadingResult() pour gérer les états loading/erreur dans le template. Passer ensuite cet observable au composant ModalComponent via la propriété contentLoading qui gèrera l'affichage des états.

Exemple de structure de composant modal :

@Component()
export class ModalFeatureComponent extends BaseModalComponent<ModalFeatureData> {
// only if preloading data is needed, use defer only if using this.data
private readonly _firstContent$ = defer(() =>
this._service.fetchAsyncData(this.data.item.id).pipe(asDataLoadingResult()),
);

protected onSubmit() {
//...
this._service.updateItem(request).subscribe({
next: () => {
// on success : close the modal
this.activeModal.close({
type: "primaryAction",
} as ModalResult);
},
});
}

// only if returning data is needed
protected onSubmitWithData(): void {
//...
this._service.updateItem(request).subscribe({
next: () => {
// on success : close the modal and return data
const data: ModalFeatureResult = {};
this.activeModal.close({
type: "primaryAction",
data, // typed as ModalFeatureResult
} as ModalResult<ModalFeatureResult>);
},
});
}
}

Exemple de template HTML simplifié :

@if (firstContent$ | async; as firstContentResult) {
<shared-modal
[title]="'FEATURE.TITLE' | translate"
[contentLoading]="firstContentResult"
[content]="contentTpl"
[footer]="footerTpl"
/>
<ng-template #contentTpl>
@if (firstContentResult.status === 'success') {
<form id="modal-feature-form" [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Champs du formulaire -->
</form>
}
</ng-template>
<ng-template #footerTpl>
<!-- Association du bouton au formulaire, permet de gérer la soumission via le formulaire -->
<button form="modal-feature-form" type="submit" class="btn btn-primary">
{{ 'SHARED.MODAL.PRIMARY_BUTTON' | translate }}
</button>
</ng-template>
}

Exemple d'ouverture d'une modal depuis un composant :

private readonly _modalService = inject(ModalService);
private readonly _injector = inject(Injector);

protected openFeatureModal(item: ItemType) {
const data: ModalFeatureData = { item };
this._modalService
.openWithComponent<ModalFeatureResult>({
component: ModalFeatureComponent,
data: data,
}, { injector: this._injector })
.then((result) => { // result typed as ModalResult<ModalFeatureResult>
// Modal called with close
})
.catch((reason) => {
// Modal called with dismiss, gérer silencieusement
});
}

Datatable

//TODO: presentation

//TODO: sort/search function with custom types

//TODO: customCellDirective

Simple

Paginated

Externe

Cdk-Tree

Angular CDK docs

On se base sur le CDK d'Angular pour construire des arborescences dépliables.

Cas 1 : Tous les éléments sont préchargés

Exemple arborescence document manager

  1. Définition d'un type TreeNode contenant les informations des noeuds.
  2. Définition d'une méthode indiquant si un noeud à des enfants (utilisé dans le template).
  3. Initialisation d'une source de données "dataSource" (passée au cdk-tree).
  4. Initialisation d'un TreeControl permettant au cdk de manipuler les noeuds.
  5. Définition du template
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
<!-- template d'un noeud terminal "sans enfant" -->
<cdk-nested-tree-node
*cdkTreeNodeDef="let node"
class="tree-node"
[ngClass]="{
'active-node': isNodeActive(node),
'hover-node': node.hover
}"
(click)="activateNode(node)"
(mouseover)="node.hover = true"
(mouseout)="node.hover = false"
>
{{ node.name }}
</cdk-nested-tree-node>
<!-- template d'un noeud avec enfant dépliable/repliable -->
<!-- hasChild est la méthode indiquant si un noeud a des enfants -->
<cdk-nested-tree-node
*cdkTreeNodeDef="let node; when: hasChild"
class="tree-node"
[ngClass]="{
'hover-node': node.hover
}"
(mouseover)="node.hover = true"
(mouseout)="node.hover = false"
>
<button
[attr.aria-label]="'Toggle ' + node.name"
cdkTreeNodeToggle
class="btn p-0 text-start"
>
<fa-icon
[icon]="treeControl.isExpanded(node) ? icons.fold : icons.unfold"
></fa-icon>
{{ node.name }}
</button>
<div [class.d-none]="!treeControl.isExpanded(node)">
<!-- container des noeuds enfant (géré par le cdk tree) -->
<ng-container cdkTreeNodeOutlet></ng-container>
</div>
</cdk-nested-tree-node>
</cdk-tree>
attention

Lors d'un rechargement dynamique d'élément de cette arborescence et notamment lors de la suppression de certain noeud, il faut s'assurer de passer au TreeControl une fonction trackBy en configuration qui permet de gérer l'identité d'un noeud. Si on l'omet : les éléments supprimés resteront affichés.

treeControl = new NestedTreeControl<DocumentTreeNode, string>(
(node) => node.children,
{
trackBy: trackByFn, // (node: TreeNode) => string
},
);

En revanche il ne faut pas passer de propriété trackBy au composant cdk-tree, un problème d'implémentation empêche la suppression des noeuds (see issue)

Cas 2 : On charge les enfants d'un noeud dynamiquement

Exemple arborescence structures

Afin de réfleter les ajouts de noeuds il est important d'utiliser un Observable pour mettre à jour les enfants et s'assurer que le tree "voit" les mises à jour, ici on utilise un BehaviorSubject.

export interface StructureTreeNode {
id: number;
intitule: string;
children: BehaviorSubject<StructureTreeNode[]>;
loading = false;
}

On écoute dynamiquement les actions de pliage/dépliage des noeuds qui sont executés par la directive CdkTreeNodeToggle pour charger les enfants.

this.treeControl.expansionModel.changed.subscribe((change) => {
//change.added = expanding node / change.removed = collapsing node
if (change.added) {
// change.added is a list to handle case where multiple nodes are affected at the same time
change.added.forEach((node) => this.loadChildren(node));
}
}),
astuce

Il est déconseillé d'ajouter la configuration trackBy au TreeControl dans ce cas car cela change la nature des évènements de changement, ils n'émettent plus des noeuds mais des id correspondant au retour de la méthode trackBy, ce qui implique de recherche le noeud dans le dataSource pour mettre à jour les enfants.

treeControl = new NestedTreeControl<StructureTreeNode, number>(
(node) => node.children,
{
trackBy: (node) => node.id, //return a number
},
);

this.treeControl.expansionModel.changed.subscribe((change) => {
// change is automatically typed as SelectionChange<number>, the param node is now a number
change.added.forEach((node) => this.loadChildren(node));
}),

Stepper

Angular CDK docs

On se base sur le CDK d'Angular pour construire des formulaires sur plusieurs étapes.

Example file uploader

//TODO: complete docs