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.
118 lines
4.9 KiB
TypeScript
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 });
|
|
}
|
|
}
|