import { Meta, Canvas } from '@storybook/addon-docs/blocks'; import * as AsyncStories from '../app/shared/ui/async/async.stories'; # RemoteData & Async An async fetch has exactly four states: still loading, loaded-but-empty, failed, or loaded-with-a-value. Modeling that as `loading`/`error`/`data` booleans permits nonsense combinations ("loading **and** error", "data **and** error" — which one does the UI believe?). `src/app/shared/application/remote-data.ts` closes that off with one tagged union instead: ```ts type RemoteData = { tag: 'Loading' } | { tag: 'Empty' } | { tag: 'Failure'; error: E } | { tag: 'Success'; value: T }; ``` ## Combining sources Two or more independent fetches often need to render as ONE state (e.g. a registration call and a BRP call feeding the same page). `map`/`map2`/`map3`/`andThen` combine them with one precedence rule: **Failure beats Loading beats Empty beats Success** — if either source failed, the combined result is a failure; only when every source succeeded do you get a combined value. ```ts map2(registration, person, (reg, p) => ({ registration: reg, person: p })); ``` ## Rendering it: `` `shared/ui/async` renders exactly one of the four templates — never two at once, by construction, since the component switches on the union's tag. Feed it either: - **`[resource]`** — a raw Angular `resource()` (the common case; the component projects it into a `RemoteData` internally via `fromResource`), or - **`[data]`** — an already-combined `RemoteData` (e.g. from a store's `computed()` using `map`/`map2`). The default loading UI is a spinner, delay-gated (~250ms) so a fast response never flashes it; override with an `appAsyncLoading` template. `appAsyncEmpty` and `appAsyncError` are likewise optional — omit them and you get a sensible default (a "geen gegevens" message / an alert with a retry button). ## The `appAsyncLoaded` slot isn't generically typed to your value This is a real Angular constraint, not an oversight: a structural directive's type parameter can only be inferred from an **input bound on that same element** (this is how `*ngFor="let x of items"` and `*ngIf="x as y"` work — the type comes from `ngForOf`/`ngIf`, inputs on the very same tag). `` sits on a *different* node than ``, so `p` cannot inherit a type from that sibling input, even though they're nested in the same template. Angular types it `unknown`, and `ngTemplateContextGuard` can't fix that without an input to seed it from — the shared `AsyncComponent`/`AsyncLoadedDirective` pair is properly generic internally, but that genericity stops at the component's own boundary. The idiom this repo uses instead — see `brief.page.ts`, `dashboard.page.ts`, `registration-detail.page.ts` — is a small **typed `computed()`** that unwraps the `Success` value, narrowed locally in the template with `@if (x(); as p)`: ```ts // in the component class protected readonly loaded = computed(() => { const s = this.model(); // or store.someRemoteData() return s.tag === 'loaded' ? s : undefined; }); ``` ```html @if (loaded(); as s) { } ``` No `$any()`, no cast — `loaded()` is a real, checked `T | undefined`, and `@if (…; as s)` narrows it the same way any other nullable signal would. ## The `?scenario=` dev toggle Any data page can be forced through all four states without touching the backend: `?scenario=slow|loading|empty|error` (dev-only, `scenario.interceptor.ts`) rewrites the timing/outcome of `/api/*` calls. Try it on `/brief` or `/dashboard`. ## Where the fetch ends and the domain begins A store's own state machine (its `*.machine.ts`) should own the **domain** lifecycle of what it holds (draft → submitted → approved, in the brief's case) — not the network fetch's loading/failure, which is a generic concern `RemoteData` already models. Where a machine's own `loading`/`failed` tags purely mirror the fetch (nothing extra beyond "not loaded yet" / "the GET failed"), project them onto a `RemoteData` computed at the store layer for `` to render, the way `BriefStore.remoteData` does — the machine keeps deciding what the *letter* is doing, `RemoteData` keeps deciding what the *fetch* is doing.