Frontend design
De lib/ directory bevat de core business logic en utilities van de VEMAP frontend applicatie. Het is gestructureerd volgens een modulaire architectuur met strikte naleving van software design principes.
Directory Structuur
Section titled “Directory Structuur”Voor de volledige file tree, klik hier.
Directorylib/
- app.factory.js Alpine.js app initialization
Directoryalpine/ Alpine.js data components
Directoryauth/ Authenticatie forms
- …
Directorycharts/ Chart data components
Directoryadmin/ Admin dashboards
- …
Directorymanager/ Manager dashboards
- …
Directoryorganisation/ Organisatie dashboards
- …
Directoryshared/ Herbruikbare chart types
- …
Directoryforms/ Form handling components
- …
Directorymail/ Email editor
- …
Directorystats/ Statistics components
Directoryadmin/
- …
Directorymanager/
- …
Directoryorganisation/
- …
Directoryviews/ View data components
- …
Directoryconfig/ Applicatie configuratie
- …
Directorymiddleware/ Astro middleware
- …
Directoryparser/ Data parsers
- …
Directoryservices/ Business logic services
- …
Directorystores/ Alpine.js reactive stores
- …
Directoryutils/ Utility functies
Directoryhttp/ HTTP client en headers
- …
Directorycharts/ Chart utilities
- …
Architectuur Principes
Section titled “Architectuur Principes”De lib architectuur is gebouwd op drie fundamentele software design principes:
KISS (Keep It Simple, Stupid)
Section titled “KISS (Keep It Simple, Stupid)”Elke module heeft één duidelijk doel en is zo eenvoudig mogelijk gehouden:
- Utility functies zijn standalone en onafhankelijk
- Services encapsuleren complexe business logic in simpele interfaces
- Stores bieden simpele state management zonder boilerplate
Voorbeeld:
// Utils zijn simpel en doen één ding goedexport const validateEmail = (email) => { return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);};
// Services bieden simpele interfaces voor complexe operatiesexport const userLogOut = async () => { await post(createFullUrl("authentication/logout"), authHeaders()); deleteCookie("user_session"); deleteRefreshToken(); window.location.href = "/login";};DRY (Don’t Repeat Yourself)
Section titled “DRY (Don’t Repeat Yourself)”Herbruikbare logica is gecentreerd in utility functies en services:
- Utils voorkomen code duplicatie voor validatie, formatting, HTTP calls
- Stores centraliseren state management
- Services delen API communicatie logica
- Config centraliseert alle configuratie
Voorbeeld:
// Zonder DRY: Error handling overal verschillendtry { const response = await fetch(url); if (!response.ok) alert("Error!");} catch (e) { console.error(e);}
// Met DRY: Gecentreerde error handlingimport { handleHttpError } from "./utils/errors";const result = handleHttpError(response, "create", { notificationStore: Alpine.store("notificationStore"),});SoC (Separation of Concerns)
Section titled “SoC (Separation of Concerns)”Elke module categorie heeft een specifieke verantwoordelijkheid:
| Categorie | Verantwoordelijkheid | Voorbeeld |
|---|---|---|
| Services | Business logic & API communicatie | auth.service.js |
| Utils | Herbruikbare utility functies | validation.js, dates.js |
| Stores | State management (Alpine.js) | user.store.js |
| Alpine | UI logic & form handling | login.data.js |
| Config | Configuratie & environment settings | app.js, environments.js |
| Middleware | Request interceptie & auth checks | auth.middleware.js |
| Parser | Data parsing & formatting | papaparse.init.js |
Data Flow (SoC in actie):
Alpine Component → Service → HTTP Client → API ↓ ↓ ↓ UI Logic Business Logic TransportDependency Injection
Section titled “Dependency Injection”Stores en services gebruiken dependency injection voor flexibiliteit en testbaarheid. Deze injectie is noodzakelijk als gevolg van Astro.js middleware met SSR, waarbij Alpine.js geïnitialiseerd moet worden voordat de pagina volledig gebouwd is.
Astro.js configuratie
Section titled “Astro.js configuratie”De dependency injection wordt geconfigureerd via astro.config.mjs:
import { defineConfig } from "astro/config";import alpinejs from "@astrojs/alpinejs";
export default defineConfig({ output: "server", adapter: netlify({ edgeMiddleware: true, }), integrations: [ alpinejs({ entrypoint: "./src/lib/app.factory.js", }), react(), ],});Entry Point Factory
Section titled “Entry Point Factory”De app.factory.js fungeert als entry point voor Alpine.js initialisatie:
// app.factory.js registreert alle stores met dependenciesAlpine.store("userStore", userStore(Alpine.reactive.persist));Alpine.store("slideoverStore", slideoverStore(Alpine.effect));Alpine.store("taskStore", taskStore(Alpine.reactive.persist, Alpine.nextTick));SSR Workflow
Section titled “SSR Workflow”- Server-side rendering: Astro.js bouwt de pagina op de server
- Middleware execution: Alpine.js middleware wordt uitgevoerd
- Dependency injection: Stores worden geïnjecteerd met Alpine.js dependencies
- Client hydration: Alpine.js wordt geactiveerd op de client
- Reactive updates: Stores zijn klaar voor interactiviteit
Dit zorgt ervoor dat Alpine.js correct functioneert in de SSR omgeving zonder hydration mismatches.
Module Categorieën
Section titled “Module Categorieën”Core Services
Section titled “Core Services”| Module | Beschrijving | Documentatie |
|---|---|---|
auth.service | AWS Cognito authenticatie | Auth Service |
fileUpload.service | S3 file uploads | File Upload Service |
Utilities
Section titled “Utilities”| Module | Beschrijving | Documentatie |
|---|---|---|
validation | Input validatie | Validation Utils |
cookies | Cookie management | Cookie Utils |
tokens | JWT utilities | Token Utils |
dates | Datum utilities | Date Utils |
strings | String utilities | String Utils |
formatters | Data formatters | Formatters |
functions | Algemene utilities | Function Utils |
errors | Error handling | Error Handling |
session | Session utilities | Session Utils |
http/client | HTTP requests | HTTP Client |
http/headers | HTTP headers | HTTP Headers |
auth | Auth utilities | Auth Utils |
constants | App constants | Constants |
files | File utilities | File Utils |
Stores
Section titled “Stores”| Module | Beschrijving | Documentatie |
|---|---|---|
user.store | Gebruikersbeheer | User Store |
project.store | Projectbeheer | Project Store |
closure.store | Afsluitingenbeheer | Closure Store |
notification.store | Toast notifications | Notification Store |
slideover.store | Slideover forms | Slideover Store |
formModal.store | Form modals | Form Modal Store |
tenant.store | Tenant beheer | Tenant Store |
files.store | Bestanden beheer | Files Store |
task.store | Stakeholder taken | Task Store |
event.store | Event timeline | Event Store |
metrics.store | Applicatie metrics | Metrics Store |
Best Practices
Section titled “Best Practices”Deze best practices illustreren hoe KISS, DRY en SoC in de praktijk worden toegepast.
KISS: Houd Het Simpel
Section titled “KISS: Houd Het Simpel”Services voor API Calls - Verberg complexiteit achter simpele interfaces:
Direct HTTP calls met veel boilerplate:
const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${getCookie("token")}`, "x-user-id": user.id, }, body: JSON.stringify(data),});if (!response.ok) { // Error handling...}Via service layer (simpel en duidelijk):
import { createUser } from "../services/user.service";const user = await createUser(userData);DRY: Herhaling Vermijden
Section titled “DRY: Herhaling Vermijden”Utils voor Herbruikbare Logic - Schrijf logica één keer, gebruik het overal:
Dezelfde validatie overal herhalen:
// In component Aconst isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email);
// In component Bconst isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email);
// In component Cconst isValid = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(email);Via validation util (één implementatie):
import { validateEmail } from "../utils/validation";const isValid = validateEmail(email); // Overal hetzelfde gedragStores voor Shared State - Eén bron van waarheid:
Component-local state (duplicatie):
// Component Alet users = [];fetch("/api/users").then((r) => (users = r.data));
// Component Blet users = []; // Dezelfde data opnieuw ophalen!fetch("/api/users").then((r) => (users = r.data));Via store (gedeelde state):
// Eén keer ophalen, overal beschikbaarawait Alpine.store("userStore").getUsers();const users = Alpine.store("userStore").users;SoC: Verantwoordelijkheden Scheiden
Section titled “SoC: Verantwoordelijkheden Scheiden”Error Handling via Utils - Scheidt error handling van business logic:
Error handling mixed met business logic:
async function saveUser(data) { try { const response = await fetch("/api/users", { method: "POST", body: JSON.stringify(data), }); if (response.status === 400) { alert("Ongeldige data!"); } else if (response.status === 403) { alert("Geen toegang!"); } else if (response.status === 500) { alert("Server error!"); } // Business logic mixed met error handling } catch (e) { console.error(e); }}Via error handler (gescheiden concerns):
// Business logicasync function saveUser(data) { const response = await post(createFullUrl("users"), apiHeaders(), data);
if (!response || response.error) { // Error handling is gedelegeerd handleHttpError(response, "create", { notificationStore: Alpine.store("notificationStore"), customTitle: "Gebruiker Opslaan Mislukt", }); return; }
// Alleen business logic hier Alpine.store("userStore").addUsers([response.data]);}Dependency Injection
Section titled “Dependency Injection”Flexibiliteit & Testbaarheid - Inject dependencies in plaats van hard-coding:
Hard-coded dependencies:
export const myStore = { data: [], save() { sessionStorage.setItem("data", JSON.stringify(this.data)); },};Met dependency injection:
export default function myStore(persist) { return { data: persist([]).using(sessionStorage), // Nu testbaar met mock storage };}Type Safety via Constants
Section titled “Type Safety via Constants”Voorkom typos - Gebruik constants in plaats van magic strings:
Magic strings (foutgevoelig):
if (response.status === 404) { // Typo risico}if (user.role === "ROLE_ADMIN") { // Typo risico}Via constants (type-safe):
import { STATUS_CODES, ROLES } from "../utils/constants";
if (response.status === STATUS_CODES.NOT_FOUND) { // Type-safe}if (user.role === ROLES.ADMIN) { // Type-safe}