Les 3: Single Page Applications
Les 3: Single Page Applications
Momenteel is de functionaliteit van de To-Do applicatie redelijk beperkt. Tijdens deze les breiden we de applicatie uit met routing en voegen we meerdere pagina's toe zodat we taken kunnen bewerken en bekijken. Om dit te verwezenlijken gebruiken we een centrale datastore en delen we de pagina's op in herbruikbare componenten.
Deze les bouwt voort op de oplossingen van de oefeningen van les 2 (te downloaden op GitFront).
Routing
We bouwen een single page application, dit betekent dat routing afgehandeld wordt door JavaScript en niet door een browser/server combo.
Zoals in les 2 gezien, heeft elke module een afzonderlijk *-routing.module.ts bestand, hierin worden de verschillende routes (pagina's) gedeclareerd. Dit routing bestand moet natuurlijk geïmporteerd worden in de bijhorende *.module.ts bestanden.
// Niet relevante imports weggelaten.
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
Op lijn 9 in bovenstaande code zien we dat Ionic standaard de RouteReuseStrategy provider implementeert in een klasse IonicRouteStrategy en deze inlaad in app.module.ts. De RouteReuseStrategy bepaald wanneer een pagina herbruikt wordt en wanneer er een nieuwe instantie aangemaakt wordt.
Binnen Ionic worden zeer weinig pagina's herbruikt. Enkel als de route 100% dezelfde is en de pagina nog in het navigation-stack zit, wordt de component herbruikt. Het navigation-stack is een collectie die alle bezochte pagina's bewaard, als de gebruiker een link volgt, dan wordt deze nieuwe pagina toegevoegd aan het navigation-stack. Als een gebruiker op een terug-knop drukt (hard- of software), dan wordt de pagina verwijderd uit het stack. Als een pagina herbruikt wordt, dan worden zaken als de scroll positie en de waarden van formuliervelden ook bewaard.
De template file app.component.html bevat een <ion-router-outlet> component, hierin worden de bezochte routes geladen. Wil je iets tonen dat altijd zichtbaar moet zijn, zoals een side-menu, dan kan dit buiten de <ion-router-outlet> component geplaatst worden.
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
De <ion-router-outlet> component is een wrapper rond de Angular router en voegt hier animaties aan toe. Het is vanzelfsprekend dat deze animaties aangepast worden aan het platform waarop de app draait. Zou je animaties willen afzetten, dan kan dat door het attribuut animated van de <ion-router-outlet> component op false te zetten.
Routes definiëren
Binnen de routing modules is een onderscheid te maken tussen app-routing.module.ts en alle andere routing modules. De klasse AppRoutingModule gebruikt de methode RouterModule.forRoot(...), deze maakt een nieuwe RoutingService aan en laad de nodige routes in. Alle andere modules gebruiken RouterModule.forChild(...) en maken geen RoutingService aan, maar herbruiken diegene die in de app-routing.module is aangemaakt. Omdat de RoutingService herbruikt wordt op de verschillende pagina's, is het in elk van deze pagina's mogelijk om het volledige navigatie-stack te lezen en te bewerken.Daarnaast laden beide methodes de routing directives (zie verder) en de gedefinieerde routes in.
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'home',
loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }
In elke routing module worden een reeks routes gedefinieerd, dit gebeurt in een array van Route objecten (waarvoor een type Routes is gemaakt). Deze array wordt als argument meegegeven aan de forRoot en forChild methodes.
In bovenstaande code is op lijnen 9-13 een lege route gedefinieerd (path: ''). Dit is de homepage van je applicatie, de default route, en redirect de gebruiker naar '/home'. Als deze laatste route bezocht wordt, wordt zoals te zien op lijnen 5-8, de HomePageModule ingeladen.
Merk op dat in app-routing.module.ts enkel routes gedefinieerd zijn die één niveau diep gaan, iets als '/home/contact' wordt in home-routing.module.ts gedefinieerd en niet in app-routing.module.ts.
Binnen home-routing.module.ts wordt opnieuw een lege default route gedefinieerd (lijnen 6-9). Deze route toont de HomePage component. Merk op dat deze route relatief is ten opzichte van de parent, de lege route komt dus overeen de route '/home'.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomePage } from './home.page';
const routes: Routes = [
{
path: '',
component: HomePage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomePageRoutingModule {}
Merk op dat in Ionic gebruik gemaakt wordt van lazy-loading (app-routing.module.ts, lijn 7), en dat voor de componenten binnen een module eager-loading gebruikt wordt (home-routing.module, lijn 8).
Begrip: Eager-loading & lazy-loading
Er zijn twee manieren om resources te laden, eager-loading en lazy-loading. Deze technieken zijn alom bekend en worden niet enkel in Angular gebruikt.
Eager-loading betekent dat de resource geladen wordt zodra de applicatie start, dus kan het even duren voor de applicatie opgestart is, dit is natuurlijk niet altijd ideaal.
Het alternatief is lazy-loading, deze techniek laad de resource pas als de gebruiker hiernaar vraagt. De applicatie zal sneller starten, maar het is mogelijk dat de gebruiker tijdens het gebruik van de app wat langer moet wachten omdat een bepaalde resource nog niet geladen is.
Naast deze optimalisaties, importeert Ionic standaard ook de PreloadAllModules strategie in app-routing.module.ts. Dankzij deze strategie start de applicatie enkel met het home screen, maar zodra dit geladen is en de gebruiker de applicatie kan gebruiken, worden de andere modules asynchroon, op de achtergrond, geladen.
New Task Page
We breiden de To-Do app uit met een nieuwe pagina waarmee een taak aangemaakt, bekeken, en bewerkt kan worden. We zullen pagina's, services en componenten altijd laten genereren door Ionic, zo zijn alle imports die standaard aanwezig moeten zijn, automatisch in orde.
Om een pagina te genereren gebruiken we het ionic generate commando, het eerste argument van dit commando is het type bestand dat we willen genereren, in dit geval page. Vervolgens moeten we de locatie en naam opgeven van het nieuwe bestand. Dit gebeurt op basis van een pad ten opzichte van de app folder. De nieuwe pagina zal een subpagina worden van de HomePage, dus wordt het commando:
ionic generate page home/task
Hier is task de naam van de nieuwe pagina. Standaard worden test files gegenereerd, aangezien wij geen testen schrijven tijdens deze lessenreeks is het aan te raden om ook geen testfiles te genereren. Het uiteindelijke commando wordt dan
ionic generate page home/task --no-spec
Ionic heeft, nadat het commando uitgevoerd is, een nieuwe map task aangemaakt met een template, logic file, stylesheet, module en routing-module.

Navigeren naar de nieuwe pagina
Bovenstaand commando heeft invloed op de routing modules, app-routing.module.ts blijft onveranderd, in home-routing.module.ts is echter een nieuwe route toegevoegd waarmee de TaskPage bezocht kan worden. De TaskPage is een pagina, dus gebruikt Ionic lazy-loading om de module in te laden. De task-routing.module.ts bevat slechts één route, die TaskPage component laad.
const routes: Routes = [
{
path: '',
component: HomePage,
},
{
path: 'task',
loadChildren: () => import('./task/task.module').then( m => m.TaskPageModule)
}
];
const routes: Routes = [
{
path: '',
component: TaskPage
}
];
Vorige les werd een FAB-button gebruikt om een alert te tonen, deze les vervangen we de alert met een nieuwe pagina. De methodes presentAlert en newTask mogen dus verwijderd worden uit home.page.ts.
Het routerLink directive kan als attribuut toegevoegd worden aan een HTML-element of Angular component om een Angular router link toe te voegen. De FAB-button bevindt zich in home.page.html, als op deze knop gedrukt wordt, willen we navigeren naar de nieuw aangemaakte, TaskPage.
Het routerLink directive werkt steeds relatief ten opzichte van de pagina waarop het directive zich bevindt. We moeten als waarde dus task (zie home-routing-module.ts) meegeven aan het directive. Willen je toch een absoluut pad gebruiken, dan moet dit voorafgegaan worden door een forward slash ('/').
<ion-fab *ngIf='fabIsVisible' [vertical]='verticalFabPosition'
horizontal='end' slot='fixed'>
<ion-fab-button routerLink='task'>
<ion-icon name='add'></ion-icon>
</ion-fab-button>
</ion-fab>
Meestal wordt er geen string meegeven aan het routerLink directive, maar een array. Alle elementen in de array worden automatisch geconcateneerd met een '/' als scheidingsteken. Dit maakt het eenvoudig om parameters toe te voegen aan een URL, zonder dat we met string concatenatie (+) of template strings (``) moeten werken. Het voorgaande kan dus als volgt herschreven worden.
<ion-fab *ngIf='fabIsVisible' [vertical]='verticalFabPosition'
horizontal='end' slot='fixed'>
<ion-fab-button [routerLink]="['task']">
<ion-icon name='add'></ion-icon>
</ion-fab-button>
</ion-fab>
Begrip: routerLink directive
Het routerLink directive kan op Angular componenten of HTML elementen toegevoegd worden om te navigeren naar een andere pagina. Dit directive neemt een array als argument die alle URL segmenten tenopzichte van de huidige locatie bevat. Als het eerste element in de array begint met een '/' is het pad absoluut.
<some-angular-component-or-HTML-element [routerLink]="['bar', 'baz']">
</some-angular-component-or-HTML-element>
<some-angular-component-or-HTML-element [routerLink]="['/foo', 'bar', 'baz']">
</some-angular-component-or-HTML-element>
2-way databinding
Voor we verder kunnen moet de Task interface uitgebreid worden. We voegen een optionele deadline en beschrijving toe.
export interface Task {
name: string;
id: number;
done: boolean;
description?: string;
deadline?: string;
}
De UI voor de TaskPage is gedefinieerd in de startbestanden. Plaats deze bestanden in je project. Via de FAB-button kunnen we nu de TaskPage bekijken.

Tijdens de oefeningen van les 2 hebben we gebruik gemaakt van event binding om de waarde van een formulierelement in de logica file te kunnen lezen. Event binding is nuttig, maar voor formulierelementen bestaat er een betere optie: 2-way databinding.
Begrip: 2-way databinding
2-way databinding is een techniek die het mogelijk maakt om via één directive eenvoudig de link te leggen tussen een variabele in de logica file en het value attribuut van een formulierelement. Elke pagina die door Ionic gegenereerd is, bevat automatisch de juiste imports voor 2-way databinding.
2-way databinding kan gezien worden als een combinatie van event binding en property binding. De notatie, [(ngModel)]="varInLogicFile", reflecteert dit. De vierkante haken geven aan dat de waarde van de variabele varInLogicFile weergegeven moet worden in het formulier, dankzij property binding zijn wijzigingen in de TypeScript variabele onmiddellijk zichtbaar in de UI. De ronde haken geven aan dat na een change-event, de nieuwe waarde die in het formulier ingegeven is ook weggeschreven moet worden naar de variabele varInLogicFile.
<some-form-element [(ngModel)]="foo">
</some-form-element>
export class Bar {
foo = '';
}
We passen 2-way databinding toe op alle formulierelementen in de TaskPage.
<ion-content [fullscreen]="true">
<ion-item lines="full">
<ion-label position="floating">Task name</ion-label>
<ion-input type="text" class="ion-margin-top"
[(ngModel)]='taskName'>
</ion-input>
</ion-item>
<ion-item lines="full">
<ion-label position="floating">Description</ion-label>
<ion-textarea cols="20" rows="4" [autoGrow]="true"
[(ngModel)]='description'>
</ion-textarea>
</ion-item>
<ion-item lines="none">
<ion-label>Deadline</ion-label>
</ion-item>
<div class="ion-align-items-center">
<ion-datetime firstDayOfWeek="1" [yearValues]="yearValues" presentation="date"
[(ngModel)]='deadline'>
</ion-datetime>
</div>
</ion-content>
export class TaskPage implements OnInit {
taskName: string;
deadline: string;
description: string;
// Niet relevante code is weggelaten voor de duidelijkheid.
}
Het ngModel directive is beschikbaar in elke pagina die door ionic g gegenereerd wordt. De module voor zo'n pagina importeert standaard de FormsModule waar het ngModel directive zit.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TaskPageRoutingModule } from './task-routing.module';
import { TaskPage } from './task.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
TaskPageRoutingModule
],
declarations: [TaskPage]
})
export class TaskPageModule {}
Annuleren en terugkeren naar de vorige pagina
Momenteel doet de X knop, die linksbovenaan staat, nog niets. We kunnen deze knop op drie manieren implementeren.

De meest naïeve oplossing bestaat uit het gebruiken van een routerLink directive om terug te keren naar de HomePage. Deze optie zal, voorlopig, werken, maar als we de TaskPage op verschillende manieren kunnen bereiken is deze optie ontoereikend. We willen tenslotte terugkeren naar de vorige pagina, niet naar de root van de applicatie.
<ion-buttons slot="start">
<ion-button [routerLink]="['/']">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
De tweede optie maakt gebruik van de NavController service die aangeboden wordt Angular. Deze service kan gebruikt worden om steeds terug te keren naar de vorige pagina. Als de TaskPage op meerdere manieren bezocht kan worden zal onze app dus nog steeds correct werken. Deze optie heeft echter nog één belangrijk probleem. We ontwikkelen mobiele applicaties, deze moeten zoveel mogelijk integreren met de UI van het besturingssysteem waarop de applicatie draait.
<ion-buttons slot="start">
<ion-button (click)="navController.back();">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
export class TaskPage implements OnInit {
// Niet relevante code weggelaten.
constructor(public navController: NavController) {
// Niet relevante code weggelaten.
}
}
De laatste en beste optie is de <ion-back-button> component. Deze knop past zich, net zoals alle Ionic componenten, automatisch aan op basis van het besturingssysteem en brengt de gebruiker steeds terug naar de vorige pagina. Tijdens het testen en ontwikkelen van de applicatie is het echter mogelijk dat er geen vorige pagina is (omdat de applicatie herladen wordt), hierom maak je best gebruik van het defaultHref attribuut, zo heeft de knop steeds pagina om naartoe te gaan.
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>

Services
Vorige les hebben we in home.page.ts een lijst van taken bijgehouden. Vanaf er twee pagina's in een applicatie staan is dit ontoereikend. Beide pagina's moeten toegang hebben tot de lijst om taken toe te voegen, te verwijderen of bij te werken. Een array bijhouden in één pagina zou betekenen dat we op verschillende pagina's andere taken zien, een onbruikbare app dus. Services bieden hier een oplossing.
Begrip: Service
Een service is een klasse die los staat van een bepaalde pagina, maar wel geïnjecteerd kan worden in de constructor van een logica file.
Een service is een singleton, dit betekent dat er slechts één instantie van de klasse aangemaakt kan worden. Als de service in verschillende klassen geïnjecteerd wordt zullen deze klassen allemaal dezelfde instantie delen. Hierdoor functioneert de service als een single source of truth. Met andere woorden, alle pagina's kunnen via de service data aanpassen en de aangepaste data kan gebruikt worden in alle andere pagina's.
Services worden opnieuw aangemaakt door middel van de Ionic CLI. Ook hier is het mogelijk om via een parameter aan te geven dat we geen test-files willen genereren.
ionic generate service services/task --skipTests=true

We verhuizen alle CRUD-code voor taken naar de nieuwe service. Alle instantievariabelen krijgen de access modifier # (private), zo verhinderen we dat de data rechtstreeks aangepast wordt in de pagina's, alle wijzigingen moeten via de service gaan. Hierdoor programmeren we alle operaties slechts één keer, in de service, dit is niet alleen properder, maar maakt het ook eenvoudiger om bugs op te sporen.
@Injectable({
providedIn: 'root'
})
export class TaskService {
#taskList: Task[] = [];
#id = 0;
constructor() {
}
getAllTasks(): Task[] {
return this.#taskList;
}
deleteTask(id: number): void {
this.#taskList = this.#taskList.filter(t => t.id !== id);
}
toggleTaskStatus(id: number): void {
const task = this.#taskList.find(t => t.id === id);
task.done = !task.done;
}
newTask(name: string, description: string, deadline?: string): void {
this.#taskList.push({
name, // Syntactische suiker voor name: name,
id: this.id,
done: false,
description,
deadline
}));
this.id++;
}
}
Ook de andere herbruikbare code uit home.page.ts kan best in de TaskService geplaatst worden. Zo blijven enkel de event handlers en de presentatielogica over in de logica files en kan alle andere code herbruikt worden doorheen de app.
@Injectable({
providedIn: 'root'
})
export class TaskService {
// Overige code weggelaten omwille van de duidelijkheid
getFilteredTasks(filter: Filter): Task[] {
return this.getAllTasks()
.filter(t => TaskService.taskMatchesFilter(t, filter));
}
private static taskMatchesFilter(task: Task, filter: Filter): boolean {
if (Filter.all === filter) {
return true;
}
return (filter === Filter.completed && task.done) ||
(filter === Filter.toDo && !task.done);
}
}
Nu kunnen we de TaskService injecteren in de HomePage, we geven deze service een public modifier zodat de service rechtstreek vanuit de template file aangesproken kan worden. We passen de template tenslotte ook aan zodat de data uit de service komt in de plaats van uit de logica file.
export class HomePage {
constructor(public taskService: TaskService) {}
// Overige code weggelaten omwille van de duidelijkheid
}
<ion-list lines='full'>
<ion-item-sliding *ngFor='let task of taskService.getFilteredTasks(selectedFilter)'>
<ion-item-options side='start'>
<ion-item-option color='danger' (click)='taskService.deleteTask(task.id)'>
<ion-icon slot="icon-only" name='trash'></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label>
{{task.name}}
</ion-label>
<ion-icon *ngIf='task.done' name='checkmark-circle' color='success'
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
<ion-icon *ngIf='!task.done' name='checkmark'
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
</ion-item>
</ion-item-sliding>
</ion-list>
Om een nieuwe taak aan te maken is het nodig om de service ook te injecteren in de TaskPage klasse. Via een nieuwe methode createTask kunnen we dan een nieuwe taak aanmaken door middel van de geïnjecteerde service. Als een taak aangemaakt is, moeten we natuurlijk terug navigeren naar de vorige pagina, dit kan met de NavController die we eerder gebruikt hebben om een back-knop te bouwen.
export class TaskPage implements OnInit {
// Niet relevante code weggelaten.
constructor(public navController: NavController, public taskService: TaskService) {
}
createTask(): void {
this.taskService.newTask(this.taskName, this.description, this.deadline);
this.navController.back();
}
}
Navigatie met parameters
De TaskPage wordt momenteel gebruikt om nieuwe taken aan te maken, maar we kunnen deze pagina eveneens gebruiken om een bestaande taak to openen en te bewerken. Hiervoor is namelijk exact dezelfde lay-out nodig als om een taak aan te maken.
Om een taak te bewerken of een bestaande taak te tonen is het noodzakelijk om te weten over welke taak het gaat, deze informatie moet doorgegeven worden aan de TaskPage, hiervoor kunnen routing parameters gebruikt worden. Om een route met een parameter te definiëren kan /:paramNaam achteraan de route toegevoegd worden. Deze syntax is uitbreidbaar naar meerdere parameters, e.g. /:param1Naam/:param2Naam.
Het is niet voldoende om aan de bestaande route ('/task') een parameter toe te voegen. Dit zou betekenen dat we de route niet meer kunnen gebruiken om een nieuwe taak aan te maken. We moeten dus twee routes definiëren naar de TaskPage, een route met parameter (read/update) en een route zonder parameter (create).
const routes: Routes = [
{
path: '',
component: HomePage,
},
{
path: 'task',
loadChildren: () => import('./task/task.module').then(m => m.TaskPageModule)
},
{
path: 'task/:id',
loadChildren: () => import('./task/task.module').then(m => m.TaskPageModule)
}
];
Route met parameter definiëren in de template
Nu de route bestaat kunnen we deze gebruiken in de HomePage. Als op een taak geklikt wordt openen we de route 'task/:id'. Merk op dat de elementen in de array op lijn 2 automatisch geconcateneerd worden met een '/' als scheidingsteken.
<ion-item>
<ion-label [routerLink]="['task', task.id]">
{{task.name}}
</ion-label>
<ion-icon *ngIf='task.done' name='checkmark-circle'
color='success' (click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
<ion-icon *ngIf='!task.done' name='checkmark'
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
</ion-item>
Visuele problemen?
Heb je visuele problemen met het routerLink directive, zoals een kader rond de knop als je er op klikt. Het routerLink is bedoeld voor a, ion-button, en ion-fab-button elementen. Op andere elementen kan het gebeuren dat er een kader getoond wordt als er op de ingedrukt wordt. Je kan dit programmatorisch oplossen.

Dit probleem is eenvoudig op te lossen door gebruik te maken van een click handler en de twee services Router en ActivatedRoute. De eerste service bevat methodes om te navigeren, de tweede bevat informatie over de huidige route (URL, parameters, ...). We injecteren deze twee services in de HomePage.
Vervolgens definiëren we een click handler in de HomePage template. Hier gebruiken we de ActivatedRoute service om een pad relatief ten opzichte van de huidige route te definiëren. Zonder deze optionele tweede parameter zou de navigate methode werken ten opzichte van de root van de applicatie.
export class HomePage {
// Niet relevante code weggelaten
constructor(public taskService: TaskService,
public router: Router, public activatedRoute: ActivatedRoute) {
}
}
<ion-item>
<ion-label
(click)="router.navigate(['task', task.id], {relativeTo: activatedRoute})">
{{task.name}}
</ion-label>
<ion-icon *ngIf='task.done' name='checkmark-circle'
color='success' (click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
<ion-icon *ngIf='!task.done' name='checkmark'
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
</ion-item>
Bestaande taak bekijken
Nu de route bruikbaar is, moet de TaskPage nog uitgebreid worden. Hier injecteren we de ActivatedRoute service om de navigatie parameters op te halen.
export class TaskPage implements OnInit {
// Niet relevante code wegelaten.
constructor(public navController: NavController, public taskService: TaskService,
public activatedRoute: ActivatedRoute) {
// Niet relevante code wegelaten.
}
}
We kunnen deze service gebruiken in een nieuwe methode die de parameter id inleest en, indien de parameter bestaat, de details van de taak ophaalt. Let op, ook al hebben we de parameter in de HomePage template meegegeven als number, is het toch nodig om de conversie naar van string naar number te maken. De parameter wordt via de URL doorgegeven en is dus per definitie een string.
We hebben natuurlijk ook een methode getTask nodig in de TaskService.
setData(): void {
const id = this.activatedRoute.snapshot.paramMap.get('id');
// No need to continue with the function if no parameter was specified.
if (id === null) {
return;
}
// Route parameters are always strings, the conversion to number must happen manually.
this.id = Number(id);
const task = this.taskService.getTask(this.id);
if (task) {
this.taskName = task.name;
this.deadline = task.deadline ?? '';
this.description = task.description ?? '';
this.done = task.done;
}
}
getTask(id: number): Task | undefined {
return this.#taskList.find(t => t.id === id);
}
Tenslotte moet de setData methode nog opgeroepen worden, hiervoor gebruiken we de ngOnInit methode die automatisch toegevoegd wordt door ionic generate. We moeten deze methode gebruiken omdat de route parameter pas beschikbaar is als de constructor uitgevoerd is en de ngOnInit methode zal na de constructor uitgevoerd worden.
export class TaskPage implements OnInit {
// Niet relevante data weggelaten.
ngOnInit(): void {
this.setData();
}
}
Bestaande taak bijwerken
Om een bestaande taak bij te werken is weinig werk vereist, we gebruiken opnieuw de TaskPage, hierboven hebben we de details van een bestaande taak opgehaald, nu rest enkel om wijzigingen weg te schrijven. Hiervoor hebben we een nieuwe methode updateTask nodig in de TaskService.
updateTask(updatedTask: ITask): void {
const task = this.taskList.find(t => t.id === updatedTask.id);
if (task === undefined) {
console.error('Trying to update a nonexistent task.');
return;
}
Object.assign(task, updatedTask);
}
Vervolgens voegen we ook een methode updateTask toe aan de TaskPage. Nu hebben we echter 2 mogelijke acties als er op het vinkje (creëren/bijwerken) gedrukt wordt. Als het id undefined is zullen we een taak bijwerken, anders een taak aanmaken. Hiervoor gebruiken we een nieuwe methode. We moeten deze methode natuurlijk ook koppelen in de template.
handleCreateAndUpdate(): void {
if (this.id === undefined) {
this.createTask();
} else {
this.updateTask();
}
this.navController.back();
}
private updateTask(): void {
if (!this.id) {
this.createTask();
} else {
this.taskService.updateTask({
id: this.id,
name: this.taskName,
deadline: this.deadline,
description: this.description,
done: this.done
});
}
}
private createTask(): void {
this.taskService.newTask(this.taskName, this.description, this.deadline);
}
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>New Task</ion-title>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-buttons slot="end">
<ion-button (click)="handleCreateAndUpdate()">
<ion-icon slot="icon-only" name="checkmark"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
Shared Components
Leesbare en onderhoudbare code is belangrijk, daarom is het aan te raden om zoveel mogelijk gebruik te maken van componenten, een herbruikbaar en afgezonderd stuk code.
De HomePage bevat een lijst van taken waarin elk list-item exact dezelfde structuur heeft, voor zo'n list-item kan een herbruikbare component gebouwd worden. Deze component kan dan later (in de oefeningen) op een andere pagina herbruikt worden en zo wordt de template van de HomePage korter en overzichtelijker.

Net zoals services en pagina's maken we een component ook aan via ionic generate. Een component die in meerdere modules (pagina's) gebruikt wordt, moet voor Angular afgezonderd worden in een module (vanaf versie 14 is een alternatief beschikbaar, maar dit is nog in beta en gebruiken we nog niet). Deze module kan dan geïmporteerd worden in alle pagina's die de component gebruiken. We noemen deze module shared en maken hier alvast een map voor aan.
Een component kan natuurlijk ook gegenereerd worden als onderdeel van een bestaande module. Dit is echter enkel nuttig als de component in geen geval gebruikt zal worden in meerdere pagina's of projecten.
ionic generate module shared
ionic generate component shared/taskItem --no-spec

Om de nieuwe TaskItemComponent te gebruiken in de HomePage moet deze eerst gedeclareerd en geëxporteerd worden in de SharedModule. Om, zonder problemen, gebruik te kunnen maken van de Ionic componenten moet de IonicModule ook geïmporteerd worden in de SharedModule. Tenslotte is ook de RouterModule nodig, deze module zorgt ervoor dat het [routerLink] directive beschikbaar is. Deze module moet vervolgens geïmporteerd worden in de HomeModule.
@NgModule({
declarations: [TaskItemComponent],
exports: [TaskItemComponent],
imports: [
CommonModule,
IonicModule,
RouterModule
]
})
export class SharedModule { }
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
HomePageRoutingModule,
SharedModule
],
declarations: [HomePage]
})
export class HomePageModule {}
TaskItemComponent
De TaskItemComponent moet een taak weergeven, dit betekent dat de taak doorgeven moet worden aan deze component. Een navigatie parameter is geen oplossing, want de component wordt gebruikt als onderdeel van een pagina en is zelf geen pagina. Omdat een component gebruikt kan worden als een HTML-element, is het ideaal om de taak door te geven via de attributen van dit HTML-element. Een attribuut kan door middel van de @Input decorator gedefinieerd worden op een component. Op lijn 6 wordt [task] gebruik om de Angular compiler duidelijk te maken dat dit attribuut verplicht meegegeven moet worden. Vervolgens kunnen we de non-null assertions (!) gebruiken om aan aan de TypeScript compiler duidelijk te maken dat het attribuut altijd ingevuld zal zijn (TypeScript & Angular communiceren niet altijd).
Aangezien deze component één item in de lijst voorstelt, zal deze component ook de deletefunctionaliteit moeten ondersteunen. Daarom importeren we de TaskService.
import { Component, Input, OnInit } from '@angular/core';
import { TaskService } from '../../services/task.service';
import { Task } from '../../../datatypes/task';
@Component({
selector: 'app-task-item[task]',
templateUrl: './task-item.component.html',
styleUrls: ['./task-item.component.scss'],
})
export class TaskItemComponent implements OnInit {
@Input() !task: Task;
constructor(public taskService: TaskService,
public activatedRoute: ActivatedRoute, public router: Router) {}
ngOnInit(): void {}
}
Via de @Component decorator wordt in bovenstaande code gespecifieerd onder welke naam de nieuwe component gebruikt kan worden. In dit geval kan de component gebruikt worden als
<app-task-item></app-task-item>
De template task-item.component.html krijgt alle inhoud die in home.page.html in de *ngFor stond. Merk op dat er geen <ion-content> aanwezig is in de component. Vervolgens kunnen we deze component gebruiken in de HomePage.
<ion-item-sliding>
<ion-item-options side='start'>
<ion-item-option color='danger' (click)='taskService.deleteTask(task.id)'>
<ion-icon slot="icon-only" name='trash'></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label (click)="router.navigate(['task', task.id], {relativeTo: activatedRoute})">
{{task.name}}
</ion-label>
<ion-icon *ngIf='task.done' name='checkmark-circle'
color='success' (click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
<ion-icon *ngIf='!task.done' name='checkmark'
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
</ion-item>
</ion-item-sliding>
<ion-list lines='full'>
<app-task-item
*ngFor='let t of taskService.getFilteredTasks(selectedFilter)' [task]='t'>
</app-task-item>
</ion-list>
Hier geven we de taak die getoond moet worden in de component door via het task attribuut op lijn 3. Merk op dat we hier property binding ([]) moeten gebruiken, anders zou de waarde t niet gezien worden als de iteratie variable, maar als een string.