Files
atomic-design-poc/docs/project/readable-codebase/RD-08-wizards-to-effect-map.md
T
ehoandClaude Sonnet 5 b8aced75e3 refactor: migrate the 3 wizards to the effect map and Primary (RD-08)
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>
2026-09-04 17:20:52 +02:00

7.9 KiB
Raw Blame History

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 the Seed exemption
  • The three machines' new primary exports (RD-07)
  • apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.tsonPrimary 256, onRetry 263, runIfSubmitting 276-288
  • apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.tsonPrimary 368, onRetry 375, runIfSubmitting 387-406
  • apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.tsonPrimary 613, onRetry 620, runIfIndienen 635-644
  • docs/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)

  1. 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-wizard Submitting
    intake-wizard Submitting
    registratie-wizard Indienen

    StoreEffects keys are typed as Model['tag'], so writing Submitting for 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.

  2. 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.

  3. The optimistic begin/confirm/rollback calls 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, so reduce stays pure. The registratie wizard has no optimistic calls — do not add any.

  4. Delete onPrimary and onRetry entirely 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 today
    

    Retry needs no effect call because Failed → Submitting/Indienen is a tag transition, so the map fires it. This is the 1:1 output-to-message mapping .claude/skills/form-machine/SKILL.md:74-78 already claims.

  5. Leave the two dispatch calls that sit inside Angular effect()s aloneintake-wizard's SetPolicy and registratie-wizard's PrefillAdres. Both land on an unchanged tag, so no effect fires, and both are already untracked. Do not key an effect on an editing tag (Editing/Answering/Invullen) — that is the livelock store.spec.ts:18-31 guards against.

  6. 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.

  7. If a wizard drops below 250 rule-lines, delete its eslint-disable max-lines header in this same commit. See Risks — this is expected for herregistratie-wizard and is RD-02's mechanism working, not a problem to route around.

Files

  • apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts
  • apps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.ts
  • apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts

No machine changes. No story changes expected. No xlf changes.

Steps

  1. For each wizard: register the effect body on createStore under the key from decision 1, dropping the guard preamble.
  2. Delete runIfSubmitting / runIfIndienen.
  3. Delete onPrimary and onRetry; dispatch Primary and Retry from the template (decision 4).
  4. Run npm run lint. If a file is now under budget, delete its eslint-disable max-lines header (decision 7).
  5. Update this ticket's Status: to done and the README's RD-08 row to done.
  6. 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:

  1. The primary button advances through every step and submits on the last one.
  2. A failed submit shows the error, and Retry re-submits (it must leave Failed).
  3. Clicking the primary button twice quickly submits once — the trigger rule's tag-transition condition gives this for free.
  4. 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.
  • WizardStatusWizardPhase. 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-wizard is 252 rule-lines and will likely drop below 250. Deleting three methods (~21 lines) and adding a map registration (~12) nets roughly 9. Its eslint-disable max-lines then becomes unnecessary and reportUnusedDisableDirectives: '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's Seed exemption is what stops them firing real network calls. If a story flips to a failed state on load, or storybook-a11y goes 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 dispatch and the effect call were paired twice. The tag-transition rule prevents it. Do not add a busy guard on top; that would be a second mechanism for one rule.
  • behaviour-spec.mdx drift if you add or rename a spec. Run npm run gen:behaviour-spec in the same commit if so.