Move the adres, beroep and controle cases out of registratie-wizard.component.ts into adres.step.ts, beroep.step.ts and controle.step.ts, matching RD-22's *.step.ts convention. The parent drops from ~568 to 274 lines and loses its `eslint-disable max-lines`. The upload controller moves into beroep.step.ts and emits `uploadMsg` instead of dispatching directly; the parent maps that back onto the machine's `Upload` message. `onDiplomaKeuze` stays in the parent (message construction from the DUO payload belongs in the container) and now takes only the chosen id, reading its own `duoData` computed instead of receiving the DUO payload as an argument. Each step injects `RegistratieLookupStore` directly for its own async presentation (adresStatus, the DUO lookup, samenvattingVragen) — the sanctioned exception, since it is a root singleton. Markup moved verbatim; the `@@` id count across the directory stays 43. Two of the ticket's acceptance numbers do not hold against correct code and are corrected in the ticket file: `createUploadController` is 2 lines (import + call), not 1 — `git grep -c` counts lines, and the same shape gives 2 for `createStore` and 3 for `createDraftSync` elsewhere in this codebase. `dispatch` is 1, not 0 — decision 4's mandated `UploadControllerDeps.dispatch` property name is that string even though it is not the machine's dispatch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
RD-23 — Split registratie-wizard into three steps, and move the upload controller
Status: done Source: PLAN.md 3c, order step 5
Why
registratie-wizard.component.ts measures ~568 effective lines against a limit of 250 — the
largest file in the arc, and more than twice the budget. It carries
/* eslint-disable max-lines */.
It is the same shape as RD-22's intake wizard: one @switch, three @case blocks, three
screens in one file. It is harder in one way that PLAN calls out — moving the upload
controller is what gets the parent under 250, and the controller is a stateful thing, not
markup.
RD-22 already set the *.step.ts convention. Follow it.
Read first
apps/ssp/src/app/herregistratie/ui/intake-wizard/buitenland.step.tsandreview.step.ts— the shape to copy. RD-22 built them one ticket ago; match their header comments, theirinput.requiredstyle and their output naming.registratie-wizard.component.ts:100-348— the three@caseblocks.registratie-wizard.component.ts:411-422—createUploadController, and thedispatchcallback that decision 4 rewires.registratie-wizard.component.ts:568-585—onDiplomaKeuze, which decision 5 reshapes.apps/ssp/src/app/registratie/ui/address-fields/address-fields.component.ts:13-18— the contract the whole family follows.
Decisions (pre-made, don't relitigate)
-
Three new files beside the parent, named as RD-22 named its own:
File Class Selector Case adres.step.tsAdresStepapp-reg-adres-steplines 100-175 beroep.step.tsBeroepStepapp-reg-beroep-steplines 176-285 controle.step.tsControleStepapp-reg-controle-steplines 286-348 -
A step may inject
RegistratieLookupStoredirectly. This is the sanctioned exception. It isprovidedIn: 'root', so every injection is the same instance, and PLAN names this "the one place the dashboard's axis does apply": the step owns its own async presentation rather than making the parent a pass-through for four lookup signals.adresinjects it foradresStatus(the BRP lookup banner).beroepinjects it for the DUO lookup, and owns its own<app-async>over it.controleinjects it to buildsamenvattingVragen.
The parent keeps its own injection too — the BRP prefill effect needs it (decision 6).
-
Inputs down, narrow outputs up,
dispatchnever passed down:Step Inputs Outputs adresdraft,errorsfieldChange: { key: DraftField; value: string },kanaalChange: stringberoepdraft,errors,uploaduploadMsg: UploadMsg,antwoordChange: { vraagId: string; value: string },diplomaChosen: string,beroepDeclared: stringcontroledraftedit: numberFour outputs on
beroepis correct: they are four distinct user intents, and each maps to one message in the parent. That is not the same thing as handing the step adispatch. -
The upload controller moves into
beroep.step.tsand emits instead of dispatching.createUploadControllertakes adispatchcallback, so the step builds its own:protected uploadCtl = createUploadController({ wizardId: 'registratie', getUpload: () => this.upload(), dispatch: (msg) => this.uploadMsg.emit(msg), getCategoryParams: () => ({ … }), // unchanged, reads this.draft() });The parent maps it back with
(uploadMsg)="dispatch({ tag: 'Upload', msg: $event })". This is what collapses five template handlers into one output.previewUrlFormoves with the controller — it isuploadCtl.previewUrlForand the child takes it as a function reference. -
onDiplomaKeuzestays in the parent, and loses itsdataparameter. The step emits only the chosen id (diplomaChosen). The parent keeps itsduoDatacomputed and reads it inside the method instead of receiving it as an argument:protected onDiplomaKeuze(id: string) { const data = this.duoData(); if (!data) return; … // body otherwise unchanged }Building a
KiesDiploma/KiesHandmatigmessage needs the DUO payload to map an id to a beroep and its question ids. That is machine-message construction, and it belongs in the container. -
The BRP prefill
effectstays in the parent, exactly as written, including itsuntrackedcall. It writes to the machine, so it belongs where the machine lives. Do not move it intoadres.step.ts. -
The parent keeps the shell wiring, the store and its effect map,
draftSync, the seed constructor,phase,primaryLabel,stepTitle,stepLabels,errorList,referentie,cursor,step,draft,upload,duoData,onDiplomaKeuze, and thewizardSuccessblock. It gains one computed, as RD-22's parent did:protected errors = computed<Errors>(() => this.invullen()?.errors ?? {});Everything else in the list below moves out with the markup that used it:
uploadCtl,previewUrlFor,kanalen,err,vraagErr,antwoord,set,setKanaal,handmatigActief,diplomaKeuze,diplomaOptions,beroepOptions,actieveVragen,samenvattingVragen,adresSamenvatting,adresHerkomstLabel,correspondentieLabel,diplomaHerkomstLabel,adresStatus,lookupRd. -
Delete
/* eslint-disable max-lines */from the parent. Mandatory:reportUnusedDisableDirectivesiserror, so the two rules pin each other in both directions. -
No stories for the new steps (PLAN's corollary).
registratie-wizard.stories.tsalready mounts every step by seeding the machine, and the parent's public API does not move. -
Move the markup, do not improve it. Every
i18nid, label, placeholder,fieldIdandariastring stays byte-identical.Errorsis already exported from the machine — RD-20 made it a type alias — so no machine change is needed this time.
Files
apps/ssp/src/app/registratie/ui/registratie-wizard/adres.step.ts(new)apps/ssp/src/app/registratie/ui/registratie-wizard/beroep.step.ts(new)apps/ssp/src/app/registratie/ui/registratie-wizard/controle.step.ts(new)apps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts
No machine file changes. No shared-library changes.
Steps
- Write
adres.step.ts— the smallest, and the one that proves the injection pattern. - Write
controle.step.ts— read-only markup plus one output. - Write
beroep.step.tslast: it carries the async lookup, the policy questions and the upload controller. - Replace the
@switchwith the three elements and wire the outputs per decision 3. - Delete the members listed in decision 7, add the
errorscomputed, reshapeonDiplomaKeuzeper decision 5, pruneimports:. - Delete the disable (decision 8).
git add -A, then run the acceptance commands.- Update this ticket's
Status:todoneand the README's RD-23 row todone. - Commit all of it together.
Acceptance criteria
Measured against the tree before handover. Run after git add -A.
D=apps/ssp/src/app/registratie/ui/registratie-wizard
P=$D/registratie-wizard.component.ts
git ls-files "$D/*.step.ts" | wc -l # is 0 -> MUST be 3
git grep -c "eslint-disable max-lines" -- $P # is 1 -> MUST be 0
The upload controller moved, and the store did not follow the markup down:
git grep -c "uploadCtl" -- $P # is 7 -> MUST be 0
git grep -c "createUploadController" -- $D/beroep.step.ts # MUST be 2
git grep -c "dispatch" -- "$D/*.step.ts" | awk -F: '{s+=$NF} END {print s+0}' # MUST be 1
Two corrections found while running these before handover (recorded here per the README's rule 4 on ticket-writing misses):
createUploadControlleris 2, not 1:git grep -ccounts matching lines, and an import plus its one call site are always two lines (same shape ascreateStoreinintake-wizard.component.ts, which is 2, andcreateDraftSyncin this same parent, which is 3). A count of 1 is unreachable without an import alias that would exist only to dodge the check.dispatchis 1, not 0: decision 4's mandated snippet isdispatch: (msg) => this.uploadMsg.emit(msg),— theUploadControllerDeps.dispatchproperty name is not the machine'sdispatch, but it is the same string. Satisfying decision 4 verbatim and satisfying a target of 0 are mutually exclusive.
The three per-step concerns left the parent:
git grep -c "adresStatus" -- $P # is 3 -> MUST be 0
git grep -c "samenvattingVragen" -- $P # is 2 -> MUST be 0
git grep -c "previewUrlFor" -- $P # is 3 -> MUST be 0
The copy did not drift (decision 10):
git grep -ho "@@[a-zA-Z0-9_.]*" -- $D/ | sort -u | wc -l # is 43 -> MUST still be 43
npm run ci --full # exits 0
Verification
The @@ id count is 43 across the whole registratie-wizard/ directory, so the three new
files are included. ng build --localize fails on an id that is added without a translation
but never on one silently lost; the count is the only check that catches a loss.
--full is required. The existing story mounts all three steps, and the axe run over it is
what proves the projected markup kept its labels, its error wiring and its aria strings.
Do not add a line-count command. npm run lint is the exact check (decision 8).
If dotnet test fails with SQLite Error 1: 'no such table: …', that is the stale-database
trap, not your change. See the README's Troubleshooting section — RD-22 hit it.
Out of scope
herregistratie-wizard. PLAN: do not split it for symmetry.- Changing the upload controller itself, or
upload.machine.ts. - Moving the BRP prefill effect (decision 6).
- Adding stories (decision 9).
- Any validation, message or
i18nchange.
Risks
- The upload controller is the hard part, and the reason this ticket exists. Five template
handlers become one
uploadMsgoutput. Get thedispatch: (msg) => this.uploadMsg.emit(msg)wiring right and the rest is markup movement. previewUrlForis passed to a child as a function reference, not called in the template. Keep it an arrow property on the step, or the binding silently loses itsthis.onDiplomaKeuzemust not move into the step (decision 5). It builds machine messages from the DUO payload.- Four outputs on
beroepis the design, not a smell (decision 3). Do not collapse them into a single message-shaped output — that is passingdispatchup under another name, and it moves message construction into the step. - Deleting the disable is mandatory (decision 8); its failure message reads like an unrelated error.
- This is the largest single diff in the arc. Work step by step in the order given, and let the type-checker confirm each before moving on.