# BIG-register Self-Service Portal — Atomic Design POC A small Angular app that shows how **atomic design** makes a frontend cheap to build, reuse and extend. The domain is the **BIG-register** self-service portal (the Dutch register of healthcare professionals, run by CIBG). It looks like an NL Design System app, branded **Rijkshuisstijl**, and demonstrates a robust **async-state pattern** where the UI can never reach an inconsistent state. > Demo / POC — **no real login** (DigiD is faked) and synthetic seed data. The > business rules and data *are* served by a real **ASP.NET Core backend** > (`backend/`) consumed through a generated typed client, so the BFF + DDD design > is demonstrable, not hand-waved. Free **Fira Sans** stands in for the licensed > Rijksoverheid font and a text wordmark for the logo. --- ## Run it ```bash docker compose up # frontend + backend together → app http://localhost:4200, Swagger http://localhost:5000/swagger ``` Or run the two halves separately: ```bash npm install npm start # app → http://localhost:4200 (proxies /api → backend, proxy.conf.json) # in another terminal: cd backend && dotnet run --project src/BigRegister.Api # API → http://localhost:5000/swagger npm run storybook # component library, organized by atomic layer npm run gen:api # regenerate the typed API client from the backend OpenAPI doc ``` Flow: **Login → Dashboard → Mijn gegevens (wijziging) → Herregistratie → Intake**. The backend hosts the business rules (profession derivation, policy questions, eligibility, thresholds); see **[backend/README.md](backend/README.md)**. > **New here:** a **branching intake questionnaire** (`/intake`) where later questions > appear based on earlier answers and progress survives a page reload, plus a visual > walkthrough of the state-management ideas. See > **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** for diagrams (atomic-design pyramid, > the dispatch→reduce→view loop, RemoteData states, and "why not just signals") and a > section on **connecting to a .NET backend**. ### See every data state (scenario toggle) Append `?scenario=` to any data page (e.g. `/dashboard`) to force an async state: | URL | What you see | |-----|--------------| | `/dashboard` | real data (fast) | | `/dashboard?scenario=slow` | skeletons for ~2.5s, then data | | `/dashboard?scenario=loading` | the loading state, held open | | `/dashboard?scenario=empty` | "geen gegevens" empty state | | `/dashboard?scenario=error` | error message + **Opnieuw proberen** (retry) | --- ## How atomic design works here (folder = layer) Atomic design organizes UI into five layers, each built from the one below. In this repo the folder structure *is* the hierarchy (`src/app/`): | Layer | What it is | Examples here | |-------|-----------|---------------| | **atoms/** | smallest building blocks; wrap one design-system element | `button`, `text-input`, `heading`, `link`, `alert`, `status-badge`, `spinner`, `skeleton` | | **molecules/** | a few atoms combined into a unit | `form-field` (label + input + error), `data-row`, `async` (state wrapper) | | **organisms/** | larger, self-contained sections | `site-header`, `site-footer`, `login-form`, `registration-summary`, `registration-table`, `change-request-form` | | **templates/** | page skeletons that define layout; content is projected in | `page-layout` (header/content/footer chrome), `page-shell` (back-link + heading + intro + content) | | **pages/** | a template filled with real data | `login`, `dashboard`, `registration-detail`, `herregistratie` | Each atom is a thin Angular standalone component that applies the Utrecht/Rijkshuisstijl CSS classes — so the design system does the visual work and we only own a small, typed component API. --- ## Where you actually notice the benefit **1. Reuse — the same blocks appear everywhere.** | Component | Appears in | |-----------|-----------| | `button` | login, change-request, herregistratie, async retry, Storybook | | `form-field` + `text-input` | login form *and* change-request *and* herregistratie | | `status-badge` | dashboard summary, detail summary | | `page-shell` / `page-layout` | all four pages | | `site-header` / `site-footer` | every page | | `async` + `skeleton` | dashboard, detail | Change a component once and every screen that uses it updates. **2. A whole new page = composition, no new components.** `pages/herregistratie/herregistratie.page.ts` is a complete new flow assembled entirely from existing atoms/molecules/templates — zero new building blocks. The branching **intake wizard** went further: it needed only **one new atom** (`radio-group`) and **one new organism** (`intake-wizard`); the form fields, buttons, alerts, spinner and page shell were all reused. That's the payoff: new screens cost almost nothing. **3. Templates remove per-page boilerplate.** Every page used to repeat its own back-link + heading + intro markup. `page-shell` captures that once; pages now read like `…`. **4. Theming is one import.** The look comes from `@rijkshuisstijl-community/design-tokens`. `src/styles.scss` imports the `lintblauw` palette and applies `rhc-theme lintblauw` on ``. Swap the palette import to re-theme the whole app — no component changes. --- ## State management (no impossible states) Data fetching uses Angular's native, signal-based **`resource`** over the generated typed client (no NgRx, no extra dependency). Each context's `infrastructure/*.adapter.ts` exposes a resource that carries `status()`, `value()`, `error()` and `reload()` as signals, and a `parse*` function validates the response at the trust boundary (DTO → domain). The screen-shaped ("BFF-lite") endpoints return server-computed decisions the FE renders rather than recomputes (see ADR-0001). The molecule **``** turns those signals into UI. It renders **exactly one** of four slots, chosen by a single `computed` — so loading, empty, error and loaded are mutually exclusive *by construction*. You cannot render data and an error at the same time, or show stale content during a hard failure: those states are unrepresentable. ```html ``` - **Loaded** — your content, with the value. - **Loading** — your skeleton, or a default **delayed spinner** (only appears after ~250ms, so fast connections never flash a spinner; slow ones get feedback). Skeletons are also delay-gated. → *handles slow vs fast connections.* - **Empty** — your message, or a default "Geen gegevens gevonden" (driven by an `isEmpty` predicate). - **Error** — your template, or a default alert + a **retry** button that calls `resource.reload()`. Because each data-fetching page wraps its content in ``, correct loading/empty/error handling is automatic and consistent across the app. --- ## Page transitions The chrome (`templates/shell` — header + footer) is **persistent**: it mounts once and hosts the ``, so navigating doesn't re-create it (no white flash). Only the routed content cross-fades, via Angular's native **`withViewTransitions()`** — the header/footer get a stable `view-transition-name` in `styles.scss` so they're excluded from the fade. `prefers-reduced-motion` disables the animation; non-Chromium browsers degrade to an instant navigation. ## Tech notes - Angular 22 (standalone components, signals, `httpResource`, view transitions, control flow `@if/@for`). - Styling: `@rijkshuisstijl-community/{design-tokens,components-css}` (Utrecht + RHC CSS, pre-themed Rijkshuisstijl) — imported in `src/styles.scss`, no hand-written theme. - Data: ASP.NET Core backend (`backend/`, in-memory seeded) exposed via an OpenAPI contract; the FE consumes an **NSwag-generated** typed client (`npm run gen:api`). The `?scenario=` toggle (`shared/infrastructure/scenario.interceptor.ts`) is **dev-only** — it is not wired into production builds. - `.npmrc` sets `legacy-peer-deps=true` because `@storybook/angular`'s peer range lags Angular 22; the builder runs fine (build verified). ### Dependency security The **shipped app has 0 known vulnerabilities** (`npm audit --omit=dev`). All advisories live in dev/build tooling (Storybook + the Angular build chain) and never reach the bundle. `package.json` `overrides` pin patched transitive versions, taking the full audit from 16 (incl. 3 high) down to **5 low** — the remainder all cascade from `@babel/core`'s low-severity sourceMappingURL issue, which only "fixes" by jumping to Babel 8 (a breaking change across the Storybook/Babel chain) and is deliberately left. We do **not** run `npm audit fix --force`: its proposed fix downgrades Angular 22 → 21. ### Deliberately out of scope (POC) Real auth/DigiD, real BRP/DUO upstreams, a database/persisted audit store, i18n, NgRx, licensed Rijkshuisstijl fonts/logo. (The backend itself *is* implemented.)