State
Aujourd'hui les états sont gérés par des services dédiés sans utiliser de solution de type "Store" (NgXS / NgRxStore).
Service as store
Des instances dédiées sont instanciées et injectées lors de la création du composant (i.e: ce ne sont pas des singletons).
Ex:
//Injection d'un service gérant la page de détail
@Component({
//...
providers: [TypologiesDetailService],
})
export class TypologiesDetailPageComponent {}
Dans le cas d'une page de détail avec onglets, on a découpé pour avoir un sous service pour chaque onglet. Les données communes sont dans le service parent au niveau de la page de détail et chaque onglets peut avoir son propre service dédié.
Lors de la mise à jour de données communes, on doit faire la mise à jour le plus haut possible dans la hierarchie de service pour notifier tous les observateurs.
Chaque donnée est gérée dans un BehaviorSubject. La majorité de ces services utilisent les données d'un resolver qui a pré-chargé les données avant de naviguer sur la page affichant le composant. La donnée du resolver permet d'instancier la valeur initiale de ce Subject.
Exemple de gestion d'une donnée par un service :
@Injectable()
export class TypologiesDetailService {
// les données du resolver utilisent un typage fort avec des constantes (ici : TYPOLOGIE_DETAIL_RESOLVER_KEY) et des types (ici : TypologieDetailResolverData)
//1. initialisation du behavior subject
private _typologie$ = new BehaviorSubject<Typologie>(
(
this.activatedRoute.snapshot.data[
TYPOLOGIE_DETAIL_RESOLVER_KEY
] as TypologieDetailResolverData
).typologie,
);
//2. exposition d'un observable
readonly typologie$ = this._typologie$.asObservable();
//3. getter
get typologie(): Typologie {
return this._typologie$.getValue();
}
//4. setter
set typologie(typologie: Typologie) {
this._typologie$.next(typologie);
}
constructor(private activatedRoute: ActivatedRoute) {
//5. écoute des nouvelles valeurs résolus par un changement du router
this.activatedRoute.data.pipe(skip(1)).subscribe((data) => {
const resolvedData = data[
TYPOLOGIE_DETAIL_RESOLVER_KEY
] as TypologieDetailResolverData;
this.typologie = resolvedData.typologie;
});
}
}
- On initialise le behavior subject une valeur résolu statiquement (non observable)
- On expose un observable -> il sera utilisé lorsque l'on doit réagir à l'émission de nouvelles valeurs
- On expose un getter -> il sera utilisé lors d'un accès synchrone à la valeur (ex: appel à une API pour la typologie actuelle - on récupère l'id de la typologie en cours)
- On expose un setter -> si on autorise la donnée a être mise à jour depuis l'extérieur (ex: sous services)
- On écoute les nouvelles valeurs des changements de routes -> notamment pour les navigations vers une autre typologie depuis la même page (sans rafraichissement de l'app Angular)
Un sous service devrait être la façade du service parent mais parfois les 2 services ont été injectés séparement.
Ex:
Facade :
TypologieDetailService -> VisaService (redéfinit et délègue au service TypologieDetailService certaines méthodes) -> Composants onglet visa
Sans façade :
TypologieDetailService -> Composants onglet visa
VisaService-> Composants onglet visa
Dépendances entre le modèle principal et les sous-domaines
Sur une page de détail, le service parent (ex : ConventionsDetailService, TypologiesDetailService) charge une seule fois le modèle principal et le partage avec tous les onglets et sous-services. Or ce modèle dépend parfois des sous-domaines gérés par les onglets :
- Champs calculés par le backend à partir d'un sous-domaine. Ex :
Convention.hasTextePrincipaldépend des textes,Convention.hasNonComputedPartnerdépend des partenaires (voir convention.model.ts). - Listes recopiées dans le service parent pour calculer des alertes ou des règles d'édition transverses aux onglets. Ex : les viseurs (
circuitViseurs$), les référents juridiques, les sous-types ou les domaines d'une typologie.
Si un sous-service modifie son sous-domaine et ne rafraîchit que sa propre liste, l'état partagé devient obsolète : les autres onglets, alertes et modales affichent des données fausses jusqu'au rechargement complet de la page.
Règle
Après chaque mutation d'un sous-domaine, se demander : le modèle principal (ou un état du service parent) dépend-il de ce que je viens de modifier ?
-
Oui, un champ calculé par le backend → rafraîchir le modèle principal via le service parent puis les données du sous-domaine, enchaînés avec
switchMappour que l'IHM se mette à jour une fois tout à jour. On l'expose comme une méthode nommée du sous-service, et on garde une méthoderefresh<Domaine>$()plus légère pour les mutations sans impact sur le modèle principal (évite unGETinutile)./**
* Refresh the convention (e.g. `hasTextePrincipal`) along with the texte principal and its annexes.
* To use when the presence or absence of the texte principal changes.
*/
refreshConventionAndTexteEtAnnexes$() {
return this._detailService
.refreshConventionWithGrants$()
.pipe(switchMap(() => this.refreshTexteEtAnnexes$()));
}Implémentation de référence :
ConventionTexteService.refreshConventionAndTexteEtAnnexes$(). -
Oui, une liste recopiée → pousser la nouvelle liste vers le setter du service parent depuis le setter privé du sous-service, pour ne jamais l'oublier.
private setViseurs(viseurs: Array<ViseurListItem>): void {
this._viseurs$.next(viseurs);
this._detailService.setCircuitViseurs(viseurs);
}Implémentation de référence :
VisaService.setViseurs(). -
Non → rafraîchir uniquement les données du sous-domaine.
- Si la mutation peut changer le statut de la convention, utiliser
refreshConventionWithGrants$(true): cela force le rechargement complet de la page (tous les onglets dépendent du statut). - Si la mutation ne change que les droits de l'utilisateur,
refreshGrants$()suffit (pas de rechargement de la convention).