Files
atomic-design-poc/src/app/registratie/ui/change-request-form/change-request-form.component.ts
Edwin van den Houdt 0d623f90e8 feat(fp): WP-08 — one store idiom + machine naming + TEA MDX
Rename change-request.machine.ts's bare State/Msg to ChangeRequestState/
ChangeRequestMsg (the last machine not context-prefixed), document the
createStore-is-the-idiom + naming convention in CLAUDE.md §3, and add the
Foundations/State Machines (TEA) curriculum page. The wizard pages already
wired createStore (confirmed by reading each and by git log) -- the WP's
"hand-wired signal(model)" premise was stale; recorded as a deviation.
2026-07-03 21:50:53 +02:00

118 lines
4.9 KiB
TypeScript

import { Component, computed, input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import {
AddressFieldsComponent,
AdresValue,
AdresErrors,
} from '@registratie/ui/address-fields/address-fields.component';
import { createStore } from '@shared/application/store';
import { whenTag } from '@shared/kernel/fp';
import { ChangeRequestState, ChangeRequestMsg, initial, reduce } from '@registratie/domain/change-request.machine';
import { createSubmitChangeRequest } from '@registratie/application/submit-change-request';
/**
* Organism: change-request (adreswijziging) form. Uses the SAME idiom as the
* wizards — all state in one signal driven by the pure `reduce`
* (change-request.machine.ts), submitted via a `submit-*` command returning
* `Result`. Renders the shared `<app-address-fields>`; the server re-validates.
*/
@Component({
selector: 'app-change-request-form',
imports: [FormsModule, ButtonComponent, HeadingComponent, AlertComponent, AddressFieldsComponent],
template: `
@if (state().tag === 'Submitted') {
<app-alert type="ok" i18n="@@changeRequest.success">
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5
werkdagen bericht.
</app-alert>
<div class="app-section">
<app-button
variant="secondary"
(click)="dispatch({ tag: 'Reset' })"
i18n="@@changeRequest.nieuwe"
>Nieuwe wijziging doorgeven</app-button
>
</div>
} @else {
<app-heading [level]="2" i18n="@@changeRequest.heading">Adreswijziging doorgeven</app-heading>
<form (ngSubmit)="onSubmit()" class="form-horizontal app-section">
<div class="form-header">
<div class="form-action">
<span class="meta" i18n="@@form.verplichteVelden">* verplichte velden</span>
</div>
</div>
<app-address-fields
idPrefix="cr"
[value]="adres()"
[errors]="errors()"
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })"
/>
@if (failedError()) {
<app-alert type="error"
><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container>
{{ failedError() }}</app-alert
>
}
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
{{ state().tag === 'Submitting' ? submitBezigLabel : submitLabel }}
</app-button>
</form>
}
`,
})
export class ChangeRequestFormComponent {
// The submit command owns the ApiClient dependency (via the change-request
// adapter); the UI holds only this bound command. Field initializer = injection
// context, like createStore below.
private submit = createSubmitChangeRequest();
private store = createStore<ChangeRequestState, ChangeRequestMsg>(initial, reduce);
/** Optional seed so Storybook / tests can mount any state directly. */
seed = input<ChangeRequestState>(initial);
readonly state = this.store.model;
protected dispatch = this.store.dispatch;
protected readonly submitLabel = $localize`:@@changeRequest.submit:Wijziging indienen`;
protected readonly submitBezigLabel = $localize`:@@changeRequest.submitBezig:Bezig met indienen…`;
private editing = computed(() => whenTag(this.state(), 'Editing'));
protected errors = computed<AdresErrors>(() => this.editing()?.errors ?? {});
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
protected referentie = computed(() => whenTag(this.state(), 'Submitted')?.referentie ?? '');
/** The address shown in the fields — the live draft while editing, the parsed
data while submitting/failed (so the user sees what they sent). */
protected adres = computed<AdresValue>(() => {
const s = this.state();
if (s.tag === 'Editing') return s.draft;
if (s.tag === 'Submitting' || s.tag === 'Failed') {
return { straat: s.data.straat, postcode: s.data.postcode, woonplaats: s.data.woonplaats };
}
return { straat: '', postcode: '', woonplaats: '' }; // Submitted shows the success alert, not the fields
});
constructor() {
queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() }));
}
onSubmit() {
this.dispatch({ tag: 'Submit' });
this.runIfSubmitting();
}
/** Effect: when we entered Submitting, call the command, then dispatch the outcome. */
private async runIfSubmitting() {
const s = this.state();
if (s.tag !== 'Submitting') return;
const r = await this.submit(s.data);
if (r.ok) this.dispatch({ tag: 'SubmitConfirmed', referentie: r.value });
else this.dispatch({ tag: 'SubmitFailed', error: r.error });
}
}