Full-width layout, page-shell template, httpResource async states, README
- Fix full-bleed header/footer (align-items:stretch + block hosts; centered content column via shared --app-content-max). - New templates/page-shell (back-link + heading + intro + content, narrow mode); all pages refactored to compose it. - Async state management with native httpResource + <app-async> wrapper that renders exactly one of loading/empty/error/loaded (impossible states unrepresentable); delayed spinner + skeleton atoms for slow/fast connections. - Scenario interceptor (?scenario=slow|loading|empty|error) to demo every state. - Storybook: spinner/skeleton/page-shell/async-states stories. - README rewritten as a guide (atomic design, reuse benefits, state handling). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
161
README.md
161
README.md
@@ -1,48 +1,131 @@
|
||||
# BIG-register Self Service Portal — Atomic Design POC
|
||||
# BIG-register Self-Service Portal — Atomic Design POC
|
||||
|
||||
An Angular POC showing how **atomic design** makes a frontend cheap to build and
|
||||
reuse. Domain: the **BIG-register** self-service portal (NL register of healthcare
|
||||
professionals). Styled with the **Rijkshuisstijl** via the NL Design System /
|
||||
Utrecht component CSS — no hand-written theme.
|
||||
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.
|
||||
|
||||
## The atomic-design story (folder = layer)
|
||||
> Demo / POC — no real data, no real login. Free **Fira Sans** stands in for the
|
||||
> licensed Rijksoverheid font and a text wordmark for the logo.
|
||||
|
||||
```
|
||||
src/app/
|
||||
atoms/ button · text-input · heading · link · alert · status-badge
|
||||
molecules/ form-field (label+input+error) · data-row
|
||||
organisms/ site-header · site-footer · login-form · registration-summary
|
||||
registration-table · change-request-form
|
||||
templates/ page-layout (header + content + footer)
|
||||
pages/ login · dashboard · registration-detail
|
||||
core/ models · registration.service (HttpClient → public/mock/*.json)
|
||||
```
|
||||
---
|
||||
|
||||
**Reuse in action** — the same building blocks appear everywhere:
|
||||
- `page-layout` wraps all three pages.
|
||||
- `site-header` + `site-footer` render on every page.
|
||||
- `form-field` + `text-input` + `button` are shared by the login form *and* the
|
||||
change-request form.
|
||||
- `status-badge` shows on both the dashboard and the detail page.
|
||||
|
||||
Adding a new page is just composing existing organisms inside the layout — see
|
||||
`pages/` for how little code each page is.
|
||||
|
||||
## Styling / theming
|
||||
|
||||
`@rijkshuisstijl-community/design-tokens` + `@rijkshuisstijl-community/components-css`
|
||||
provide Rijkshuisstijl-themed Utrecht components as CSS. The theme is applied by the
|
||||
`rhc-theme lintblauw` classes on `<body>` (see `src/index.html`). Atoms are thin
|
||||
Angular wrappers that apply these CSS classes — so the design system does the visual
|
||||
work and we only own the component API.
|
||||
|
||||
> POC shortcuts: free **Fira Sans** stands in for the licensed Rijksoverheid font, a
|
||||
> text wordmark stands in for the logo, login is mocked, and data is static JSON.
|
||||
|
||||
## Run
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start # ng serve → http://localhost:4200 (login → dashboard → detail)
|
||||
npm start # app → http://localhost:4200
|
||||
npm run storybook # component library, organized by atomic layer
|
||||
```
|
||||
|
||||
Flow: **Login → Dashboard → Mijn gegevens (wijziging) → Herregistratie**.
|
||||
|
||||
### 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. 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 `<app-page-shell heading="…" backLink="…">…`.
|
||||
|
||||
**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 `<body>`. 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 **`httpResource`** (no NgRx,
|
||||
no extra dependency). `core/registration.service.ts` exposes resources that carry
|
||||
`status()`, `value()`, `error()`, `hasValue()` and `reload()` as signals.
|
||||
|
||||
The molecule **`<app-async>`** 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
|
||||
<app-async [resource]="reg" [isEmpty]="regEmpty">
|
||||
<ng-template appAsyncLoaded let-r> <app-registration-summary [reg]="r" /> </ng-template>
|
||||
<ng-template appAsyncLoading> <app-skeleton [count]="6" /> </ng-template>
|
||||
<!-- appAsyncEmpty / appAsyncError are optional → sensible defaults -->
|
||||
</app-async>
|
||||
```
|
||||
|
||||
- **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 `<app-async>`, correct
|
||||
loading/empty/error handling is automatic and consistent across the app.
|
||||
|
||||
---
|
||||
|
||||
## Tech notes
|
||||
|
||||
- Angular 22 (standalone components, signals, `httpResource`, 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.
|
||||
- Mock data: JSON in `public/mock/`, timing/outcome shaped by `core/scenario.interceptor.ts`.
|
||||
- `.npmrc` sets `legacy-peer-deps=true` because `@storybook/angular`'s peer range lags
|
||||
Angular 22; the builder runs fine (build verified).
|
||||
|
||||
### Deliberately out of scope (POC)
|
||||
Real auth/DigiD, real backend, i18n, NgRx, licensed Rijkshuisstijl fonts/logo.
|
||||
|
||||
Reference in New Issue
Block a user