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>
236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# 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.ts` and `review.step.ts` —
|
|
**the shape to copy.** RD-22 built them one ticket ago; match their header comments, their
|
|
`input.required` style and their output naming.
|
|
- `registratie-wizard.component.ts:100-348` — the three `@case` blocks.
|
|
- `registratie-wizard.component.ts:411-422` — `createUploadController`, and the `dispatch`
|
|
callback 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)
|
|
|
|
1. **Three new files beside the parent, named as RD-22 named its own:**
|
|
|
|
| File | Class | Selector | Case |
|
|
| ------------------ | -------------- | ----------------------- | ------------- |
|
|
| `adres.step.ts` | `AdresStep` | `app-reg-adres-step` | lines 100-175 |
|
|
| `beroep.step.ts` | `BeroepStep` | `app-reg-beroep-step` | lines 176-285 |
|
|
| `controle.step.ts` | `ControleStep` | `app-reg-controle-step` | lines 286-348 |
|
|
|
|
2. **A step may inject `RegistratieLookupStore` directly. This is the sanctioned exception.**
|
|
It is `providedIn: '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.
|
|
|
|
- `adres` injects it for `adresStatus` (the BRP lookup banner).
|
|
- `beroep` injects it for the DUO lookup, and owns its own `<app-async>` over it.
|
|
- `controle` injects it to build `samenvattingVragen`.
|
|
|
|
The parent keeps its own injection too — the BRP prefill effect needs it (decision 6).
|
|
|
|
3. **Inputs down, narrow outputs up, `dispatch` never passed down:**
|
|
|
|
| Step | Inputs | Outputs |
|
|
| ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `adres` | `draft`, `errors` | `fieldChange: { key: DraftField; value: string }`, `kanaalChange: string` |
|
|
| `beroep` | `draft`, `errors`, `upload` | `uploadMsg: UploadMsg`, `antwoordChange: { vraagId: string; value: string }`, `diplomaChosen: string`, `beroepDeclared: string` |
|
|
| `controle` | `draft` | `edit: number` |
|
|
|
|
Four outputs on `beroep` is 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 a `dispatch`.
|
|
|
|
4. **The upload controller moves into `beroep.step.ts` and emits instead of dispatching.**
|
|
`createUploadController` takes a `dispatch` callback, so the step builds its own:
|
|
|
|
```ts
|
|
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. `previewUrlFor` moves with the
|
|
controller — it is `uploadCtl.previewUrlFor` and the child takes it as a function reference.
|
|
|
|
5. **`onDiplomaKeuze` stays in the parent, and loses its `data` parameter.** The step emits only
|
|
the chosen id (`diplomaChosen`). The parent keeps its `duoData` computed and reads it inside
|
|
the method instead of receiving it as an argument:
|
|
|
|
```ts
|
|
protected onDiplomaKeuze(id: string) {
|
|
const data = this.duoData();
|
|
if (!data) return;
|
|
… // body otherwise unchanged
|
|
}
|
|
```
|
|
|
|
Building a `KiesDiploma`/`KiesHandmatig` message 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.
|
|
|
|
6. **The BRP prefill `effect` stays in the parent**, exactly as written, including its
|
|
`untracked` call. It writes to the machine, so it belongs where the machine lives. Do not move
|
|
it into `adres.step.ts`.
|
|
|
|
7. **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 the `wizardSuccess`
|
|
block. It gains one computed, as RD-22's parent did:
|
|
|
|
```ts
|
|
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`.
|
|
|
|
8. **Delete `/* eslint-disable max-lines */` from the parent.** Mandatory:
|
|
`reportUnusedDisableDirectives` is `error`, so the two rules pin each other in both
|
|
directions.
|
|
|
|
9. **No stories for the new steps** (PLAN's corollary). `registratie-wizard.stories.ts` already
|
|
mounts every step by seeding the machine, and the parent's public API does not move.
|
|
|
|
10. **Move the markup, do not improve it.** Every `i18n` id, label, placeholder, `fieldId` and
|
|
`aria` string stays byte-identical. `Errors` is 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
|
|
|
|
1. Write `adres.step.ts` — the smallest, and the one that proves the injection pattern.
|
|
2. Write `controle.step.ts` — read-only markup plus one output.
|
|
3. Write `beroep.step.ts` last: it carries the async lookup, the policy questions and the upload
|
|
controller.
|
|
4. Replace the `@switch` with the three elements and wire the outputs per decision 3.
|
|
5. Delete the members listed in decision 7, add the `errors` computed, reshape `onDiplomaKeuze`
|
|
per decision 5, prune `imports:`.
|
|
6. Delete the disable (decision 8).
|
|
7. `git add -A`, then run the acceptance commands.
|
|
8. Update this ticket's `Status:` to `done` and the README's RD-23 row to `done`.
|
|
9. Commit all of it together.
|
|
|
|
## Acceptance criteria
|
|
|
|
Measured against the tree before handover. Run after `git add -A`.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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):
|
|
|
|
- `createUploadController` is **2**, not 1: `git grep -c` counts matching lines, and an
|
|
import plus its one call site are always two lines (same shape as `createStore` in
|
|
`intake-wizard.component.ts`, which is 2, and `createDraftSync` in this same parent, which
|
|
is 3). A count of 1 is unreachable without an import alias that would exist only to dodge
|
|
the check.
|
|
- `dispatch` is **1**, not 0: decision 4's mandated snippet is
|
|
`dispatch: (msg) => this.uploadMsg.emit(msg),` — the `UploadControllerDeps.dispatch`
|
|
property name is not the machine's `dispatch`, 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:
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
git grep -ho "@@[a-zA-Z0-9_.]*" -- $D/ | sort -u | wc -l # is 43 -> MUST still be 43
|
|
```
|
|
|
|
```bash
|
|
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 `i18n` change.
|
|
|
|
## Risks
|
|
|
|
- **The upload controller is the hard part, and the reason this ticket exists.** Five template
|
|
handlers become one `uploadMsg` output. Get the `dispatch: (msg) => this.uploadMsg.emit(msg)`
|
|
wiring right and the rest is markup movement.
|
|
- **`previewUrlFor` is 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 its `this`.
|
|
- **`onDiplomaKeuze` must not move into the step** (decision 5). It builds machine messages from
|
|
the DUO payload.
|
|
- **Four outputs on `beroep` is the design, not a smell** (decision 3). Do not collapse them into
|
|
a single message-shaped output — that is passing `dispatch` up 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.
|