Add /concepts showcase page
A teaching page pairing each pattern's impossible-state-permitting "before" with the "after" the type system enforces: discriminated unions, the RemoteData fold, parse-don't-validate (live), and the wizard state machine. Composition-only — no new atoms. Linked from the dashboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
147
src/app/pages/concepts/concepts.page.ts
Normal file
147
src/app/pages/concepts/concepts.page.ts
Normal file
@@ -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 <app-async> can be driven through every state without HTTP. */
|
||||
function fakeResource<T>(status: string, value?: T, error?: Error): Resource<T> {
|
||||
return { value: () => value as T, status: () => status, error: () => error, hasValue: () => value !== undefined, reload: () => {} } as unknown as Resource<T>;
|
||||
}
|
||||
|
||||
/** 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: `
|
||||
<app-page-shell heading="Onmogelijke toestanden onmogelijk maken" backLink="/dashboard">
|
||||
<app-alert type="info">
|
||||
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).
|
||||
</app-alert>
|
||||
|
||||
<!-- 1. Discriminated unions -->
|
||||
<app-heading [level]="2">1 · Discriminated unions</app-heading>
|
||||
<div class="cols">
|
||||
<div class="col">
|
||||
<p class="tag bad">Fout — vlakke interface</p>
|
||||
<pre>{{ unionBad }}</pre>
|
||||
<p class="rhc-paragraph">Een doorgehaalde registratie houdt tóch een herregistratiedatum: onmogelijke toestand.</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="tag good">Goed — sum type</p>
|
||||
<app-registration-summary [reg]="doorgehaald" />
|
||||
<p class="rhc-paragraph">De variant <code>Doorgehaald</code> kent geen herregistratiedatum, dus de rij bestaat simpelweg niet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. RemoteData fold -->
|
||||
<app-heading [level]="2">2 · RemoteData fold</app-heading>
|
||||
<div class="cols">
|
||||
<div class="col">
|
||||
<p class="tag good">Eén molecuul, vier elkaar uitsluitende toestanden</p>
|
||||
<p class="tag">Loading</p>
|
||||
<app-async [resource]="loadingRes"><ng-template appAsyncLoaded let-v>{{ v }}</ng-template><ng-template appAsyncLoading><app-skeleton [count]="2" height="1.2rem" [delay]="0" /></ng-template></app-async>
|
||||
<p class="tag">Empty</p>
|
||||
<app-async [resource]="emptyRes" [isEmpty]="isEmpty"><ng-template appAsyncLoaded let-v>{{ v }}</ng-template></app-async>
|
||||
<p class="tag">Failure</p>
|
||||
<app-async [resource]="errorRes"><ng-template appAsyncLoaded let-v>{{ v }}</ng-template></app-async>
|
||||
<p class="tag">Success</p>
|
||||
<app-async [resource]="successRes" [isEmpty]="isEmpty"><ng-template appAsyncLoaded let-v><ul class="rhc-unordered-list">@for (i of v; track i) {<li>{{ i }}</li>}</ul></ng-template></app-async>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="tag good">De exhaustieve fold</p>
|
||||
<pre>{{ foldCode }}</pre>
|
||||
<p class="rhc-paragraph">Een nieuwe variant toevoegen breekt de compile via <code>assertNever</code> tot je hem afhandelt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Parse, don't validate -->
|
||||
<app-heading [level]="2">3 · Parse, don't validate</app-heading>
|
||||
<div class="cols">
|
||||
<div class="col">
|
||||
<p class="tag good">Smart constructor → Result</p>
|
||||
<app-text-input inputId="pc" [ngModel]="raw()" (ngModelChange)="raw.set($event)" name="pc" placeholder="Typ een postcode, bijv. 1234 AB" />
|
||||
</div>
|
||||
<div class="col">
|
||||
@if (parsed().ok) {
|
||||
<p class="tag good">ok</p>
|
||||
<pre>Postcode = "{{ parsed().ok ? $any(parsed()).value : '' }}"</pre>
|
||||
<p class="rhc-paragraph">Een gevalideerde <code>Postcode</code> is een ander type dan een ruwe string.</p>
|
||||
} @else {
|
||||
<p class="tag bad">err</p>
|
||||
<pre>{{ $any(parsed()).error }}</pre>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. State machine / wizard -->
|
||||
<app-heading [level]="2">4 · Form als state machine</app-heading>
|
||||
<div class="cols">
|
||||
<div class="col">
|
||||
<p class="tag bad">Fout — losse booleans</p>
|
||||
<pre>{{ machineBad }}</pre>
|
||||
<p class="rhc-paragraph">Niets verhindert "submitting" mét validatiefouten of een successcherm met errors.</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="tag good">Goed — één tagged union stuurt de UI</p>
|
||||
<app-herregistratie-wizard />
|
||||
</div>
|
||||
</div>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
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<string[]>('loading');
|
||||
emptyRes = fakeResource<string[]>('resolved', []);
|
||||
errorRes = fakeResource<string[]>('error', undefined, new Error('Demo'));
|
||||
successRes = fakeResource<string[]>('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 ? 🤷`;
|
||||
}
|
||||
@@ -48,6 +48,9 @@ import { Aantekening } from '../../core/models';
|
||||
<p>
|
||||
<app-link to="/herregistratie">Herregistratie aanvragen →</app-link>
|
||||
</p>
|
||||
<p>
|
||||
<app-link to="/concepts">Functionele patronen (impossible states) →</app-link>
|
||||
</p>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user