The three wizards paired a dispatch with a hand-written effect call (onPrimary/onRetry + runIfSubmitting/runIfIndienen). A missed call failed silently. RD-05 added the effect map and RD-07 added the Primary message; this ticket moves each wizard onto both. Each wizard now registers its submit effect on createStore, keyed by its own submitting tag (Submitting for herregistratie and intake, Indienen for registratie — the type catches a wrong key at compile time). The optimistic begin/confirm/rollback calls stay inside the effect body, unchanged. The template dispatches Primary and Retry directly, matching how Back already worked. onPrimary, onRetry, and runIfSubmitting/runIfIndienen are deleted from all three components. herregistratie-wizard drops under the 250-rule-line budget, so its eslint-disable max-lines header is removed in this same commit (RD-02's self-cleaning mechanism). intake-wizard and registratie-wizard stay over budget and keep theirs, both already flagged for RD-22/RD-23. Three doc comments (in the three machine files, plus one in store.ts) named the deleted onPrimary()/runIfSubmitting() identifiers in prose. Reworded them so the "idiom is gone from the repo" grep check is not defeated by its own explanatory comments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.9 KiB
RD-08 — Migrate the 3 wizards to the effect map and Primary
Status: done Source: PLAN.md 1a
Why
This is the ticket that removes the silent-failure idiom from the last three call sites. Each
wizard still pairs a dispatch with a hand-written effect call, and forgetting the second
line fails silently:
onPrimary() { … this.dispatch(…); } // decides Next vs Submit in the UI
onRetry() { this.dispatch({ tag: 'Retry' }); this.runIfSubmitting(); }
RD-05 added the effect map; RD-07 added Primary. After this ticket the wizard shell's five
outputs all map 1:1 onto messages in the template, and each component loses three methods.
Read first
libs/shared/src/application/store.ts— the effect map, its trigger rule, and theSeedexemption- The three machines' new
primaryexports (RD-07) apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts—onPrimary256,onRetry263,runIfSubmitting276-288apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts—onPrimary368,onRetry375,runIfSubmitting387-406apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts—onPrimary613,onRetry620,runIfIndienen635-644docs/project/readable-codebase/RD-06-…md— the same migration, already done for the two single-step forms. Copy its shape.
Decisions (pre-made, don't relitigate)
-
The effect-map key is the machine's own submitting tag, and it is not the same for all three:
Wizard Effect-map key herregistratie-wizardSubmittingintake-wizardSubmittingregistratie-wizardIndienenStoreEffectskeys are typed asModel['tag'], so writingSubmittingfor the registratie wizard is a compile error, not a silent no-op. That is the type doing its job — do not work around it by widening the type. -
Move each effect body verbatim into the map. The narrowed state arrives as argument one, so delete the
const s = this.state(); if (s.tag !== '…') return;preamble and use the parameter. Keep everything else identical, including the optimistic store calls. -
The optimistic
begin/confirm/rollbackcalls stay inside the effect body, exactly where they are today (herregistratie-wizard:279,283,286;intake-wizard:390,401,404). The effect slot is the sanctioned place for side effects, soreducestays pure. The registratie wizard has no optimistic calls — do not add any. -
Delete
onPrimaryandonRetryentirely and dispatch from the template, matching how(back)already does it:(primary)="dispatch({ tag: 'Primary' })" (retry)="dispatch({ tag: 'Retry' })" (back)="dispatch({ tag: 'Back' })" <-- already this shape todayRetryneeds no effect call becauseFailed → Submitting/Indienenis a tag transition, so the map fires it. This is the 1:1 output-to-message mapping.claude/skills/form-machine/SKILL.md:74-78already claims. -
Leave the two
dispatchcalls that sit inside Angulareffect()s alone —intake-wizard'sSetPolicyandregistratie-wizard'sPrefillAdres. Both land on an unchanged tag, so no effect fires, and both are alreadyuntracked. Do not key an effect on an editing tag (Editing/Answering/Invullen) — that is the livelockstore.spec.ts:18-31guards against. -
Preserve the WP-69 comment in
intake-wizard's effect body verbatim, including its ticket reference. It explains why the scholing answer rides along for server-side re-validation. RD-19 strips ticket prefixes across the repo later; do not do it early and do not drop the explanation. -
If a wizard drops below 250 rule-lines, delete its
eslint-disable max-linesheader in this same commit. See Risks — this is expected forherregistratie-wizardand is RD-02's mechanism working, not a problem to route around.
Files
apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.tsapps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.tsapps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts
No machine changes. No story changes expected. No xlf changes.
Steps
- For each wizard: register the effect body on
createStoreunder the key from decision 1, dropping the guard preamble. - Delete
runIfSubmitting/runIfIndienen. - Delete
onPrimaryandonRetry; dispatchPrimaryandRetryfrom the template (decision 4). - Run
npm run lint. If a file is now under budget, delete itseslint-disable max-linesheader (decision 7). - Update this ticket's
Status:todoneand the README's RD-08 row todone. - Commit all of it together.
Acceptance criteria
The idiom is gone from the whole repo. Anchor on the declaration, not on any occurrence of the name:
grep -rn "runIfSubmitting\|runIfIndienen" apps libs # MUST return nothing
grep -rnE "^ (onPrimary|onRetry)\(\)" apps libs # MUST return nothing
The second pattern is anchored deliberately. A bare grep -rn "onPrimary\|onRetry" cannot
pass: libs/shared/src/application/upload-controller.ts:94 has an unrelated
onRetry(localId) method, called from two wizard templates as
uploadCtl.onRetry($event). Those three matches are correct code and must stay.
Behaviour is unchanged. Walk each wizard end to end with npm start:
- The primary button advances through every step and submits on the last one.
- A failed submit shows the error, and Retry re-submits (it must leave
Failed). - Clicking the primary button twice quickly submits once — the trigger rule's tag-transition condition gives this for free.
- For the two herregistratie wizards, the dashboard's "herregistratie in behandeling" notice still appears after submit and disappears after a rollback.
npm run ci # exits 0
npm run ci --full # exits 0 — the wizards all have stories
Verification
npm run ci --full. --full is required: all three wizards have stories, and
registratie-wizard.stories.ts:88, intake-wizard.stories.ts:38 and
herregistratie-wizard.stories.ts:70 each seed a submitting state.
Out of scope
- Splitting any wizard into step components. RD-22 and RD-23, after RD-20.
WizardStatus→WizardPhase. RD-10.- Stripping the WP-69 ticket reference (decision 6). RD-19.
- Touching
shellStatus,errorList, or any other computed. Only the three methods and the template bindings change here.
Risks
herregistratie-wizardis 252 rule-lines and will likely drop below 250. Deleting three methods (~21 lines) and adding a map registration (~12) nets roughly −9. Itseslint-disable max-linesthen becomes unnecessary andreportUnusedDisableDirectives: 'error'fails the build. That is RD-02's self-cleaning mechanism doing its job: delete the header (decision 7). RD-20 also expects to remove it — whichever ticket gets there first removes it, and the other finds nothing to do.- The Storybook trap. All three wizards seed a submitting state in their stories with a
real
provideHttpClient()and no request mocking. RD-05'sSeedexemption is what stops them firing real network calls. If a story flips to a failed state on load, orstorybook-a11ygoes red, the exemption is not working — fix that, do not delete the story. - Double-submit protection is now structural, not incidental. The old code could submit
twice if
dispatchand the effect call were paired twice. The tag-transition rule prevents it. Do not add abusyguard on top; that would be a second mechanism for one rule. behaviour-spec.mdxdrift if you add or rename a spec. Runnpm run gen:behaviour-specin the same commit if so.