2. Single Page Applications
2. 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 Canvas).
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 1 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],
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 importeert 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 aangemaakt is. 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 laadt 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/taskHier 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-specIonic 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 er 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 laadt.
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 methode presentAlert mag 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. Wil 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 ten opzichte 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 ITask interface uitgebreid worden. We voegen een optionele deadline en beschrijving toe.
export interface Task {
name: string
id: string
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 1 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.
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-input type="text" labelPlacement="floating" label="Task name" class="ion-margin-top"
[(ngModel)]="taskName">
</ion-input>
</ion-item>
<ion-item lines="full">
<ion-textarea label="Description" labelPlacement="floating" 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 | null = null
deadline: string | null = null
description: string | null = null
// 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 werkt, 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.
<ion-buttons slot="start">
<ion-button (click)="navController.back()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>import {NavController} from '@ionic/angular'
export class TaskPage implements OnInit {
navController = inject(NavController)
// Niet relevante code weggelaten.
}Bovenstaande 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.
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: Services
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.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class FooService {
constructor() { }
}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 naar de nieuwe service (en passen de create methode aan zodat de beschrijving en de deadline meegegeven kunnen worden). 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. Dit is niet alleen properder, maar maakt het ook eenvoudiger om bugs op te sporen en te voorkomen.
We verhuizen de code om de taken te filteren ook naar de service, maar enkel die code die herbruikbaar is, de change handler voor het formulier blijft dus in home.page.ts.
Enkel de event handlers en de presentatielogica blijven over in de logica file.
@Injectable({
providedIn: 'root'
})
export class TaskService {
#taskList: ITask[] = []
constructor() {
}
get tasks(): ITask[] {
return window.structuredClone(this.#taskList)
}
createTask(name: string, description: string, deadline?: string): void {
this.#taskList.push({
name,
id: window.crypto.randomUUID(),
done: false,
description,
deadline,
})
}
toggleTaskStatus(id: string): void {
const task = this.#taskList.find(t => t.id === id)
if (task) {
task.done = !task.done
}
}
deleteTask(id: string): void {
this.#taskList = this.#taskList.filter(t => t.id !== id)
}
private static taskMatchesFilter(task: ITask, filter: Filter): boolean {
if (Filter.all === filter) {
return true
}
return filter === Filter.completed && task.done || filter === Filter.toDo && !task.done
}
getFilteredTasks(filter: Filter): ITask[] {
return this.#taskList
.filter(t => TaskService.taskMatchesFilter(t, filter))
}
}Nu kunnen we de TaskService injecteren in de HomePage, we houden de service public zodat deze 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 {
taskService = inject(TaskService)
constructor() {}
}<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; else notCompleted' name='checkmark-circle'
color='success'
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
<ng-template #notCompleted>
<ion-icon name="ellipse-outline"
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
</ng-template>
</ion-item>
</ion-item-sliding>
</ion-list>Info
Hierboven hebben we de task property (gedefinieerd met een functie) en de getFilteredTasks methode gebruikt in een *ngIf en *ngFor, dit werkt, maar is absoluut niet performant. Als je een log statement één van deze twee methodes plaats, zal je zien dat deze meer dan 10 keer worden opgeroepen voor de initiële render. Ook voor alle daaropvolgende renders worden de methodes opnieuw en opnieuw opgeroepen. Voorlopig hebben we geen andere mogelijk oplossing, in les 6 bespreken we verschillende lifecycle hooks die dit probleem kunnen oplossen. Je mag voor het examenproject methodes uit een service rechtstreeks oproepen in een template, maar als je het op een performantere manier oplost, krijg je meer punten voor de kwaliteit en complexiteit.
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, die de geïnjecteerde service gebruikt, kunnen we dan een nieuwe taak aanmaken. 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.
Tenslotte moeten we de createTask methode nog linken aan de knop in de UI. Omdat de service de centrale datastore is voor de volledige applicatie, wordt de nieuwe taak onmiddellijk zichtbaar op de HomePage.
export class TaskPage implements OnInit {
// Niet relevante code weggelaten.
navController = inject(NavController)
taskService = inject(TaskService)
constructor() {
}
createTask(): void {
if (this.taskName) {
this.taskService.createTask(this.taskName, this.description ?? '', this.deadline ?? undefined)
this.navController.back()
}
}
}<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)="createTask()">
<ion-icon slot="icon-only" name="checkmark"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>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 te 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 gebruiken 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>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.
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.
We hebben natuurlijk ook een methode getTask nodig in de TaskService.
export class TaskPage implements OnInit {
// Niet relevante code weggelaten.
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
}
this.id = id
const task = this.taskService.getTask(this.id);
if (task) {
this.taskName = task.name;
this.deadline = task.deadline ?? '';
this.description = task.description ?? '';
}
}
}export class TaskService {
// Niet relevante code weggelaten.
getTask(id: string): ITask | undefined {
return window.structuredClone(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. Het is best-practice om de constructor zo snel mogelijk uit te laten voeren en hier dus zo weinig mogelijk code in te plaatsen. Om data in te laden gebruiken we steeds de ngOnInit methode (of een andere lifecycle hook, zie les 6). De ngOnInit methode wordt automatisch uitgevoerd nadat de constructor geladen is.
export class TaskPage implements OnInit {
// Niet relevante code weggelaten.
ngOnInit(): void {
this.setData();
}
}Bestaande taak bijwerken
Voordat we de UI kunnen aanpassen om de nieuwe taken toe te voegen hebben we natuurlijk een methode updateTask nodig in de TaskService. Merk op dat we het Partial utility type gebruiken voor de update methode, deze generische interface bouwt een nieuwe interface waarin alle properties optioneel zijn. Op deze manier is het mogelijk dat de methode gebruikt wordt om één of meer properties te updaten. Omdat het id niet optioneel is (zonder het id weten we niet welke taak bijgewerkt moet worden), voeg we dit nog expliciet toe. Hiervoor gebruiken we een intersection type, dit is een type dat bestaat uit de combinatie van verschillende andere types. Dus iets van het type A & B moet voldoen aan zowel interface A als interface B.
export class TaskService {
// Niet relevante code weggelaten.
updateTask(updatedTask: Partial<ITask> & {id: string}): void {
const task = this.#taskList.find(t => t.id === updatedTask.id)
if (task === undefined) {
console.error('Trying to update a nonexistent task.')
return
}
// Kopieer alle inhoud van het updatedTask object naar het task object.
// Bestaande properties in task, die dezelfde naam hebben als een property in updatedTask, worden overschreven.
Object.assign(task, updatedTask)
}
}Vervolgens voegen we ook een methode updateTask toe aan de TaskPage. Er zijn nu 2 mogelijke acties als er op het vinkje (creëren/bijwerken) gedrukt wordt. Als het id undefined is zullen we een nieuwe taak aanmaken, als het id een waarde heeft zullen we de taak waarnaar het id verwijst updaten. Om deze controle te implementeren gebruiken we een nieuwe methode de we natuurlijk ook koppelen in de template.
export class TaskPage implements OnInit {
// Niet relevante code weggelaten.
handleCreateAndUpdate(): void {
if (this.id) {
this.#updateTask()
} else {
this.#createTask()
}
this.navController.back()
}
#createTask(): void {
if (this.taskName) {
this.taskService.createTask(this.taskName, this.description ?? '', this.deadline ?? undefined)
}
}
#updateTask(): void {
if (this.id && this.taskName) {
this.taskService.updateTask({
id: this.id,
name: this.taskName,
description: this.description ?? '',
deadline: this.deadline ?? undefined,
})
}
}
}<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.
Begrip: Angular Component
Om een bepaald onderdeel van de UI af te zonder wordt gebruik gemaakt van een component. Via de @Component decorator krijgt de component een naam, in onderstaand voorbeeld is dit app-foo. Deze naam geeft aan hoe de component opgeroepen kan worden in een andere component, in onderstaand voorbeeld kan dit dus als <app-foo></app-foo/>.
ionic generate component somePath/foo --no-specimport {Component, OnInit} from '@angular/core'
@Component({
selector: 'app-foo',
templateUrl: './app-foo.component.html',
styleUrls: ['./app-foo.component.scss'],
})
export class FooComponent implements OnInit {
constructor() {
}
ngOnInit() {
}
}:::
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.

Een component die in meerdere modules (pagina's) gebruikt wordt, moet voor Angular afgezonderd worden in een module. 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, maar dit is 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. De SharedModule 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.
Begrip: Angular Inputs
Binnen Angular is het mogelijk om een input property mee te geven aan een component. Dit gebeurt via de @Input() decorator.
We kunnen optioneel ook meegeven dat de input property verplicht is. Dit werkt echter enkel voor de Angular compiler, maar niet voor de TypeScript compiler, het is dus ook nodig om de non-null-assertion operator te gebruiken om TypeScript expliciet duidelijk te maken dat deze property nooit undefined is.
import {Component, Input, OnInit} from '@angular/core'
@Component({
selector: 'app-foo',
templateUrl: './app-foo.component.html',
styleUrls: ['./app-foo.component.scss'],
})
export class FooComponent implements OnInit {
@Input({required: true}) bar!: IBar
constructor() {
}
ngOnInit() {
}
}Aangezien deze component één item in de lijst voorstelt, zal deze component ook de deletefunctionaliteit moeten ondersteunen. Daarom injecteren we de TaskService.
import {Component, inject, Input, OnInit} from '@angular/core'
import {ITask} from '../../../models/ITask'
import {TaskService} from '../../services/task.service'
@Component({
selector: 'app-task-item',
templateUrl: './task-item.component.html',
styleUrls: ['./task-item.component.scss'],
})
export class TaskItemComponent implements OnInit {
@Input({required: true}) task!: ITask
taskService = inject(TaskService)
constructor() {
}
ngOnInit() {
}
}De template task-item.component.html krijgt alle inhoud die in home.page.html binnen *ngFor stond. Merk op dat er geen <ion-content> aanwezig is in TaskItem, de component bevat enkel een stukje van de UI en geen volledige pagina. Tenslotte gebruiken we deze component gebruiken in de HomePage.
<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 [routerLink]="['task', task.id]">
{{task.name}}
</ion-label>
<ion-icon *ngIf='task.done; else notCompleted' name='checkmark-circle'
color='success' (click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
<ng-template #notCompleted>
<ion-icon name="ellipse-outline"
(click)='taskService.toggleTaskStatus(task.id)'></ion-icon>
</ng-template>
</ion-item>
</ion-item-sliding><ion-list lines="full" *ngIf="taskService.getFilteredTasks(selectedFilter).length > 0; else noMatches">
<app-task-item *ngFor='let task of taskService.getFilteredTasks(selectedFilter)'
[task]="task">
</app-task-item>
</ion-list>We geven de taak die getoond moet worden in de component door via het task attribuut op lijn 3. Merk op dat we hiervoor property binding ([]) moeten gebruiken, anders zou de waarde task niet gezien worden als de iteratie variable, maar als een string.