Files
ng-signals-template/README.md
T
ehoandClaude Sonnet 5 927404afd7 feat: minimal signals + RemoteData template with a users feature
Extracted from atomic-design-poc: RemoteData<E,T> (bare union + fromResource,
no combinators) + a trimmed <app-async> switch, no Elm-style store — state is
resource() plus one plain signal. Minimal DDD layering per context
(domain/infrastructure/application/ui) combined with atomic design inside
ui/ (atoms/molecules/organisms/templates/pages), mirroring the POC's
conventions at template scale.

One worked feature (users/): a list with a click-through to a detail view
(its own independent resource() fetch) and a Back action. Tests are
BDD-style (describe/it, one assertion per it) and black-box (assert
rendered DOM/emitted events only) - 100% branch/line coverage. README.md
documents what's deliberately omitted vs. the POC and the concrete growth
path back to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 23:27:09 +02:00

5.9 KiB

ng-signals-template

A minimal Angular 22 starter: signals for state, resource() for async data, and a RemoteData type that makes "loading but has an error," "success with no value," and similar illegal combinations impossible to construct. One worked feature (users/) shows the whole pattern end to end, including an action (click a user → see their details → go back).

It's extracted from a larger reference app — the "POC" referenced throughout this document — which shows the same ideas grown up to production scale (multi-context DDD, enforced architecture boundaries, i18n, a real design system, generated API clients). This template deliberately keeps only the part of that setup useful from day one, and documents exactly where to reach for the rest as a project grows.

Running it

npm start   # ng serve
npm test    # ng test (Vitest)

What's here

  • RemoteData<E, T> (shared/application/remote-data.ts) — a 4-variant union (Loading | Empty | Failure | Success) plus fromResource(), which projects Angular's own resource() into one. No store, no reducer — resource() already holds the async state; RemoteData just normalizes it for exhaustive rendering.
  • <app-async> (shared/ui/molecules/async.component.ts) — a @switch over all 4 states: a spinner while loading, an empty message, a failure message with a retry button, or your projected content on success. Reused by both fetches in users/.
  • <app-page-shell> (shared/ui/templates/page-shell.component.ts) — a heading plus one content slot. That's it.
  • users/ — one feature context, laid out the same way a bigger one would be: domain/ (pure types, no Angular import), infrastructure/ (the only file allowed to call fetch), application/ (composes infrastructure + resource() — this is also exactly where a real store would slot in later), ui/ (organisms + the page). Clicking a user in the list sets one plain signal on the page, which swaps in a UserDetailComponent that does its own independent fetch.
  • Path aliases @shared/* and @users/* (see tsconfig.json) instead of relative ../../ imports, one per context — add one per new context you create.
  • Tests are BDD-style (describe/it, one expect per it) and black-box: they assert on rendered DOM and emitted events, never on a component's private fields, so a test never breaks just because an internal was refactored.

What's deliberately not here (vs. the POC)

Missing Why
Elm-style store (createStore/Model-Msg-reduce) Not needed until a page's state has more than a couple of interacting fields — see Growth path below.
i18n ($localize + translation file) POC-specific requirement (a Dutch app shipping English too); irrelevant for a single-locale starter.
CIBG Huisstijl theming / token bridge The POC's specific design system; a starter has no house style to vendor yet.
dependency-cruiser boundary enforcement Real value once you have 2+ contexts that must not import each other; overhead for one.
contracts/ layer + generated API client + parse* boundary Only earns its keep once you're consuming a real backend's OpenAPI contract, not a public test API.
Storybook + axe a11y gate Testing/documentation infrastructure that pays off at a much bigger component count.
CI pipeline Nothing to gate yet with one context and no deploy target.

Growth path — when you outgrow this

Each of these is a real, working pattern in the POC — copy it when you actually need it, not before:

  • A page's state grows past 2-3 interacting fields, or needs undo/multi-step flow → add an Elm-style store: shared/application/store.ts (createStore) + a *.machine.ts per feature (Model/Msg/pure reduce).
  • You have 2+ contexts that must not import each other → add dependency-cruiser (.dependency-cruiser.js) to enforce the domain → application → infrastructure/ui direction this template already follows by convention but doesn't check.
  • You're consuming a real backend's OpenAPI contract → add a contracts/ layer (wire DTOs) + a generated typed client + a hand-written parse* boundary in infrastructure/ (see ADR-0001, .claude/skills/bff-endpoint/SKILL.md if you're working from the POC directly).
  • A second locale → wrap user-facing copy in $localize with a stable custom id and add a translation .xlf file (see the POC's CLAUDE.md "User-facing copy" convention).
  • A real design system → vendor your CSS, then bridge your own token names onto it the way ADR-0003 (docs/reference/architecture/0003-cibg-huisstijl.md in the POC) does — keep your token names stable, only their values change.
  • Testing/a11y at real component count → add Storybook + the axe a11y addon so every component's states are visually verifiable and accessibility-checked, not just behavior-tested.

Folder convention

Each business capability is a context under src/app/<context>/, split into domain/application/infrastructure/ui — dependencies point inward (ui → application → domain; only application reaches infrastructure). Inside a context's ui/, components are organized by atomic-design layer (atoms → molecules → organisms → templates → pages); shared/ holds only cross-context building blocks. When you add a second context, mirror users/'s shape.