9. Multipage apps
9. Multipage apps
Dit hoofdstuk behandeld multipage apps in Vite. Om de concepten te illustreren wordt gebruik gemaakt van onderstaande startbestanden.
Startbestanden
Pagina's
Momenteel wordt de inhoud van /src/pages/home/home.html ingelezen in main.ts. Deze inhoud wordt dan gekoppeld aan de DOM en eventuele eventhandlers worden geregistreerd. Alhoewel deze aanpak werkt voor een eenvoudige website met één pagina, wordt het snel onoverzichtelijk en inefficient als je meerdere pagina's hebt. In zo'n geval wordt main.ts heel snel heel groot, verder moet je veel conditionele structuren toevoegen zodat events enkel op de juiste pagina gekoppeld worden. Het is duidelijk dat er een betere oplossing gevonden moet worden.
Via een abstracte klasse Page zonderen we alle gemeenschappelijke logica voor een pagina af. Voor elke pagina wordt dan een concrete subklasse voorzien die de UI en eventhandling afhandelt voor een specifieke pagina.
Begrip: Abstracte klasse
Een abstracte klasse is een klasse die niet geïnstantieerd kan worden, maar bedoeld is als basis voor andere klassen. Een abstracte klasse bevat een combinatie van concrete en abstracte methoden, een concrete methode is een methode die een body heeft en niet noodzakelijk overschreven moet worden in een subklasse. Een abstracte methode bevat daarentegen geen body en moet door een subklasse worden geïmplementeerd.
Een abstracte klasse dwingt af dat alle afgeleide klassen bepaalde methoden implementeren. Dit bevordert structuur, hergebruik van code en maakt je programma flexibeler en beter onderhoudbaar.
abstract class AbstractClass {
// Abstract method
abstract abstractMethod(): void;
// Concrete method
concreteMethod() {
console.log('This is a concrete method');
}
}Bekijk de cursus OOP of wikipedia voor meer informatie.
Page klasse
We beginnen met een eenvoudige klasse waar nog geen rekening gehouden wordt met events en die dus enkel een UI kan renderen.
Om een pagina te renderen, hebben we slechts twee dingen nodig, de HTML die gerenderd moet worden en het element waarin dit moet gebeuren. De eerste parameter is dynamisch en verschil voor elke pagina, daarom kan deze best doorgegeven worden via de constructor van de klasse. Alhoewel we de HTML-code als string kunnen bewaren, is het handiger als we de dit onmiddellijk converteren naar een HTMLElement, de klasse bevat zo een HTMLElement waarin alle de volledige pagina en alle interactiviteit (events) verwerkt zitten. Op deze manier moeten we later de (meeste) events niet opnieuw koppelen als de data op de website wijzigt.
Aangezien de body niet van buiten de klasse uitgelezen of aangepast moet worden, kan deze best private of protected gemaakt worden. Later in de les moet de body aangesproken worden vanuit een subklasse, daarom gebruiken we hier een protected veld. De property moet na de initialisatie van de klasse ook niet meer aangepast worden, bijgevolg gebruiken we de readonly modifier om de property constant te maken.
De container waarin de HTML-code gerenderd moet worden is natuurlijk gelijk voor alle pagina's, bijgevolg kan hier een static property voor gebruikt worden zodat niet elke klasse opnieuw een referentie naar het root-element moet ophalen. Ook hier gebruiken we een readonly veld, maar aangezien de subklassen deze property niet moeten aanspreken, kunnen we nu een private veld gebruiken. Dit wordt vanaf ECMAScript2022 aangeduid met een hekje vooraan in de naam van de property.
Info
TypeScript ondersteund ook de private modifier, maar deze modifier werkt enkel at-compile-time, het hekje maakt deel uit van ECMAScript en is dus ook at-runtime beschikbaar (in gewone JavaScript).
Tips
Je krijgt de Page klasse op het examen en moet deze enkel gebruiken als superklasse van één of meer concrete subklassen.
export abstract class Page {
protected readonly body: HTMLDivElement
static readonly #root = document.querySelector<HTMLDivElement>('#app')!
protected constructor(body: string) {
this.body = document.createElement('div')
this.body.innerHTML = body
}
render() {
Page.#root.innerHTML = ''
Page.#root.appendChild(this.body)
}
}Pagina renderen
Met behulp van deze nieuwe klasse kan de homepagina gerenderd worden. Om de applicatie overzichtelijk te houden plaatsten we naast het HTML-bestand een TypeScript bestand met dezelfde naam waarin de interactiviteit voor de pagina vastgelegd wordt.
Voorlopig maken we in home.ts enkel een concrete subklasse aan van de Page klasse. De HTML-code uit home.html wordt geïmporteerd, vervolgens geven we deze code mee aan de constructor van de abstracte superklasse.
Nu dat de subklasse gedefinieerd is, kan deze geïmporteerd worden in main.ts, waar vervolgens de render methode opgeroepen wordt.
import {Page} from '../../router/page.ts'
import HTML from './home.html?raw'
export class HomePage extends Page {
constructor() {
super(HTML)
}
}import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
import {HomePage} from './pages/home/home.ts'
const homePage = new HomePage()
homePage.render()
Bovenstaande code werkt, maar is momenteel nog niet veel beter dan wat er in de vorige les besproken is. Om een echt onderhoudbare applicatie te schrijven, moeten componenten geïntroduceerd worden en moet een degelijke router gebouwd worden zodat we niet voor elke link manueel een nieuw click-event moeten toevoegen.
Custom elements
Componenten zijn stukjes van de UI die afgezonderd worden en herbruikt kunnen worden over verschillende pagina's. We kunnen dit natuurlijk volledig zelf implementeren, maar JavaScript voorziet hiervoor al een mechanisme.
Begrip: Custom element
Een custom element is een zelfgedefinieerd HTML-element waarvan de werking en UI volledig bepaald wordt door de programmeur. Het element kan, na registratie bij de browser, gebruikt worden als eender welk ander HTML-element.
// Een minimaal custom element.
class HelloWorld extends HTMLElement {
static observedAttributes = ["foo"]
constructor() {
// Verplichte call naar de constructor van de superklasse.
super()
}
// Wordt uitgevoerd nadat het element gekoppeld is.
// Moet de UI opbouwen.
connectedCallback() {
const helloWorld = document.createElement('h1')
helloWorld.innerText = 'Hello world!'
// This verwijst hier naar een HTMLElement (want daarvan erven we over)
this.appendChild(helloWorld)
}
// Wordt uitgevoerd als een attribuut van het element verandert.
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
}
// Registreer een nieuw HTML element <hello-world></hello-world> bij de browser.
// De eerste parameter is de naam van het element, de tweede de constructor.
// De naam van het element moet een koppelteken bevatten.
customElements.define('hello-world', HelloWorld);
// Element kan opgeroepen worden als: <hello-world foo="someValue"></hello-world>CustomElement klasse
We maken opnieuw een abstracte klasse aan waarin we de algemene logica voor elke component afzonderen. Net zoals bij de Page klasse geven we ook voor de CustomElement klasse de HTML-inhoud van de component door via de constructor.
De methode connectedCallback wordt automatisch opgeroepen als de component gedetecteerd wordt in de DOM. Dit is dan ook de functie waar de UI uitgebouwd moet worden. Merk op dat we this.innerHTML en this.appendChild gebruiken, omdat een custom element overerft van de HTMLElement klasse, kunnen we alle methodes en attributen gebruiken die we in hoofdstuk 4 besproken hebben.
Tips
Je krijgt de CustomElement klasse op het examen en moet deze enkel gebruiken als superklasse van één of meer concrete subklassen.
export abstract class CustomElement extends HTMLElement {
protected readonly componentBody: HTMLDivElement
protected constructor(body: string) {
super()
this.componentBody = document.createElement('div')
this.componentBody.innerHTML = body
}
connectedCallback() {
this.innerHTML = ''
this.appendChild(this.componentBody)
}
}Component renderen
Net zoals voor een pagina, voor we ook voor een custom element een concrete subklasse. Aangezien de navbar op elke pagina terugkomt, is dit een ideale kandidaat voor een custom element.
Elke nieuwe component moet geregistreerd worden in main.ts. We registreren alvast de series en author componenten die in de startbestanden zitten.
import {CustomElement} from '../../router/customElement.ts'
import HTML from './navbar.html?raw'
export class Navbar extends CustomElement {
constructor() {
super(HTML)
}
}import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
import {HomePage} from './pages/home/home.ts'
import {Navbar} from './components/navbar/navbar.ts'
import {Series} from './components/series/series.ts'
import {Author} from './components/author/author.ts'
customElements.define('custom-navbar', Navbar)
customElements.define('custom-series', Series)
customElements.define('custom-author', Author)
const homePage = new HomePage()
homePage.render()<div class="min-vh-100 d-flex flex-column">
<custom-navbar></custom-navbar>
<header class="bg-primary text-white text-center py-5 flex-grow-1">
<div class="container">
<h1>Manage Your Personal Library with Ease</h1>
<p class="lead">Track, organize, and explore your book collection like never before.</p>
</div>
</header>
<!-- Rest van de code weglaten, zie startbestanden. -->
</div>Router
De startbestanden bevatten al markdown en concrete subklassen voor pagina's waarmee auteurs, series en boeken bewerkt kunnen worden. Om te wisselen tussen deze pagina's zouden we het Navbar custom element kunnen uitbreiden met events voor elke link in de navbar, maar dan werken links die elders op de pagina staan nog niet.
Om het routing process efficiënt te laten verlopen, schrijven we een nieuwe klasse die een pathname koppelt aan een pagina. Het pad / wordt dus gekoppeld aan HomePage klasse terwijl /authors gekoppeld wordt aan de AuthorsPage klasse. Als de gebruiker op een link klikt, handelt de klasse alles af, van het renderen van de nieuwe pagina tot het koppelen van links.
Via de constructor van de nieuwe klasse geven we een object mee dat padnamen mapt naar de constructor van de klasse die deze pagina implementeert. Verder controleren we ook of de URL die de gebruiker ingegeven heeft in de adresbalk voorkomt in dit object, indien ja, navigeren we naar de bijhorende pagina, anders naar de root pagina.
Om te navigeren moeten we, zoals we daarstraks met de homepagina gedaan hebben, een instantie aanmaken van bijhorende klasse en vervolgens de render-methode oproepen.
Tenslotte voegen we een methode toe die naar links zoekt in de gerenderde pagina en hier een click handler aan toevoegt die de navigatie afhandelt. Om links te identificeren gebruiken we het data-link attribuut. Zo kunnen links toegevoegd worden op alle elementen, ook diegenen die geen a tag zijn. In de event handler gebruiken we preventDefault zodat de pagina niet herlaadt als het data-link attribuut gebruikt wordt in combinatie met een a tag. Verder gebruiken we ook window.history.pushState om de URL in de adresbalk te tonen.
Tips
Je krijgt de Router klasse op het examen en moet deze enkel gebruiken om te navigeren tussen pagina's.
import {Page} from './page.ts'
// Een type dat alle concrete subklassen van de Page klasse voorstelt.
type ConcretePage = new () => Page
// Map een padnaam naar een constructor van een subklasse van Page.
type RouteMap = Record<string, ConcretePage>
export class Router {
readonly #pages: RouteMap
constructor(pages: RouteMap) {
this.#pages = pages
const pathName = window.location.pathname
this.navigate(this.#pages[pathName] ? pathName : '/')
}
navigate(path: string) {
const page = new this.#pages[path]()
page.render()
this.#setupRouter()
}
#setupRouter() {
document.querySelectorAll('[data-link]')?.forEach(link => {
const path = link.getAttribute('data-link')!
link.addEventListener('click', (evt) => {
evt.preventDefault()
window.history.pushState(null, '', `${window.location.origin}${path}`)
this.navigate(path)
})
})
}
}De router kan nu gebruikt worden in main.ts. De HTML-code van de navbar moet natuurlijk ook uitgebreid worden met data-link attributen.
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
import {HomePage} from './pages/home/home.ts'
import {Navbar} from './components/navbar/navbar.ts'
import {Series} from './components/series/series.ts'
import {Author} from './components/author/author.ts'
import {AuthorsPage} from './pages/authors/authors.ts'
import {SeriesPage} from './pages/series/series.ts'
import {Router} from './router/router.ts'
import {LibraryPage} from './pages/library/library.ts'
customElements.define('custom-navbar', Navbar)
customElements.define('custom-series', Series)
customElements.define('custom-author', Author)
new Router({
'/': HomePage,
'/library': LibraryPage,
'/authors': AuthorsPage,
'/series': SeriesPage,
})<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" data-link="/" href="/">My Library</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link" data-link="/library" href="/library">My books</a></li>
<li class="nav-item"><a class="nav-link" data-link="/authors" href="/authors">Authors</a></li>
<li class="nav-item"><a class="nav-link" data-link="/series" href="/series">Series</a></li>
</ul>
</div>
</div>
</nav>Data delen tussen pagina's
De applicatie bevat drie pagina's. Twee van de pagina's worden gebruikt om de series en auteurs te beheren, de derde pagina bevat de boeken in de bibliotheek. Op de derde pagina moeten de series en auteurs uitgelezen worden zodat er een dropdownmenu mee gebouwd kan worden.
Deze structuur betekent dat dat de data niet enkel op de pagina zelf mag bewaard worden (als onderdeel van de klasse). De data moet centraal bewaard worden in een object dat op de andere pagina's geïmporteerd kan worden. Voorlopig bewaren we de data in-memory, volgende les wordt dit uitgebreid met persistentie via localstorage, indexedDB en een rest API.
De startbestanden bevatten enkele series, boeken en auteurs die gebruikt kunnen worden om de rest van de les te testen.
import {Book} from '../models/book.ts'
import {Author} from '../models/author.ts'
import {Series} from '../models/series.ts'
interface DataManager {
books: Book[]
authors: Author[]
series: Series[]
}
export const dataManager: DataManager = {
books: [ ... ],
authors: [ ... ],
series: [ ... ],
}Dynamische UI
De abstracte klasse Page bevat een methode render die de UI van de applicatie opbouwt. Als een subklasse dynamische elementen moet renderen, moet render overschreven worden. Natuurlijk moet de methode in de superklasse nog wel opgeroepen worden, zo wordt eerste de UI gerenderd die gedefinieerd is via de constructor van de pagina en daarna eventuele toevoegingen.
Het is belangrijk dat de containers met dynamische data leeggemaakt worden voordat ze opnieuw opgevuld worden. Aangezien de data dynamisch is, betekent dit dat er aanpassingen kunnen gebeuren en dat de UI opnieuw gerenderd moet worden. Als de containers niet leeggemaakt worden, worden de nieuwe elementen toegevoegd aan de bestaande elementen en krijg je dubbele data.
export class LibraryPage extends Page {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
readonly #typeSelect = this.body.querySelector<HTMLInputElement>('select[name="type"]')!
readonly #seriesSelect = this.body.querySelector<HTMLSelectElement>('select[name="series"]')!
constructor() {
super(HTML)
}
render() {
super.render()
// Boeken tabel opvullen.
this.#bookList.innerHTML = ''
dataManager.books.map(b => this.#bookList.appendChild(this.#buildBookRow(b)))
// Series dropdown opvullen.
this.#seriesSelect.innerHTML = ''
this.#seriesSelect.appendChild(this.#createSelectOption('', '--Series--'))
dataManager.series.forEach(s => this.#seriesSelect.appendChild(this.#createSelectOption(s.id, s.name)))
// Author dropdown opvullen.
this.#authorSelect.innerHTML = ''
this.#authorSelect.appendChild(this.#createSelectOption('', '--Author--'))
dataManager.authors.forEach(a => this.#authorSelect.appendChild(this.#createSelectOption(a.id, a.name)))
}
#buildBookRow(book: Book): HTMLTableRowElement {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
#createTableCell(content: string | HTMLElement): HTMLTableCellElement {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
#createSelectOption(value: string, label:string): HTMLOptionElement {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
#createBook(): void {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
}
Events
De startbestanden bevatten al verschillende methodes in concrete subklasse voor de bibliotheek pagina, maar deze methodes moeten echter nog gekoppeld worden aan de submit knop van het formulier.
De constructor is hier de ideale plaats voor omdat deze gegarandeerd één keer per instantie uitgevoerd wordt, dit betekent dat we elk event ook maar één keer registreren bij de DOM en dus bugs vermijden. Het is belangrijk dat de render methode opnieuw opgeroepen wordt in de event handler, doen we dit niet, dan zien we de nieuwe boeken niet.
Merk op dat prentDefault opgeroepen wordt, dit is cruciaal. Doen we dit niet, dan wordt het formulier verstuurd naar de (onbestaande) server en wordt de pagina herladen, dit is natuurlijk niet de bedoeling
Via de reset methode van een formulier-element, kan het formulier in één lijn code leeg gemaakt worden.
export class LibraryPage extends Page {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
readonly #typeSelect = this.body.querySelector<HTMLInputElement>('select[name="type"]')!
readonly #seriesSelect = this.body.querySelector<HTMLSelectElement>('select[name="series"]')!
readonly #bookForm = this.body.querySelector<HTMLFormElement>('#bookForm')!
constructor() {
super(HTML)
this.#bookForm.addEventListener('submit', evt => {
evt.preventDefault()
this.#createBook()
this.#bookForm.reset()
this.render()
})
}
render() {
super.render()
this.#bookList.innerHTML = ''
dataManager.books.map(b => this.#bookList.appendChild(this.#buildBookRow(b)))
this.#seriesSelect.innerHTML = ''
this.#seriesSelect.appendChild(this.#createSelectOption('', '--Series--'))
dataManager.series.forEach(s => this.#seriesSelect.appendChild(this.#createSelectOption(s.id, s.name)))
this.#authorSelect.innerHTML = ''
this.#authorSelect.appendChild(this.#createSelectOption('', '--Author--'))
dataManager.authors.forEach(a => this.#authorSelect.appendChild(this.#createSelectOption(a.id, a.name)))
}
#buildBookRow(book: Book): HTMLTableRowElement {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
#createTableCell(content: string | HTMLElement): HTMLTableCellElement {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
#createSelectOption(value: string, label:string): HTMLOptionElement {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
#createBook(): void {
// Truncated, zie startbestanden/uitgewerkt voorbeeld.
}
}Custom elements met attributen
Bovenstaande code werkt, maar de buildBookRow, createTableCell en createSelectOption methodes zijn nogal omslachtig. Een UI opbouwen in JavaScript vraagt veel code, zelfs voor eenvoudige dingen. Custom elements, zoals de <custom-navbar> die we eerder gedefinieerd hebben, kunnen ook via JavaScript aangemaakt worden en kunnen bijgevolg gebruikt worden om eenvoudig kleine stukjes van de UI te bouwen.
De startbestanden bevatten al een custom element om informatie over een auteur weer te geven. Aangezien een custom element een klasse is, kunnen we een author component aanmaken via de constructor van de klasse.
import {Author} from '../../components/author/author.ts'
export class AuthorsPage extends Page {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
constructor() {
super(HTML)
this.#authorForm.addEventListener('submit', (evt) => {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
})
}
render() {
super.render()
this.#authorList.innerHTML = ''
dataManager.authors.forEach(a => {
const author = new Author()
this.#authorList.appendChild(author)
})
}
#createAuthor(): void {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
}
}export class Author extends CustomElement {
readonly #name = this.componentBody.querySelector<HTMLDivElement>('.card-title')!
readonly #description = this.componentBody.querySelector<HTMLDivElement>('.card-text')!
readonly #image = this.componentBody.querySelector<HTMLImageElement>("#authorImg")!
readonly #imageBackground = this.componentBody.querySelector<HTMLImageElement>('#authorImgBackground')!
constructor() {
super(HTML)
}
}Na deze aanpassing ziet de auteurspagina er als volgt uit:

Het is duidelijk dat dit nog niet ideaal is. Er worden wel drie cards getoond voor de drie auteurs in de database, maar de informatie is niet correct.
Dit is logisch, de componenten worden aangemaakt, maar er wordt geen informatie doorgegeven aan de componenten. We kunnen geen gebruik maken van de constructor van de klasse om de informatie door te geven, want de constructor wordt ook gebruikt door de browser als een custom element gedetecteerd wordt in de DOM en de browser weet natuurlijk niet welke informatie doorgegeven moet worden.
Om data door te geven aan een custom element moet een attribuut toegevoegd worden aan het custom element. Attributen worden gedefinieerd in de static property observedAttributes. Via de methode attributeChangedCallback kunnen we de waarde van een attribuut aanpassen op het moment dat deze ingesteld of aangepast wordt.
export class Author extends CustomElement {
static observedAttributes = ["name", "image", "description"]
readonly #name = this.componentBody.querySelector<HTMLDivElement>('.card-title')!
readonly #description = this.componentBody.querySelector<HTMLDivElement>('.card-text')!
readonly #image = this.componentBody.querySelector<HTMLImageElement>("#authorImg")!
readonly #imageBackground = this.componentBody.querySelector<HTMLImageElement>('#authorImgBackground')!
constructor() {
super(HTML)
}
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
switch (name) {
case 'name':
this.#name.innerText = newValue
break
case 'image':
this.#image.src = newValue
this.#imageBackground.src = newValue
break
case 'description':
this.#description.innerText = newValue
break
}
}
}export class AuthorsPage extends Page {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
constructor() {
super(HTML)
this.#authorForm.addEventListener('submit', (evt) => {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
})
}
render() {
super.render()
this.#authorList.innerHTML = ''
dataManager.authors.forEach(a => {
const author = new AuthorComponent()
author.setAttribute('name', a.name)
author.setAttribute('image', a.profile)
author.setAttribute('description', a.description)
this.#authorList.appendChild(author)
})
}
#createAuthor(): void {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
}
}
Custom elements met events
Er is nog één probleem met de custom elements, alhoewel we data kunnen doorsturen naar de componenten, kunnen deze nog niets terugsturen naar de pagina's waarop ze geplaatst zijn.
De series component bevat een knop waarmee de series verwijderd moet kunnen worden. Het is niet mogelijk om de functionaliteit van deze knop te implementeren in het custom element zelf, nadat de serie verwijderd is, moet deze namelijk verdwijnen van de pagina en hiervoor moet de render functie van de SeriesPage klasse aangesproken worden. Aangezien deze niet statisch is, kan dit enkel in de klasse zelf, of als we een variabele hebben die een referentie naar de klasse bevat. We zullen dit dus in de SeriesPage klasse zelf moeten doen.
Via de CustomEvent klasse kunnen we een eigen event aanmaken dat uitgestuurd wordt als er op de delete knop gedrukt wordt. Dit event kan dan opgevangen worden in de SeriesPage klasse en daar verder afgehandeld worden.
export class Series extends CustomElement {
static observedAttributes = ["name"]
readonly #name = this.componentBody.querySelector<HTMLDivElement>('#seriesTitle')!
readonly #deleteButton = this.componentBody.querySelector<HTMLButtonElement>('#deleteBtn')!
constructor() {
super(HTML)
this.#deleteButton.addEventListener('click', (evt) => {
evt.preventDefault()
const event = new CustomEvent('seriesDeleted')
this.dispatchEvent(event)
})
}
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
switch (name) {
case 'name':
this.#name.innerText = newValue
break
}
}
}export class SeriesPage extends Page {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
constructor() {
super(HTML)
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
}
render() {
super.render()
this.#seriesList.innerText = ''
dataManager.series.forEach(s => {
const series = new Series()
series.setAttribute('name', s.name)
series.addEventListener('seriesDeleted', () => {
dataManager.series = dataManager.series.filter(s2 => s.id !== s2.id)
this.render()
})
this.#seriesList.appendChild(series)
})
}
#createSeries(): void {
// Truncated, zie startbestanden of het uitgewerkte lesvoorbeeld.
}
}import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
import {HomePage} from './pages/home/home.ts'
import {Navbar} from './components/navbar/navbar.ts'
import {AuthorsPage} from './pages/authors/authors.ts'
import {SeriesPage} from './pages/series/series.ts'
import {Router} from './router/router.ts'
import {LibraryPage} from './pages/library/library.ts'
import {Author} from './components/author/author.ts'
import {Series} from './components/series/series.ts'
customElements.define('custom-navbar', Navbar)
customElements.define('custom-author', Author)
customElements.define('custom-series', Series)
new Router({
'/': HomePage,
'/library': LibraryPage,
'/authors': AuthorsPage,
'/series': SeriesPage,
})