diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 40cfdda..975cb88 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -11,6 +11,7 @@ export const routes: Routes = [ { path: 'dashboard', loadComponent: () => import('./pages/dashboard/dashboard.page').then(m => m.DashboardPage) }, { path: 'registratie', loadComponent: () => import('./pages/registration-detail/registration-detail.page').then(m => m.RegistrationDetailPage) }, { path: 'herregistratie', loadComponent: () => import('./pages/herregistratie/herregistratie.page').then(m => m.HerregistratiePage) }, + { path: 'concepts', loadComponent: () => import('./pages/concepts/concepts.page').then(m => m.ConceptsPage) }, { path: '**', redirectTo: 'login' }, ], }, diff --git a/src/app/pages/concepts/concepts.page.ts b/src/app/pages/concepts/concepts.page.ts new file mode 100644 index 0000000..d4faecf --- /dev/null +++ b/src/app/pages/concepts/concepts.page.ts @@ -0,0 +1,147 @@ +import { Component, computed, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import type { Resource } from '@angular/core'; +import { PageShellComponent } from '../../templates/page-shell/page-shell.component'; +import { HeadingComponent } from '../../atoms/heading/heading.component'; +import { AlertComponent } from '../../atoms/alert/alert.component'; +import { TextInputComponent } from '../../atoms/text-input/text-input.component'; +import { ASYNC } from '../../molecules/async/async.component'; +import { SkeletonComponent } from '../../atoms/skeleton/skeleton.component'; +import { RegistrationSummaryComponent } from '../../organisms/registration-summary/registration-summary.component'; +import { HerregistratieWizardComponent } from '../../organisms/herregistratie-wizard/herregistratie-wizard.component'; +import { Registration } from '../../core/models'; +import { parsePostcode } from '../../core/parse'; + +/** Minimal fake Resource so can be driven through every state without HTTP. */ +function fakeResource(status: string, value?: T, error?: Error): Resource { + return { value: () => value as T, status: () => status, error: () => error, hasValue: () => value !== undefined, reload: () => {} } as unknown as Resource; +} + +/** Teaching showcase: each section pairs the impossible-state-permitting "before" + with the "after" where the type system rules it out. Composition-only. */ +@Component({ + selector: 'app-concepts-page', + imports: [ + FormsModule, PageShellComponent, HeadingComponent, AlertComponent, TextInputComponent, + ...ASYNC, SkeletonComponent, RegistrationSummaryComponent, HerregistratieWizardComponent, + ], + styles: [` + .cols { display:flex; flex-wrap:wrap; gap:1.5rem; margin:1rem 0 2.5rem } + .col { flex:1 1 20rem; min-width:18rem } + .tag { font-weight:700; font-size:0.8rem; text-transform:uppercase; letter-spacing:0.04em } + .bad { color:var(--rhc-color-rood-500) } + .good { color:var(--rhc-color-groen-500) } + pre { background:var(--rhc-color-grijs-100,#f3f3f3); padding:1rem; border-radius:4px; overflow:auto; font-size:0.85rem } + `], + template: ` + + + Vier functionele patronen die met atomic design makkelijker te tonen zijn — telkens "fout" + (de oude vorm liet het toe) naast "goed" (het type maakt het onmogelijk). + + + + 1 · Discriminated unions +
+
+

Fout — vlakke interface

+
{{ unionBad }}
+

Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand.

+
+
+

Goed — sum type

+ +

De variant Doorgehaald kent geen herregistratiedatum, dus de rij bestaat simpelweg niet.

+
+
+ + + 2 · RemoteData fold +
+
+

Eén molecuul, vier elkaar uitsluitende toestanden

+

Loading

+ {{ v }} +

Empty

+ {{ v }} +

Failure

+ {{ v }} +

Success

+
    @for (i of v; track i) {
  • {{ i }}
  • }
+
+
+

De exhaustieve fold

+
{{ foldCode }}
+

Een nieuwe variant toevoegen breekt de compile via assertNever tot je hem afhandelt.

+
+
+ + + 3 · Parse, don't validate +
+
+

Smart constructor → Result

+ +
+
+ @if (parsed().ok) { +

ok

+
Postcode = "{{ parsed().ok ? $any(parsed()).value : '' }}"
+

Een gevalideerde Postcode is een ander type dan een ruwe string.

+ } @else { +

err

+
{{ $any(parsed()).error }}
+ } +
+
+ + + 4 · Form als state machine +
+
+

Fout — losse booleans

+
{{ machineBad }}
+

Niets verhindert "submitting" mét validatiefouten of een successcherm met errors.

+
+
+

Goed — één tagged union stuurt de UI

+ +
+
+
+ `, +}) +export class ConceptsPage { + isEmpty = (v: string[]) => !v || v.length === 0; + + doorgehaald: Registration = { + bigNummer: '19012345601', naam: 'Dr. A. (Anna) de Vries', beroep: 'Arts', + registratiedatum: '2012-09-01', geboortedatum: '1985-03-14', + status: { tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'Op eigen verzoek' }, + }; + + loadingRes = fakeResource('loading'); + emptyRes = fakeResource('resolved', []); + errorRes = fakeResource('error', undefined, new Error('Demo')); + successRes = fakeResource('resolved', ['Huisartsgeneeskunde', 'Spoedeisende hulp']); + + raw = signal(''); + parsed = computed(() => parsePostcode(this.raw())); + + unionBad = `interface Registration { + status: 'Geregistreerd' | 'Doorgehaald'; + herregistratieDatum: string; // altijd aanwezig 😬 +}`; + + foldCode = `foldRemote(rd, { + loading: () => spinner, + empty: () => 'geen data', + failure: (e) => alert(e), + success: (v) => render(v), +}); // mist er één → compile-fout`; + + machineBad = `submitting = signal(false); +submitted = signal(false); +errors = signal<...>({}); +// submitting === true && errors.size > 0 ? 🤷`; +} diff --git a/src/app/pages/dashboard/dashboard.page.ts b/src/app/pages/dashboard/dashboard.page.ts index 03ee059..c07e801 100644 --- a/src/app/pages/dashboard/dashboard.page.ts +++ b/src/app/pages/dashboard/dashboard.page.ts @@ -48,6 +48,9 @@ import { Aantekening } from '../../core/models';

Herregistratie aanvragen →

+

+ Functionele patronen (impossible states) → +

`, })