From 770d454a32eafd25c76a05954ab67bb8a1ad5c3c Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Fri, 26 Jun 2026 17:25:39 +0200 Subject: [PATCH] Extract reusable address-fields organism, adopt in both registratie call-sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editable address block (straat/postcode/woonplaats) was hand-built inline in two places — the registratie wizard and the change-request form. Factor it into one pure presentational organism (values in, errors in, per-field change out) grouped in a fieldset/legend, and adopt it in both. Behaviour, validation and state flow are unchanged: the wizard still dispatches SetField (flipping adresHerkomst on edit) and the change-request form still parses the postcode on submit. Storybook entry added; other field clusters left inline by design (single-use or genuinely divergent). Co-Authored-By: Claude Opus 4.8 --- .../address-fields.component.ts | 50 +++++++++++++++++++ .../address-fields/address-fields.stories.ts | 31 ++++++++++++ .../change-request-form.component.ts | 25 +++++----- .../registratie-wizard.component.ts | 17 +++---- 4 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 src/app/registratie/ui/address-fields/address-fields.component.ts create mode 100644 src/app/registratie/ui/address-fields/address-fields.stories.ts diff --git a/src/app/registratie/ui/address-fields/address-fields.component.ts b/src/app/registratie/ui/address-fields/address-fields.component.ts new file mode 100644 index 0000000..f01f9f7 --- /dev/null +++ b/src/app/registratie/ui/address-fields/address-fields.component.ts @@ -0,0 +1,50 @@ +import { Component, input, output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; +import { TextInputComponent } from '@shared/ui/text-input/text-input.component'; + +export interface AdresValue { + straat: string; + postcode: string; + woonplaats: string; +} +export type AdresErrors = Partial>; + +/** Organism: the editable address block (straat / postcode / woonplaats), grouped + in a fieldset/legend. Pure & presentational — values in via `value`, errors in + via `errors`, every keystroke out via `fieldChange`. No store, no services, no + internal state; the container owns the Model and decides what a change means + (registratie-wizard dispatches SetField; change-request-form sets its props). + Composes the form-field molecule (×3) so labels/error wiring stay consistent. */ +@Component({ + selector: 'app-address-fields', + imports: [FormsModule, FormFieldComponent, TextInputComponent], + template: ` +
+ {{ legend() }} + + + + + + + + + +
+ `, +}) +export class AddressFieldsComponent { + value = input.required(); + errors = input({}); + /** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */ + idPrefix = input('adres'); + legend = input('Adres'); + fieldChange = output<{ key: keyof AdresValue; value: string }>(); +} diff --git a/src/app/registratie/ui/address-fields/address-fields.stories.ts b/src/app/registratie/ui/address-fields/address-fields.stories.ts new file mode 100644 index 0000000..e59e6be --- /dev/null +++ b/src/app/registratie/ui/address-fields/address-fields.stories.ts @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/angular'; +import { moduleMetadata } from '@storybook/angular'; +import { AddressFieldsComponent } from './address-fields.component'; + +const meta: Meta = { + title: 'Organisms/Address Fields', + component: AddressFieldsComponent, + decorators: [moduleMetadata({ imports: [AddressFieldsComponent] })], + render: (args) => ({ + props: { ...args, onChange: (e: unknown) => console.log('fieldChange', e) }, + template: ` + `, + }), +}; +export default meta; +type Story = StoryObj; + +const empty = { straat: '', postcode: '', woonplaats: '' }; + +export const Default: Story = { args: { value: empty, errors: {} } }; + +export const Prefilled: Story = { + args: { value: { straat: 'Stationsplein 1', postcode: '3511 ED', woonplaats: 'Utrecht' }, errors: {} }, +}; + +export const WithErrors: Story = { + args: { + value: { straat: '', postcode: '12', woonplaats: '' }, + errors: { straat: 'Vul een straat en huisnummer in.', postcode: 'Voer een geldige postcode in, bijv. 1234 AB.' }, + }, +}; diff --git a/src/app/registratie/ui/change-request-form/change-request-form.component.ts b/src/app/registratie/ui/change-request-form/change-request-form.component.ts index c43839f..f83807a 100644 --- a/src/app/registratie/ui/change-request-form/change-request-form.component.ts +++ b/src/app/registratie/ui/change-request-form/change-request-form.component.ts @@ -1,9 +1,8 @@ import { Component, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; -import { TextInputComponent } from '@shared/ui/text-input/text-input.component'; import { ButtonComponent } from '@shared/ui/button/button.component'; import { HeadingComponent } from '@shared/ui/heading/heading.component'; +import { AddressFieldsComponent, AdresValue } from '@registratie/ui/address-fields/address-fields.component'; import { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode'; /** A submitted change request carries a *parsed* postcode (branded Postcode), @@ -19,19 +18,15 @@ export interface ChangeRequest { straight from the parser's Result — no parallel "is it valid" flag to drift. */ @Component({ selector: 'app-change-request-form', - imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent], + imports: [FormsModule, ButtonComponent, HeadingComponent, AddressFieldsComponent], template: ` Adreswijziging doorgeven
- - - - - - - - - +
Wijziging indienen
@@ -46,6 +41,12 @@ export class ChangeRequestFormComponent { zipError = signal(''); submitted = output(); + onAdres(e: { key: keyof AdresValue; value: string }) { + if (e.key === 'straat') this.street = e.value; + else if (e.key === 'postcode') this.zip = e.value; + else this.city = e.value; + } + onSubmit() { const street = this.street.trim(); this.streetError.set(street ? '' : 'Vul straat en huisnummer in.'); diff --git a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts index af5d25c..67c97e1 100644 --- a/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts +++ b/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.ts @@ -10,6 +10,7 @@ import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component'; import { DataRowComponent } from '@shared/ui/data-row/data-row.component'; import { StepperComponent } from '@shared/ui/stepper/stepper.component'; import { ASYNC } from '@shared/ui/async/async.component'; +import { AddressFieldsComponent } from '@registratie/ui/address-fields/address-fields.component'; import { createStore } from '@shared/application/store'; import { RemoteData, fromResource } from '@shared/application/remote-data'; import { BrpAdapter, parseBrpAddress } from '@registratie/infrastructure/brp.adapter'; @@ -43,7 +44,8 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed" selector: 'app-registratie-wizard', imports: [ FormsModule, FormFieldComponent, TextInputComponent, RadioGroupComponent, ButtonComponent, - AlertComponent, SpinnerComponent, SkeletonComponent, DataRowComponent, StepperComponent, ...ASYNC, + AlertComponent, SpinnerComponent, SkeletonComponent, DataRowComponent, StepperComponent, + AddressFieldsComponent, ...ASYNC, ], template: ` @switch (state().tag) { @@ -67,15 +69,10 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed" We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in. } } - - - - - - - - - +