Comment réutiliser des layouts communs dans Angular en utilisant Router

La plupart des applications web sur lesquelles j’ai travaillé jusqu’à présent, avaient un design où différentes pages utilisent un layout commun. Par exemple la mise en page qui consiste en un en-tête, un pied de page et une barre latérale, qui sont fixes pour chaque page, et le contenu qui varie selon la page. L’idée logique est d’essayer d’extraire et de réutiliser les parties communes. Sur la base des documents Angular, des cours Pluralsight et d’autres documents que j’ai trouvés, j’ai trouvé deux options possibles. Pour mieux expliquer ces options, commençons par définir le projet d’exemple.

Projet d’exempleLien vers cette section

Disons que nous avons une application simple qui a 5 routes/pages différentes (connexion, inscription, tableau de bord, utilisateurs, paramètres du compte) et deux mises en page. Une disposition avec le contenu et le pied de page, appelons-la disposition 1, et la disposition 2 avec l’en-tête, le pied de page, la barre latérale et le contenu. Disons également que les pages de connexion et d’enregistrement ont le layout 1, tandis que les autres ont le layout 2.

La mise en page 1 – mise en page du pied de page uniquement
La mise en page 2 – mise en page principale

La dernière, nous pouvons dire que nos pages sont des fonctionnalités distinctes de l’application. En utilisant la structure de projet dossier par fonctionnalité, chacune de nos fonctionnalités aura un module Angular séparé avec le module de routage correspondant.

Option 1Lien vers cette section

(Vous pouvez jouer avec ici)

La mise en page est définie comme un composant dans un module séparé, et l’utiliser comme un composant parent dans le module de routage de chaque fonctionnalité spécifique.

D’abord dans le modèle de composant racine (généralement AppComponent), utilisez seulement <router-outlet> comme:

<>Copie
<router-outlet></router-outlet>

Puis définissez FooterOnlyLayoutComponentle composant pour le layout 1 avec le modèle suivant :

<>Copie
<div class="content" fxFlex> <router-outlet></router-outlet></div><app-footer></app-footer>

Enfin, pour utiliser cette disposition pour la route de connexion, la route doit être spécifiée comme suit :

<>Copie
...const routes: Routes = }];@NgModule({ imports: , exports: })export class LoginRoutingModule { }

De cette façon, lorsque l’utilisateur navigue vers/login, FooterOnlyLayoutComponent sera rendu dans le « router slot » de AppComponent, tandis que LoginComponent sera rendu dans le router slot de FooterOnlyLayoutComponent. Pour que la page d’enregistrement utilise FooterOnlyLayoutComponent, définissez la route de la même manière tout en fournissant le chemin et le composant d’enregistrement au lieu du login.

Pour le composant de la disposition 2 (MainLayoutComponent), nous avons le modèle suivant:

<>Copie
<app-header fxLayout="column"></app-header><div fxLayout="row" fxFlex="100"> <app-sidebar fxLayout="column" fxFlex="300px"></app-sidebar> <div class="content" fxLayout="column" fxFlex> <router-outlet></router-outlet> </div></div><app-footer fxLayout="column"></app-footer>

Pour utiliser cette disposition pour la page du tableau de bord, dans le module de routage du tableau de bord, spécifiez la route comme ceci :

<>Copie
...const routes: Routes = }];@NgModule({ imports: , exports: })export class DashboardRoutingModule { }

Maintenant, lorsque l’utilisateur navigue vers /dashboard, MainLayoutComponent sera rendu dans le « router slot » de AppComponent, tandis que DashboardComponent sera rendu dans le router slot de MainLayoutComponent. Pour que d’autres pages utilisent cette disposition, spécifiez leurs routes de la même manière dans leurs modules de routage correspondants.

C’est tout. Maintenant, nous avons été en mesure de réutiliser la mise en page entre plusieurs modules. Les routes de connexion et d’enregistrement utilisent le layout 1 (FooterOnlyLayoutComponent), tandis que les routes du tableau de bord, des utilisateurs et des paramètres de compte utilisent le layout 2 (MainLayoutComponent).

IssuesLink to this section

Le problème avec cette approche est que le layout est recréé inutilement à chaque changement de route. Nous pouvons vérifier cela en mettant des logs de console dans les constructeurs du composant layout, header, footer et sidebar. Si vous allez d’abord la page /dashboard, vérifiez la console, puis allez à la /users, vous verrez que les constructeurs sont appelés deux fois.

Outre les implications de performance, cela apporte une autre couche de complexité s’il y a un certain état qui doit être persistant entre les routes. Disons que notre en-tête a une entrée de recherche et que l’utilisateur a tapé quelque chose, quand il passe à une autre page, l’en-tête sera recréé et l’entrée effacée. Bien sûr, cela peut être géré en persistant l’état à un certain stockage, mais c’est encore une complexité inutile.

Option 2 – utiliser des modules chargés paresseuxLien externe à cette section

(Vous pouvez jouer avec ici)

Définir la mise en page comme un composant dans un module séparé avec routage. Appelons ce module LayoutModule. Définissez tous les modules de fonctionnalités comme des modules enfants chargés paresseusement à l’intérieur de LayoutModule.

De nouveau, dans le modèle de composant racine (AppComponent), utilisez uniquement <router-outlet>. La disposition 1 (FooterOnlyLayoutComponent) et la disposition 2 (MainLayoutComponent) ont les mêmes modèles comme dans l’option 1.

Ne pas importer les modules de fonctionnalités dans le AppModule. Au lieu de cela, nous allons les importer paresseusement dans le LayoutRoutingModule:

<>Copy
…const routes: Routes = }, { path: '', component: FooterOnlyLayoutComponent, children: },];@NgModule({ imports: , exports: })export class LayoutRoutingModule { }

Enfin, dans le module de routage de chaque module de fonctionnalité, il suffit d’utiliser un chemin vide et le composant. Par exemple, pour la connexion, les routes seraient:

<>Copy
const routes: Routes = ;

alors que pour le tableau de bord c’est:

<>Copy
const routes: Routes = ;

et nous avons terminé.

Encore, la connexion et l’enregistrement utilisent FooterOnlyLayoutComponent tandis que les autres routes utilisent MainLayout. Cependant, cette fois, nous avons évité de recréer la mise en page, l’en-tête, le pied de page et la barre latérale à chaque changement de route. Si vous mettez à nouveau les journaux de la console dans les constructeurs, vous verrez que maintenant les layouts sont recréés uniquement lorsque vous naviguez entre les routes de différents layouts. Ainsi, si vous naviguez de /dashboard à /users la mise en page ne sera pas recréée, alors que si vous allez de /dashboard à /login elle le sera.

IssuesLink to this section

Un problème plus petit est que tous les modules chargés paresseusement et leurs chemins de base doivent être définis dans LayoutRoutingModule donc cela peut devenir désordonné pour les plus grands projets. Le problème plus important est que nous devons utiliser le chargement paresseux alors que parfois peut-être vous ne le voulez pas. Il devrait être possible de réutiliser les mises en page de manière similaire sans forcer les modules à chargement paresseux. J’ai essayé de contourner cela en spécifiant loadChildren comme ceci:

<>Copy
...const routes: Routes = }, { path: '', component: FooterOnlyLayoutComponent, children: },];@NgModule({ imports: , exports: })export class LayoutRoutingModule { }

mais cela ne fonctionne que si vous n’utilisez pas AOT, ce qui est quelque chose que nous voulons absolument utiliser en production (https://github.com/angular/angular-cli/issues/4192).

Une autre solution possible serait de précharger tous les modules chargés paresseusement en spécifiant la stratégie de préchargement dans AppModule comme:

<>Copie
RouterModule.forRoot(, { preloadingStrategy: PreloadAllModules })

mais avec cela les modules sont regroupés séparément et vous vous retrouvez avec plusieurs fichiers que le client doit aller chercher ce qui est quelque chose que vous ne voulez peut-être pas. En outre, ce n’est pas approprié si vous voulez charger paresseusement seulement certains modules spécifiques. Dans ce cas, vous pouvez vouloir écrire une stratégie de préchargement personnalisée, mais vous vous retrouverez toujours avec un fichier pour chaque module.

Comment cela a été fait avec AngularJs et UI-RouterLien vers cette section

(Essayez ici)

Cela a été beaucoup plus facile à réaliser avec AngularJs et UI-Router, en utilisant des vues nommées. Là, nous devons d’abord définir l’état abstrait du layout:

<>Copy
$stateRegistry.register({ name: 'layout', abstract: true, views: { '@': { templateUrl: 'layout.html', }, 'header@layout': { component: 'header' }, 'sidebar@layout': { component: 'sidebar' }, 'content@layout': { template: '' }, 'footer@layout': { component: 'footer' } }});

puis layout.html:

<>Copy
<div class="flex-column" ui-view="header"></div><div class="flex-row flex-100"> <div class="flex-column" ui-view="sidebar"></div> <div class="flex-column flex" ui-view="content"></div></div><div class="flex-column" ui-view="footer"></app-footer>

et ensuite, lors de la définition de l’état pour la page réelle, vous devez utiliser l’état du layout comme parent et surcharger la ou les vues nommées spécifiques. Ainsi, l’état de connexion serait:

<>Copy
$stateRegistry.register({ parent: 'layout', name: 'login', url: '/login', views: { 'content@layout': { component: 'login', }, 'header@layout': { component: '' }, 'sidebar@layout': { template: '' } }});

alors que l’état du tableau de bord serait:

<>Copy
$stateRegistry.register({ parent: 'layout', name: 'dashboard', url: '/dashboard', views: { 'content@layout': { component: 'dashboard', } }});

Pour définir l’état pour le reste des pages il suffit de suivre le même schéma.

Une fois que c’est fait, ajoutons console.log au crochet $onDestroy de chaque composant et naviguons entre les pages. Nous pouvons voir que l’en-tête, la barre latérale et le pied de page ne sont pas détruits lorsque nous naviguons entre /users et /dashboard. Même lorsque nous naviguons entre la page avec la mise en page principale et la page avec la mise en page du pied de page uniquement, nous remarquerons que le pied de page est réutilisé.

ConclusionLien vers cette section

Même s’il est possible de réaliser une certaine forme de réutilisation de la mise en page avec le routeur Angular, comme décrit ci-dessus, les deux approches semblent un peu « hacky » et pénible. Il est beaucoup plus facile de le réaliser avec UI Router, où nous sommes même capables de réutiliser des composants partagés entre différents layouts, ou le routage dynamique de React.

PSi vous connaissez une meilleure façon de gérer cela avec Angular router, veuillez partager dans les commentaires 🙂

EDIT:Lien vers cette section

Option 3Lien vers cette section

Grâce à Alexander Carls et Lars Gyrup Brink Nielsen, qui ont partagé leurs idées dans les commentaires, nous avons une option 3 qui résout tous les problèmes mentionnés ci-dessus. L’idée est de s’abonner aux événements du routeur, puis sur chaque événement NavigationEnd, vous pouvez afficher/cacher des morceaux de la mise en page en fonction de la route. Exemples:

Exemple 1

Exemple 2 (avec chargement paresseux)

Discuter avec la communauté.