Add branching intake wizard (derived steps + radio-group atom)
A second wizard demonstrating a BRANCHING flow: the visible steps are derived from the answers by a pure `visibleSteps` function rather than stored, so answering "buiten Nederland gewerkt? -> ja" or reporting few hours adds steps and the progress denominator changes live. Same Elm-style store + RemoteData patterns as the fixed wizard; answers persist to localStorage. - intake.machine.ts: IntakeState union + Answers + visibleSteps + pure reduce (+spec) - intake-wizard organism, intake.page, submit-intake command - new radio-group atom (ControlValueAccessor) in shared/ui - /intake route + dashboard link + concepts showcase section - tighten Aantekening.type to a 'Specialisme' | 'Aantekening' union - README + ARCHITECTURE updated Verified live end-to-end (branches add steps 4->5->6, review, submit) with no console errors; build, unit tests, and Storybook all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
50
src/app/shared/ui/radio-group/radio-group.component.ts
Normal file
50
src/app/shared/ui/radio-group/radio-group.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Component, forwardRef, input } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
export interface RadioOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** Atom: a radio group. Thin wrapper over the Utrecht/RHC radio CSS, wired as a
|
||||
form control so it works with ngModel just like the text-input atom. */
|
||||
@Component({
|
||||
selector: 'app-radio-group',
|
||||
template: `
|
||||
<div class="utrecht-form-field-radio-group" role="radiogroup">
|
||||
@for (opt of options(); track opt.value) {
|
||||
<label class="utrecht-form-label utrecht-form-label--radio-button" style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0">
|
||||
<input
|
||||
class="utrecht-radio-button"
|
||||
type="radio"
|
||||
[name]="name()"
|
||||
[value]="opt.value"
|
||||
[checked]="value === opt.value"
|
||||
[disabled]="disabled"
|
||||
(change)="select(opt.value)"
|
||||
(blur)="onTouched()" />
|
||||
{{ opt.label }}
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioGroupComponent), multi: true }],
|
||||
})
|
||||
export class RadioGroupComponent implements ControlValueAccessor {
|
||||
options = input.required<RadioOption[]>();
|
||||
name = input.required<string>();
|
||||
|
||||
value = '';
|
||||
disabled = false;
|
||||
onChange: (v: string) => void = () => {};
|
||||
onTouched: () => void = () => {};
|
||||
|
||||
select(v: string) {
|
||||
this.value = v;
|
||||
this.onChange(v);
|
||||
}
|
||||
writeValue(v: string) { this.value = v ?? ''; }
|
||||
registerOnChange(fn: (v: string) => void) { this.onChange = fn; }
|
||||
registerOnTouched(fn: () => void) { this.onTouched = fn; }
|
||||
setDisabledState(d: boolean) { this.disabled = d; }
|
||||
}
|
||||
17
src/app/shared/ui/radio-group/radio-group.stories.ts
Normal file
17
src/app/shared/ui/radio-group/radio-group.stories.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from '@storybook/angular';
|
||||
import { RadioGroupComponent } from './radio-group.component';
|
||||
|
||||
const meta: Meta<RadioGroupComponent> = {
|
||||
title: 'Atoms/RadioGroup',
|
||||
component: RadioGroupComponent,
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-radio-group [options]="options" [name]="name" />`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<RadioGroupComponent>;
|
||||
|
||||
export const JaNee: Story = {
|
||||
args: { name: 'voorbeeld', options: [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }] },
|
||||
};
|
||||
Reference in New Issue
Block a user