1. Ionic & Angular
1. Ionic & Angular
Tijdens deze lessenreeks leer je de basis van Ionic en Angular. De voorbeeldapplicatie blijft enigszins beperkt. Er wordt geen persistente storage voorzien, ook routing en meerdere pagina's worden pas in een volgende les toegevoegd.
Project aanmaken
Om een project aan te maken kan je het ionic start commando gebruiken.
Hint
Voer dit commando niet uit in een map die met de cloud gesynchroniseerd wordt. Voor één project zijn er duizenden kleine files nodig. Dit vertraagd je cloud storage enorm. Het is ook mogelijk dat je andere problemen ondervindt als je gebruik maakt van een met de cloud gesynchroniseerde map om je project te bewaren. Een git repository met een online remote, is een veel veiligere keuze.
Het eerdergenoemde commando krijgt steeds de naam van het nieuwe project als parameter. Vervolgens wordt er gevraagd om een framework te selecteren, Angular, React en Vue worden momenteel ondersteund door Ionic. Voor deze cursus gebruiken we Angular.
Om een nieuw project met de naam 'voorbeeld' aan te maken voer je dus het commando ionic start voorbeeld uit. Tijdens dit process zul je eventueel gevraagd worden om gegevens met Google te delen en om een gratis Ionic account aan te maken. Geen van deze opties heeft invloed op de cursus, kies dus wat jou het beste lijkt.
Pick a framework!
Please select the JavaScript framework to use for your new app.
To bypass this prompt next time, supply a value for the --type option.
? Framework: (Use arrow keys)
> Angular | https://angular.io
React | https://reactjs.org
Vue | https://vuejs.orgDe tweede vraag waar Ionic een antwoord op vereist, is de start-template die gebruikt moet worden. Natuurlijk is hier geen vaste keuze, voor deze les (en de meeste andere lessen en oefeningen) gebruiken we de lege template.
Let's pick the perfect starter template!
Starter templates are ready-to-go Ionic apps that come
packed with everything you need to build your app. To bypass this
prompt next time, supply template, the second argument to ionic start.
? Starter template:
tabs | A starting project with a simple tabbed interface
sidemenu | A starting project with a side menu with navigation in the content area
> blank | A blank starter project
list | A starting project with a list
my-first-app | An example application that builds a camera with gallery
conference | A kitchen-sink application that shows off all Ionic has to offerVervolgens vraagt Ionic naar het soort project dat aangemaakt moet worden, we gebruiken de iets oudere manier die gebruik maakt van NgModules.
? Would you like to build your app with NgModules or Standalone Components?
Standalone components are a new way to build with Angular that simplifies the way you build your app.
To learn more, visit the Angular docs:
https://angular.io/guide/standalone-components
(Use arrow keys)
> NgModules
StandaloneNadat de template en het soort project gekozen zijn, zal Ionic alle nodige bestanden downloaden. Dit kan enige tijd duren, zeker op trage netwerkverbindingen of als je op een klassieke harde schijf werkt.
Ionic projectstructuur
Als je de map die hierboven aangemaakt is opent in een editor naar keuze (WebStrom, Visual Studio Code, ...), dan zul je zien dat er standaard al een hele hoop bestanden aanwezig zijn. Hieronder worden enkel die bestanden besproken die belangrijk zijn voor het verdere verloop van de cursus. In onderstaande beschrijvingen staat '/' voor de root directory, de map waarin het project bewaard is.
Waarschuwing
We beperken ons hieronder tot de bestanden die nog niet gekend zijn uit een ander vak in deze leerlijn, de studenten die het modeltraject niet volgen zijn zelf verantwoordelijk om deze informatie bij te werken. We verwijzen door naar les 1 uit het OPO frontend frameworks.
/src
De src map is de belangrijkste map in het volledige project. Hierin zullen we code schrijven, bestanden aanpassen, ...
/src/index.html
Deze file vormt het ingangspunt van je applicatie. In dit bestand mag enkel de inhoud van het head element aangepast worden. De body blijft steeds onveranderd.
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>Angular zal de app-root component dynamisch opvullen met de juiste componenten. Dit komt dus overeen met het root element in React.
/src/global.scss
Dit bestand kan gebruikt worden om SCSS of CSS-regels toe te voegen die voor de volledige applicatie gelden.
/src/main.ts
Het eerste TypeScript bestand dat gelezen en uitgevoerd wordt. Dit bestand wordt door de Angular cli uitgevoerd als we het project starten of compileren en wordt enkel in les 3 aangepast.
/src/theme/variables.scss
Het Ionic theme, standaard bevat dit bestand zowel een light als een dark theme. De kleuren die door een Ionic applicatie gebruikt worden kunnen hier aangepast worden.
/src/environments
Deze map bevat twee files, environment.ts en environment.prod.ts, deze worden respectievelijk gebruikt om globale variabelen te definiëren voor een development en productie build. We gebruiken deze bestanden in les 5 en 5 om API keys en andere relevante gegevens voor het communiceren met externe services te bewaren.
/src/assets
Hier kunnen afbeeldingen en andere statische resources geplaatst worden. Dit is echter zelden nodig. Gebruik deze map bij voorkeur niet om JavaScript bibliotheken of fonts toe te voegen aan je project. Hiervoor gebruik je een package-manager, zo worden alle resources in package.json geplaatst en kunnen deze eventueel geoptimaliseerd worden.
/src/app
Deze map bevat de eigenlijke code, hierin zullen we dan ook de meeste aanpassingen maken. Deze mappenstructuur kan zelf aangemaakt en aangepast worden, voor dit vak gebruiken we echter de door Ionic aanbevolen mappenstructuur. Een applicatie wordt opgedeeld in pagina's, deze pagina's zijn opgebouwd uit, al dan niet gedeelde, componenten en hebben eventueel sub-pagina's. Deze bestanden kunnen gegenereerd worden door het ionic generate [page | module | service | component | module | directive] commando. Hieronder wordt de samenhang tussen de verschillende bestanden in de standaard mappenstructuur uitgelegd.
/src/app/app-routing.module.ts
Hierin worden de routes (URL's) gedefinieerd. Aangezien dit de root component is, worden hier alle root routes beschreven. Dus alles van de vorm /SomePage. Meer info volgt in les 2.
/src/app/app.component.html
De template file voor de app component, hierin wordt de UI gedefinieerd en worden acties aan bepaalde evenementen gekoppeld. Hier wordt ook gedefinieerd hoe data weergegeven wordt. Dit is een HTML-bestand waarin enkele Angular syntax uitbreidingen toegestaan zijn.
/src/app/app.component.scss
De opmaak voor de template file app.component.html. Dit bestand mag zowel CSS als SCSS-code bevatten. Het is echter aan te raden om dit bestand zo min mogelijk aan te passen, Ionic voorziet namelijk heel wat UI-elementen met een adaptieve opmaak, daarnaast zijn er ook verschillende CSS utilities beschikbaar en is het dus zelden nodig om zelf CSS-code te schrijven.
/src/app/app.component.ts
Deze file bevat de TypeScript code die data ophaalt, filtert of aanpast. Ook event handlers en buisiness logic kunnen hier gedefinieerd worden.
In dit bestand wordt de link gelegd tussen de template en scss files. De decorator @Component op lijn 3, vertelt Angular dat deze file een component definieert. Op lijn 4 wordt de naam van de component gedefinieerd, dit is de naam waaronder deze component in andere componenten gebruikt kan worden. We zullen hier pas volgende les gebruik van maken. Op lijn 5 wordt de template file gekoppeld. Lijn 6 definieert de stylesheets, in tegenstelling tot de vorige attributen, is het mogelijk om meerdere stylesheets te linken aan één component.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor() {}
}/src/app/app.components.spec.ts
Hierin kunnen tests geschreven worden, dit valt buiten de scope van onze cursus.
/src/app/app.module.ts
Dit bestand wordt gebruikt om bibliotheken te importeren en te exporteren. Angular vereist een minimum van één module per project. Wij zullen echter de Ionic template volgen en één module per pagina gebruiken. Modules worden automatisch gegenereerd als we een pagina aanmaken.
We overlopen in onderstaande code de stukken die relevant zijn, niet automatisch gegenereerd kunnen worden, of later aangepast moeten worden.
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {RouteReuseStrategy} from '@angular/router';
import {IonicModule, IonicRouteStrategy} from '@ionic/angular';
import {AppComponent} from './app.component';
import {AppRoutingModule} from './app-routing.module';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
providers: [{provide: RouteReuseStrategy, useClass: IonicRouteStrategy}],
bootstrap: [AppComponent],
})
export class AppModule {
}De declarations array, die op lijn 11 gedefinieerd is, wordt gebruikt om aan te geven welke componenten, directives, pipes, ... in deze module zitten. Een component die hier gedefinieerd is, kan in alle andere componenten in de module gebruikt worden. Momenteel staat hier enkel de AppComponent omdat dit de enige component in de module is, voor de HomePage wordt namelijk een aparte module aangemaakt.
De imports array (lijn 12) wordt gebruikt om modules en bibliotheken te importeren, dit is de array die we het vaakst zullen aanpassen.
Providers (lijn 13) zijn klassen die geïnjecteerd kunnen worden in andere componten of services. We maken hier pas volgende les gebruik van.
De bootstrap array bevat de root component van je volledige applicatie. Dit attribuut wordt enkel in de app.module.ts gedefinieerd. De AppComponent wordt door Angular in het <app-root></app-root/> tag in index.html geplaatst.
/src/app/home/*
De home component bevat dezelfde structuur als de app component. Het is deze component die we tijdens deze les zullen uitbreiden.
To-Do Applicatie
Tijdens deze les worden de elementaire concepten geïllustreerd door een eenvoudige to-do app te ontwikkelen. Gebruikers kunnen taken toevoegen en als afgewerkt markeren. Zoals eerder vermeld, zal deze app nog geen persistente data bevatten.
Een Ionic applicatie kan gestart worden door in de terminal, in de root folder van je applicatie, onderstaand commando uit te voeren.
ionic serveTitels
Een Ionic app is bedoeld voor zowel iOS als Android, daardoor moeten sommige dingen dubbel gebeuren. Volgens de iOS guidelines moeten belangrijke titels waar nadruk op gelegd wordt extra groot gedefinieerd worden. Als er naar beneden gescrold wordt zal de titel kleiner worden en verplaatst worden naar de navigatiebalk. Android heeft zulke richtlijnen niet.

We vervangen alle code in home.page.html met onderstaande code. De titel op lijn 4 is de standaard titel, diegene die op lijn 13 is de grote iOS titel. Hier zorgt het attribuut collapse="condense" (lijn 10) er voor dat de grote titel niet getoond als je naar beneden scrolt.
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
To-Do
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">
To-Do
</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>ITask model
We bouwen een To-Do app, dus is er nood aan een model. We hebben dus een interface of class nodig. In dit geval willen we enkel data bijhouden en geen complexe bewerkingen uitvoeren op deze data en is een interface voldoende.
export interface ITask {
name: string;
id: string;
done: boolean;
}Taken tonen
Waarschuwing
We gebruiken in het verdere verloop van deze les, en in de volgende les een in-memory database zonder persistentie om de scope van de voorbeelden te beperken. Als je dit doet in je project, verlies je hier veel punten door. Maak gebruik van één van de manieren om data persistent te bewaren die verder in de cursus besproken worden.
We willen een lijst van taken tonen op de home pagina (home.page.html) van de applicatie. Hiervoor moeten we de template file aanpassen, maar voor we hieraan kunnen beginnen moeten we een lijst taken hebben om te tonen.
Aangezien de data op home.page.html getoond moet worden, is het nodig om de lijst taken te bewaren in de bijhorende logica file home.page.ts. We definiëren hier een lege array taskList (lijn 11).
import {Component} from '@angular/core';
import {ITask} from '../../models/ITask'
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
taskList: ITask[] = []
constructor() {
}
}We hebben (nog) geen API of persistente storage, voorlopig werken we dus met vluchtige data, data die verdwijnt nadat de pagina herladen wordt. Om de UI te bouwen is het handig als er al enkele taken aanwezig zijn. We voegen in de constructor een lus toe die 50 test taken toevoegt, elke taak met een even i wordt als afgewerkt gemarkeerd. De randomUUID methode die hieronder gebruikt wordt is enkel beschikbaar op een HTTPS verbinding.
export class HomePage {
taskList: ITask[] = []
constructor() {
for (let i = 0; i < 50; i++) {
this.taskList.push({
name: 'Task ' + i,
done: i % 2 === 0,
id: window.crypto.randomUUID(),
})
}
}
}Om de taken te tonen gebruiken we een ion-list. Angular bevat het *ngFor directive dat toegevoegd kan worden aan een HTML-element of Angular component om het een aantal keren te herhalen.
<!-- Wegelaten in het fragment -->
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<!-- Wegelaten in het fragment -->
</ion-header>
<ion-list>
<ion-item *ngFor='let task of taskList'>
<ion-label>
{{task.name}}
</ion-label>
</ion-item>
</ion-list>
</ion-content>Zoals in de voorgaande code, op lijn 8, te zien is, krijgt het *ngFor directive een normale JavaScript ES6 lus als argument. Deze lus loopt over alle taken in de array taskList, die gedefinieerd is in de logica file (home.page.ts). Binnen de <ion-item> component kan de iteratievariabele task gebruikt worden.
Begrip: *ngFor Directive
Het *ngFor directive kan gebruikt worden om over een iterable te itereren in een template. Dit directive wordt als attribuut meegegeven aan een component of HTML-element.
De syntax is als volgt:
<component *ngFor="let variable of iterable">
<!--Scope of the variable-->
</component>Op lijn 4 gebruiken we string interpolation, dit stelt ons in staat een variabele als string uit te printen, merk op dat variabele die uitgeprint wordt omgeven is door dubbele accolades.
Begrip: String Interpolation
String interpolation kan gebruikt worden om in een Angular template een string of expressie waarvan het resultaat naar een string geconverteerd kan worden, uit te printen. Wijzigingen in de variabele zijn onmiddellijk zichtbaar in de template. String interpolation wordt aangeduid met dubbele accolades.

Taken afwerken
We hebben nu een lijst van taken, maar de optie om een taak als afgewerkt te markeren ontbreekt nog. We zullen een lege cirkel gebruiken om een niet-afgewerkte taak aan te geven, een afgewerkte taak wordt aangeduid met een checkmark op een ingekleurde cirkel. Om dit te implementeren kunnen we gebruik maken van Ion Icons
Afhankelijk van de status van de taak moet een ander icoon getoond worden, Angular biedt hiervoor een oplossing. Het *ngIf directive kan gebruikt worden om elementen conditioneel te renderen.
<ion-list>
<ion-item *ngFor='let task of taskList'>
<ion-label>
{{task.name}}
</ion-label>
<ion-icon *ngIf='task.done' name='checkmark-circle'></ion-icon>
<ion-icon *ngIf='!task.done' name='ellipse-outline'></ion-icon>
</ion-item>
</ion-list>Begrip: *ngIf directive
Het *ngIf directive kan gebruikt worden om voorwaardelijk te renderen. Dit directive wordt als attribuut meegegeven aan een component of HTML-element.
De syntax is als volgt:
<component *ngIf="boolean expression">...</component>Else tak
In de plaats van verschillende *ngIf directives na elkaar te gebruiken, is het ook mogelijk om een else tak toe te voegen. Hiervoor moet de code die in de else tak getoond moet worden, omringt worden door een <ng-template> component. De component krijgt vervolgens een naam (notCompleted in onderstaand voorbeeld). We kunnen deze naam tenslotte gebruiken in het *ngIf directive.
<ion-icon *ngIf='task.done; else notCompleted'
name='checkmark-circle'
(click)='toggleTaskStatus(task.id)'></ion-icon>
<ng-template #notCompleted>
<ion-icon name="ellipse-outline"
(click)='toggleTaskStatus(task.id)'></ion-icon>
</ng-template>:::
Momenteel heeft elke afgewerkte taak een zwarte achtergrond, dit is niet bijzonder aantrekkelijk of duidelijk. Ionic voorziet een aantal basiskleuren, we gebruiken de kleur 'success' om de achtergrond van een afgewerkte taak groen te maken.
<ion-icon *ngIf='task.done' name='checkmark-circle' color='success'></ion-icon>
De status van de taak is nu duidelijk zichtbaar, nu rest enkel nog het afwerken van een taak. Hiervoor gebruiken we event binding, meer specifiek linken we het click event aan een nieuwe methode toggleTaskStatus in de logica file.
Begrip: Event Binding
Event binding kan gebruikt worden om te reageren op acties van de gebruiker, events zoals click en change worden zeer frequent gebruikt. Het event wordt steeds omringd door ronde haken. Event binding wordt gedefinieerd in de template, de syntax is als volgt:
<component (event)="someMethod()">...</component>import {Component} from '@angular/core'
@Component({
selector: 'app-someComponent',
templateUrl: 'someComponent.page.html',
})
export class SomeComponentPage {
constructor() {
}
someMethod() {
// Do something with the click event.
}
}:::
<ion-icon *ngIf='task.done' name='checkmark-circle' color='success'
(click)='toggleTaskStatus(task.id)'></ion-icon>
<ion-icon *ngIf='!task.done' name='checkmark'
(click)='toggleTaskStatus(task.id)'></ion-icon>export class HomePage {
// Niet relevante code weggelaten.
toggleTaskStatus(id: string): void {
const task = this.taskList.find(t => t.id === id)
if (task) {
task.done = !task.done
}
}
}Nieuwe taken toevoegen
Om nieuwe taken toe te voegen maken we gebruik van een floating action button (FAB). We voegen deze knop rechts onderaan toe, hiervoor stellen we, zoals in de documentatie te lezen is, het attribuut vertical in op 'bottom' en het attribuut horizontal op 'end'. De knop wordt in de <ion-content> component in home.page.html toegevoegd.
<ion-fab vertical='bottom' horizontal='end' slot='fixed'>
<ion-fab-button>
<ion-icon name='add'></ion-icon>
</ion-fab-button>
</ion-fab>Property binding
Het is mogelijk om via property binding attributen van een component een waarde te geven vanuit de logica file. Hieronder passen we de FAB aan zodat deze zijn verticale positie vanuit de logica file krijgt. Vervolgens schrijven we een methode die deze waarde elke 250 milliseconden aanpast. Dit demonstreert de kracht van property binding, maar heeft verder geen nut in onze applicatie, in het uitgewerkte lesvoorbeeld staat dit dan ook in commentaar.
<ion-fab [vertical]='verticalFabPosition' horizontal='end' slot='fixed'>
<ion-fab-button>
<ion-icon name='add'></ion-icon>
</ion-fab-button>
</ion-fab>export class HomePage {
taskList: ITask[] = []
verticalFabPosition: 'bottom' | 'top' = 'bottom';
constructor() {
// Rest van de code is verborgen.
setInterval(
() => {
this.verticalFabPosition =
this.verticalFabPosition === 'bottom' ? 'top' : 'bottom';
},
250
)
}
}Begrip: Property Binding
Property binding kan in de template gebruikt worden door vierkante haken rond een attribuut te plaatsen. De waarde van dit attribuut komt vervolgens uit een variabele in de logica file. Als de variabele aangepast wordt in de logica file, zijn deze wijzigingen ogenblikkelijk zichtbaar in de UI.
<component [attribute]="variabeleInLogicFile">...</component>;import {Component} from '@angular/core'
@Component({
selector: 'app-someComponent',
templateUrl: 'someComponent.page.html',
})
export class SomeComponentPage {
variableInLogicFile = "Some Value"
constructor() {
}
}:::
Alert
We gebruiken een popup venster om de nieuwe taak aan te maken. Dit is geen goede UX keuze voor complexere data, we gebruiken dit hier enkel omdat we nog geen routing gezien hebben. Gebruik een Alert enkel voor ja/nee vragen, of input waar maximaal één veld voor nodig is.
In de documentatie (van de controlled alert) is te zien dat we een AlertController nodig hebben, deze moet via dependency injection geïnjecteerd worden in de HomePage. We kunnen de controller injecteren op de manier zoals het in de documentatie staat, of via de inject functie. In deze cursus verkiezen we de tweede manier omdat deze veel leesbaarder is als we veel dingen moeten injecteren.
Merk op dat we de AlertController instantie twee keer als private markeren, de eerste keer doen we dit door er een # voor te zetten, de tweede keer via het private keyword. Het private keyword is een TypeScript constructie terwijl de # een nieuwe feature binnen JavaScript is. De private modifier is eenvoudig te omzeilen op de client terwijl dit voor de # onmogelijk is. Omdat de # nog niet beschikbaar is in de meeste browsers, maakt het voorlopig weinig uit wel van de twee je gebruikt.
import {Component, inject} from '@angular/core'
import {AlertController} from '@ionic/angular'
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
// Niet relevante code weggelaten.
#alertController = inject(AlertController)
constructor() {
// Niet relevante code weggelaten.
}
}import {Component, inject} from '@angular/core'
import {AlertController} from '@ionic/angular'
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
// Niet relevante code weggelaten.
constructor(private alertController: AlertController) {
// Niet relevante code weggelaten.
}
}Begrip: Dependency Injection
Bepaalde klassen kunnen in Angular geïnjecteerd worden in een pagina, component of service. Op deze manier is een instantie van de klasse beschikbaar zonder dat deze expliciet geïnstantieerd moet worden. Het injecteren kan op twee manieren:
import {Component, inject} from '@angular/core'
import {SomeInjectable} from 'foo'
@Component({
selector: 'app-bar',
templateUrl: 'bar.page.html',
styleUrls: ['bar.page.scss'],
})
export class BarPage {
#someInjectable = inject(SomeInjectable)
// Voor een publieke variable
// someInjectable = inject(SomeInjectable)
constructor() {
}
}import {Component, inject} from '@angular/core'
import {SomeInjectable} from 'foo'
@Component({
selector: 'app-bar',
templateUrl: 'bar.page.html',
styleUrls: ['bar.page.scss'],
})
export class BarPage {
constructor(private someInjectable: SomeInjectable) {}
// Voor een publieke variabele
// constructor(public someInjectable: SomeInjectable) {}
}:::
Op basis van de documentatie komen we tot volgende code om een alert weer te geven.
export class HomePage {
// Niet relevante code weggelaten.
#alertController = inject(AlertController)
constructor() {
// Niet relevante code weggelaten.
}
async presentAlert(): Promise<void> {
const alert = await this.#alertController.create({
header: 'New ITask',
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary'
}, {
text: 'OK',
handler: (inputs) => {
this.createTask(inputs.name);
}
}
],
inputs: [
{
name: 'name',
type: 'text',
placeholder: 'Task Description'
}
]
})
await alert.present()
}
createTask(name: string): void {
this.taskList.push({
name,
id: window.crypto.randomUUID(),
done: false
})
}
}Tenslotte moet de FAB button nog aangepast worden zodat deze de methode presentAlert() oproept.
<ion-fab [vertical]='verticalFabPosition' horizontal='end' slot='fixed'>
<ion-fab-button (click)='presentAlert()'>
<ion-icon name='add'></ion-icon>
</ion-fab-button>
</ion-fab>