La navigation est un élément central d’une application en général, et dans une application Angular en particulier. Dans cet article, nous allons décortiquer tout ce qui a été fait dans le commit https://github.com/AlanCrevon/LeCoinDuProf/commit/8d72854133f023d8760e14b292950ac5ef7fc0fb de LeCoinDuProf.
La navigation y est un peu plus complexe que dans un projet classique parce que LeCoinDuProf doit fonctionner à la fois en application mobile et en site internet. Donc la navigation doit fonctionner aussi simplement sur un écran 5 pouce que sur un écran 27 pouces. Mission impossible ? Non. Et pour cela, on remercie le web responsive.
Organisation des menus
L’idée (pas super originale, j’en conviens…) est la suivante : sur un grand écran, on affiche un menu latéral à gauche, et sur un petit écran, on affiche un menu en bas de l’écran. Ces menus permettront d’accéder aux différentes fonctionnalités de l’application. On ajoutera un troisième menu de navigation, qui redirige cette fois vers les pages purement informatives (Facebook, Twitter, Conditions générales d’utilisation…).
Pour ces menus, nous utilisons 3 components fournis par Ionic :
- ion-split-pane (https://ionicframework.com/docs/api/split-pane) pour le menu latéral
- ion-tabs (https://ionicframework.com/docs/api/tabs) pour le menu en bas de l’écran
- ion-fab (https://ionicframework.com/docs/api/fab) pour le menu complémentaire
Petit problème : sur une même page ne peuvent pas cohabiter le ion-split-pane et le ion-tabs. L’astuce consiste à créer un component intermédiaire affiché à l’intérieur du ion-split-pane, mais qui n’affiche le ion-tabs que quand le menu latéral n’est pas affiché.
Voyons comment s’orchestre tout ça.
Router et lazy loading
Le router de Angular utilise le fichier app-routing.module.ts pour faire le lien entre une url et le component à afficher.
L’équipe de Ionic a joué très finement ici en faisant clairement la différence entre les Pages et les Components. Les Pages sont des components avec une structure particulière sur laquelle nous reviendrons dans un prochain article.
Elle a également fait un excellent choix en utilisant par défaut le lazy loading : par défaut, l’utilisation du CLI pour générer une page génère en même temps son propre module. Un module est un paquet englobant des parties de l’application. Le lazzy loading permet de ne charger en mémoire ces parties de l’application que si et seulement si l’url qui y mène est invoquée. Mieux encore, un module peut utiliser son propre router, et donc charger lui même en lazy loading ses propres sous parties. Cela permet, au démarrage de l’application, de ne charger que le strict nécessaire, puis de charger au fur et à mesure les parties de l’application qui sont requises par les pages réellement affichées.
C’est ce qui se passe pour nous avec l’utilisation du component des tabs.
Nous le générons via :
cd src/app mkdir pages cd pages ionic g component tabs
et nous demandons au router de le charger lors de l’appel à l’url /app dans app-routing.module.ts:
const routes: Routes = [ // ... { path: 'app', loadChildren: './pages/tabs/tabs.module#TabsPageModule' }, // ... ];
Le service de navigation
Le menu latéral et le menu en bas de page exposent les mêmes liens. Plutôt que de répéter le code, nous partageons les liens vers les différentes parties de l’application via un service que nous générerons via le CLI de ionic :
cd src/app mkdir services cd services ionic g service navigation
Dans le fichier navigation.service.ts, nous déclarons un tableau qui contient toutes les instructions pour les menus (nous reviendrons sur les détails plus bas) :
public appPages = [ { title: 'Partages', url: '/app/shared', tab: 'shared', icon: 'share' }, { title: 'Recherches', url: '/app/wanted', tab: 'wanted', icon: 'search' }, { title: 'Messages', url: '/app/chats', tab: 'chats', icon: 'chatbubbles' }, { title: 'Inventaire', url: '/app/boxes', tab: 'boxes', icon: 'cube' }, { title: 'Moi', url: '/app/me', tab: 'me', icon: 'contact' }, { title: 'Modérer', url: '/app/moderate', tab: 'moderate', icon: 'construct' } ];
Le menu latéral
Pour que le service de navigation soit accessible depuis le template il faut l’injecter en tant que variable publique dans le constructeur de app.component.ts :
constructor( // ... public navigationService: NavigationService ) { // ... }
Puis dans le template app.component.html, une boucle sur navigationService.appPages permet de peupler le menu du ion-salit-pane avec son icône, son titre, et bien sûr le lien vers lequel pointer :
<ion-split-pane> <ion-menu> <ion-content> <ion-list> <ion-menu-toggle auto-hide="false" *ngFor="let appPage of navigationService.appPages"> <ion-item routerDirection="root" [routerLink]="appPage.url" color="primary"> <ion-icon slot="start" [name]="appPage.icon"></ion-icon> <ion-label> {{ appPage.title }} </ion-label> </ion-item> </ion-menu-toggle> </ion-list> </ion-content> </ion-menu> <ion-router-outlet main></ion-router-outlet> </ion-split-pane>
Le menu en bas
Pour que tabs.page.ts accède également au service de navigation, il faut là aussi l’injecter dans le constructeur :
constructor( // ... public navigationService: NavigationService ) { // ... }
Ceci permet au template tabs.page.html de peupler le menu. Attention toutefois : cette fois, l’url à afficher est relative à l’url courante. On utilise donc la propriété tab au lieu de la propriété url :
<ion-tabs> <ion-tab-bar slot="bottom" color="primary"> <ion-tab-button *ngFor="let appPage of navigationService.appPages"="appPage.tab"> <ion-icon [name]="appPage.icon"></ion-icon> <ion-label>{{ appPage.title }}</ion-label> </ion-tab-button> </ion-tab-bar> </ion-tabs>
Masquer le menu du bas
Le ion-split-pane est capable de masquer seul le menu en fonction de la taille de l’écran. La configuration par défaut définit que dès que la largeur de l’écran est inférieure à 992px, le menu est masqué. ion-tabs ne propose pas cette fonction, nous la codons donc nous même.
La détection de la largeur de l’écran doit se faire en 2 temps : d’abord la détection au moment du chargement du component, puis dès que l’écran est retaillé.
Pour cela, dans tabs.page.ts, nous allons faire appel à Platform qui nous informe de pas mal d’éléments concernant la plate-forme sur laquelle le code s’exécute (iOS, Android ou Web) et qui doit être injecté dans le constructeur :
import { Platform } from '@ionic/angular'; // ... constructor( // ... public platform: Platform ) { // ... }
Dans la classe TabsPage, nous déclarons une nouvelle propriété :
public platformWidth: any;
qui sera assignée dès le chargement, puis par l’écoute à tout événement de changement de taille de la Platform.
ngOnInit() { this.platformWidth = window.innerWidth; this.onResize(); } onResize() { this.platform.ready().then(() => { this.platform.resize.subscribe(() => { this.platformWidth = window.innerWidth; }); }); }
Enfin, nous revenons sur tabs.page.html pour brancher le ion-tabs sur platformWidth afin de le masquer automatiquement :
<ion-tab-bar slot="bottom" color="primary" *ngIf="platformWidth < 992"> <!-- ... --> </ion-tab-bar>
Le menu complémentaire
Le menu complémentaire est beaucoup plus simple. Profitons du navigation.service.ts pour y déclarer les chemins vers les pages d’information :
public infoPages = [ { title: 'Page Facebook', icon: 'logo-facebook', href: 'htps://facebook.com/lecoinduprof', routerLink: false }, { title: 'Fil Twitter', icon: 'logo-twitter', href: 'https://twitter.com/prof_coin', routerLink: false }, { title: `L'équipe`, icon: 'contacts', href: false, routerLink: '/app/team' }, { title: `Conditions générales d'utilisation`, icon: 'information-circle-outline', href: false, routerLink: '/app/guc' } ];
Notez l’utilisation de routerLink pour les pages internes à l’application et de href pour les pages externes : sur des platforms Android et iOS, ça permettra d’ouvrir un navigateur en dehors de l’application.
Il suffit maintenant d’ajouter le ionic-fab à app.component.html :
<ion-fab horizontal="end" vertical="top" slot="fixed"> <ion-fab-button color="secondary"> <ion-icon name="menu"></ion-icon> </ion-fab-button> <ion-fab-list side="bottom"> <ion-button *ngFor="let infoPage of navigationService.infoPages" [href]="infoPage.href ? infoPage.href : null" [routerLink]="infoPage.routerLink ? infoPage.routerLink : null" color="medium" shape="round" fill="outline" > <ion-icon [name]="infoPage.icon" slot="end"></ion-icon> <ion-label>{{ infoPage.title }}</ion-label> </ion-button> </ion-fab-list> </ion-fab>
Pour adapter le style, il faut modifier app.component.scss afin d’aligner correctement les boutons sur la droite de l’écran :
ion-fab-list { right: 0; align-items: flex-end; }
Ajouter des pages à l’application
Notre navigation est prête, ne reste plus qu’à générer les pages qui correspondent au NavigationService. Comme j’aime que la structure du code soit cohérente avec le fonctionnement de l’application, elles sont toutes dans le répertoire src/app/pages/tabs/pages d’où il faut lancer une suite de
ionic g page [nom de la page]
.
Il ne faut pas oublier de déclarer ces pages dans le router du module tabs (tabs.module.ts) en profitant à fond du lazy loading:
const routes: Routes = [ { path: '', component: TabsPage, children: [ { path: 'shared', loadChildren: './pages/shared/shared.module#SharedPageModule' }, { path: 'wanted', loadChildren: './pages/wanted/wanted.module#WantedPageModule' }, { path: 'chats', loadChildren: './pages/chats/chats.module#ChatsPageModule' }, { path: 'boxes', loadChildren: './pages/boxes/boxes.module#BoxesPageModule' }, { path: 'me', loadChildren: './pages/me/me.module#MePageModule' }, { path: 'moderate', loadChildren: './pages/moderate/moderate.module#ModeratePageModule' } ] } ];
One more thing
Notre navigation est prête mais il reste un détail à régler. Si l’utilisateur arrive sur la page ‘/’ ou ‘/app’, nous voulons le rediriger vers la véritable page d’accueil qui est ‘/app/shared’.
Pour cela, une dernière modification de app-routing.module.ts permet d’activer la redirection de ces url vers le chemin voulu :
const routes: Routes = [ { path: '', redirectTo: 'app/shared', pathMatch: 'full' }, { path: 'app', redirectTo: 'app/shared', pathMatch: 'full' }, // ... ];
Conclusion
Cet article aura été l’occasion de passer en revue le fonctionnement du router Angular avec le lazzy loading, d’un service simple, et de 3 différents menus de navigation possibles. J’aurais aimé me passer du component intermédiaire de tabs ou au moins de la partie ‘/app/’ dans l’url, mais pour faire fonctionner à la fois les ion-tabs et le ion-split-pane, je n’ai pas trouvé mieux.
N’hésitez pas à laisser un commentaire ou mieux encore un patch pour le code sur Le GitHub de LeCoinDuProf.
Commentaires récents