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.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { State, reduce, initial } from './change-request.machine';
|
||||
import { ChangeRequestState, reduce, initial } from './change-request.machine';
|
||||
|
||||
const editingWith = (
|
||||
draft: Partial<{ straat: string; postcode: string; woonplaats: string }>,
|
||||
): State => ({
|
||||
): ChangeRequestState => ({
|
||||
tag: 'Editing',
|
||||
draft: { straat: '', postcode: '', woonplaats: '', ...draft },
|
||||
errors: {},
|
||||
@@ -13,13 +13,13 @@ describe('change-request reduce', () => {
|
||||
it('SetField updates the draft while editing', () => {
|
||||
const s = reduce(initial, { tag: 'SetField', key: 'straat', value: 'Lange Voorhout 9' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
expect((s as Extract<State, { tag: 'Editing' }>).draft.straat).toBe('Lange Voorhout 9');
|
||||
expect((s as Extract<ChangeRequestState, { tag: 'Editing' }>).draft.straat).toBe('Lange Voorhout 9');
|
||||
});
|
||||
|
||||
it('Submit with an invalid draft stays Editing and reports field errors', () => {
|
||||
const s = reduce(editingWith({ straat: '', postcode: 'nope' }), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
const errors = (s as Extract<State, { tag: 'Editing' }>).errors;
|
||||
const errors = (s as Extract<ChangeRequestState, { tag: 'Editing' }>).errors;
|
||||
expect(errors.straat).toBeTruthy();
|
||||
expect(errors.postcode).toBeTruthy();
|
||||
});
|
||||
@@ -29,7 +29,7 @@ describe('change-request reduce', () => {
|
||||
tag: 'Submit',
|
||||
});
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as Extract<State, { tag: 'Submitting' }>).data.postcode).toBe('2514 EA');
|
||||
expect((s as Extract<ChangeRequestState, { tag: 'Submitting' }>).data.postcode).toBe('2514 EA');
|
||||
});
|
||||
|
||||
it('confirms and fails only from Submitting; Retry re-submits a failure', () => {
|
||||
|
||||
@@ -23,13 +23,13 @@ export type Errors = Partial<Record<keyof Draft, string>>;
|
||||
* Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting
|
||||
* an invalid draft, a success screen with errors) are unrepresentable.
|
||||
*/
|
||||
export type State =
|
||||
export type ChangeRequestState =
|
||||
| { tag: 'Editing'; draft: Draft; errors: Errors }
|
||||
| { tag: 'Submitting'; data: Valid }
|
||||
| { tag: 'Submitted'; data: Valid; referentie: string }
|
||||
| { tag: 'Failed'; data: Valid; error: string };
|
||||
|
||||
export const initial: State = {
|
||||
export const initial: ChangeRequestState = {
|
||||
tag: 'Editing',
|
||||
draft: { straat: '', postcode: '', woonplaats: '' },
|
||||
errors: {},
|
||||
@@ -51,16 +51,16 @@ function validate(draft: Draft): Result<Errors, Valid> {
|
||||
return { ok: false, error: errors };
|
||||
}
|
||||
|
||||
export type Msg =
|
||||
export type ChangeRequestMsg =
|
||||
| { tag: 'SetField'; key: keyof Draft; value: string }
|
||||
| { tag: 'Submit' }
|
||||
| { tag: 'Retry' }
|
||||
| { tag: 'SubmitConfirmed'; referentie: string }
|
||||
| { tag: 'SubmitFailed'; error: string }
|
||||
| { tag: 'Reset' }
|
||||
| { tag: 'Seed'; state: State }; // mount a specific state (stories/tests)
|
||||
| { tag: 'Seed'; state: ChangeRequestState }; // mount a specific state (stories/tests)
|
||||
|
||||
export function reduce(s: State, m: Msg): State {
|
||||
export function reduce(s: ChangeRequestState, m: ChangeRequestMsg): ChangeRequestState {
|
||||
switch (m.tag) {
|
||||
case 'SetField':
|
||||
return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@registratie/ui/address-fields/address-fields.component';
|
||||
import { createStore } from '@shared/application/store';
|
||||
import { whenTag } from '@shared/kernel/fp';
|
||||
import { State, Msg, initial, reduce } from '@registratie/domain/change-request.machine';
|
||||
import { ChangeRequestState, ChangeRequestMsg, initial, reduce } from '@registratie/domain/change-request.machine';
|
||||
import { createSubmitChangeRequest } from '@registratie/application/submit-change-request';
|
||||
|
||||
/**
|
||||
@@ -70,10 +70,10 @@ export class ChangeRequestFormComponent {
|
||||
// adapter); the UI holds only this bound command. Field initializer = injection
|
||||
// context, like createStore below.
|
||||
private submit = createSubmitChangeRequest();
|
||||
private store = createStore<State, Msg>(initial, reduce);
|
||||
private store = createStore<ChangeRequestState, ChangeRequestMsg>(initial, reduce);
|
||||
|
||||
/** Optional seed so Storybook / tests can mount any state directly. */
|
||||
seed = input<State>(initial);
|
||||
seed = input<ChangeRequestState>(initial);
|
||||
|
||||
readonly state = this.store.model;
|
||||
protected dispatch = this.store.dispatch;
|
||||
|
||||
Reference in New Issue
Block a user