# Learn @craft-ng This is the guided path. You build **one app**, from an empty component to a routed, tested feature — adding exactly one idea per step. If you are looking for a specific answer instead, go to the [Guide](/guide/) (organised by task) or search. ## What you will build A task list. It starts as three lines in a component and ends up with server data, optimistic updates, URL state, a validated form, a typed route and tests. | Step | What you add | | ------------------------------------------------------- | ------------------------------------------- | | [1. Your first state](/learn/01-first-state) | `craftComponent`, `state` | | [2. Derive instead of duplicate](/learn/02-derive) | computed + methods | | [3. Move logic out of the component](/learn/03-service) | `craftService` | | [4. Compose services](/learn/04-compose) | generators, `yield*` | | [5. Load server data](/learn/05-load-data) | `query` | | [6. Write server data](/learn/06-mutate-data) | `mutation`, optimistic updates | | [7. Put state in the URL](/learn/07-url-state) | `queryParams` | | [8. Build a form](/learn/08-forms) | `insertForm`, validators | | [9. Wire up routing](/learn/09-routing) | `craftRoute`, compile-time DI check | | [10. Test what you wrote](/learn/10-testing) | testing by register, architecture rules | Then: [Where to go next](/learn/next). ::: tip Wondering what this buys you over plain Angular? [What craft adds to Angular](/guide/concepts/vs-angular) is the inventory — including what it costs. ::: ## Before you start You need an Angular 21 application and Node.js 20.19+ (or 22.12+). No prior knowledge of generators, RxJS or signals internals is required — each is introduced when it first earns its place. ::: tip Read in order Every step builds on the previous one's code. Skipping ahead works, but step 4 is where the mental model clicks — don't skip that one. ::: ::: warning Experimental `@craft-ng` and this documentation are both experimental. APIs can still move between minor versions. ::: [Start → Your first state](/learn/01-first-state) --- --- url: https://ng-angular-stack.github.io/craft/learn/01-first-state.md --- # 1. Your first state **Goal:** get a reactive value on screen, and meet the two building blocks you will use in every step — `craftComponent` and a primitive. ## Install ```shell npm i @craft-ng/core@beta @craft-ng/component@beta npm i -D @craft-ng/dev-tools@beta ``` The packages are currently published on the `beta` channel. The component package contains the functional renderer, while `core` contains the reactive primitives used by the component factory. ## A component with state A Craft component is a **function**, not a class. It takes a name, meta, a logic factory, and a template: ```ts import { craftComponent, each, h1, li, ul } from '@craft-ng/component'; import { state } from '@craft-ng/core'; type Task = { id: string; title: string; done: boolean }; export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* state('tasks', [ // read yield* as "I need" { id: '1', title: 'Read step 1', done: false }, ] as Task[]); return { tasks }; }, ({ tasks }) => [ h1('Tasks'), ul(each(tasks, { track: (task) => task.id }, (task) => li(task.title))), ], ); ``` Four arguments, and each has one job: | Argument | What it is | | ------------ | ----------------------------------------------------------- | | `'Tasks'` | the component's name — used by the tooling and by host tags | | `{}` | meta: providers, styles, host properties (empty for now) | | `function*` | the **logic factory** — builds and returns the context | | `({ … }) =>` | the **template** — receives that context, returns nodes | There is no class, no decorator, no separate HTML file, and no host element wrapped around your markup. ## Inputs and outputs A component's inputs and outputs are just **parameters of the logic factory**, typed with `Input` and `Output`: ```ts import { Input, Output, button, craftComponent, div, span, } from '@craft-ng/component'; import { deepYieldable } from '@craft-ng/core'; type User = { name: string }; const UserCard = craftComponent( 'UserCard', {}, (user: Input, onRemove: Output<(user: User) => void>) => ({ user: deepYieldable(user), onRemove, }), ({ user, onRemove }) => div([ span(user.name), button({ type: 'button', *click() { yield* onRemove(yield* user()); }, }, 'Remove'), ]), ); ``` An `Input` **is a yieldable reader** — `yield* user()` reads the current value. Project nested fields with `deepYieldable` so `user.name` stays a reader. An `Output` is a yieldable callback; delegate to it with `yield*`. At the call site you pass the reader itself, not a getter: ```typescript UserCard({ user: currentUser, onRemove: removeUser, }); ``` | Angular | Craft | | ------------------------------------------- | ------------------------------------------- | | `@Input()` / `input()` / `input.required()` | a `Input` factory parameter | | `@Output()` / `output()` + `.emit(...)` | an `Output` parameter, called directly | | `[user]="u"` / `(remove)="fn($event)"` | `UserCard({ user: u, onRemove: fn })` | | Missing required input → runtime | missing parameter → **compile error** | Because it's a function call, there is no template-binding layer between caller and component: a wrong input name or type is a plain TypeScript error. ## Styling the component Styles go in the meta, and `:scope` is the component's own root: ```typescript craftComponent( 'Tasks', { styles: ` :scope { display: grid; gap: .5rem } .done { text-decoration: line-through } `, }, /* … */ ); ``` `:scope` refers to **the root of this component**. Component styles are scoped with CSS `@scope`, so the rule cannot leak into unrelated components — and Craft adds no host element or wrapper around your markup to achieve it. See [Encapsulated styles](/guide/components/styles). ## Mounting the root The app's root is a Craft component too. `provideCraftRootComponent(App)` designates it, and Angular bootstraps a thin host: ```typescript // app.config.ts export const appConfig = craftAppConfig({ providers: [provideCraftRootComponent(App)], }); ``` ```typescript // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { CraftRootComponentHost } from '@craft-ng/component'; import { toApplicationConfig } from '@craft-ng/core'; import { appConfig } from './app/app.config'; bootstrapApplication(CraftRootComponentHost, toApplicationConfig(appConfig)); ``` `toApplicationConfig` turns the craft config into the `ApplicationConfig` Angular expects, so the rest of your Angular setup is unchanged. ## The two rules of a primitive **1. A primitive is named.** `state('tasks', …)` — the first argument is always the name. It is not decoration: it tags the primitive's injector (`state:tasks`) and is what identifies this piece of state in logs, snapshots and observability. **2. It resolves to the state reference itself**: ```typescript const tasks = yield * state('tasks', []); ``` `tasks` is a yieldable reader: `yield* tasks()` in a generator, `craftUse(tasks())` at a synchronous boundary, or pass `tasks` directly to a template binding. ## What is `yield*` doing there? The factory is a generator, and `yield*` is how **this** factory drives everything it does not own — primitives and services alike. The same rule applies later to every computed and method: each entity yields its own dependencies so they show up on **its** graph. For now, treat it as "the way to use a primitive inside a factory". [Step 4](/learn/04-compose) explains what it buys you. ::: tip Coming from Angular classes? In an Angular `@Component` class there is no generator to yield from, so you drive a primitive with `craftUse(state('tasks', []))` instead. Same primitive, same result — see [Anatomy of a primitive](/guide/concepts/primitive-anatomy). ::: ## The template The template is a plain function returning nodes built with hyperscript helpers — `div`, `ul`, `li`, `button`, and one `h(tag, …)` escape hatch for anything without a helper: ```typescript ({ tasks }) => [ h1('Tasks'), ul( each(tasks, { track: (task) => task.id }, (task) => li(task.title)), ), ]; ``` Pass the reader (`tasks`) to the binding that consumes it. The renderer drives the read; wrapping `() => tasks()` is a synchronous call the yield rules reject. Use `each(...)` when the collection controls a node per item. No `*ngFor`, no change detection to think about. ## Writing to it Right now the state is read-only from the outside. Give it a writer: ```typescript const tasks = yield* state('tasks', [] as Task[], ({ set }) => ({ set })); yield* tasks.set([{ id: '1', title: 'Write step 2', done: false }]); ``` That third argument is an **insertion** — the mechanism you'll use in every step from here on. Step 2 is entirely about it. ## What you gained A component and a reactive value, both declared as functions, both named, both visible to the tooling — with no class, no constructor and no subscription. [← Overview](/learn/) [2. Derive instead of duplicate →](/learn/02-derive) --- --- url: https://ng-angular-stack.github.io/craft/learn/02-derive.md --- # 2. Derive instead of duplicate **Goal:** attach methods and derived values to your state, instead of scattering them across the component. ## The insertion argument The last argument of a primitive is an **insertion**: a function that receives the primitive's internals and returns whatever you want exposed on it. ```ts import { craftComputed, craftService, state } from '@craft-ng/core'; type Task = { id: string; title: string; done: boolean }; export const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const tasks = yield* state('tasks', [] as Task[], ({ state, set, update }) => ({ add: (title: string) => update((current) => [ ...current, { id: crypto.randomUUID(), title, done: false }, ]), toggle: (id: string) => update((current) => current.map((task) => task.id === id ? { ...task, done: !task.done } : task, ), ), remove: function* (id: string) { const current = yield* state(); return yield* set(current.filter((task) => task.id !== id)); }, remaining: craftComputed(function* () { return (yield* state()).filter((task) => !task.done).length; }), })); return tasks; }, ); ``` Everything you return is now on the ref: ```typescript yield* tasks(); // the array yield* tasks.add('Learn insertions'); yield* tasks.remaining(); // 1 ``` The context gives you `state` (the current value as a yieldable reader), `set` and `update`. Non-generator insertion methods may return `update(...)` directly — the wrapper consumes the write. `remaining` is a `craftComputed`: it does not own `state()`, so it yields it. That is how the computed's own dependency graph records the read. ## The whole component ```typescript import { button, craftComponent, each, h1, input, li, ul, } from '@craft-ng/component'; export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* state('tasks', [] as Task[], /* … as above … */); return { tasks }; }, ({ tasks }) => [ h1(function* () { return `Tasks — ${yield* tasks.remaining()} left`; }), input({ type: 'text', placeholder: 'New task…', *keydown(event) { if (event.key !== 'Enter') return; const field = event.target as HTMLInputElement; yield* tasks.add(field.value); field.value = ''; }, }), ul( each( tasks, { track: (task) => task.id, empty: () => li('Nothing to do 🎉') }, (task) => li([ input({ type: 'checkbox', checked: task.done, *change() { yield* tasks.toggle(task.id); }, }), task.title, button({ *click() { yield* tasks.remove(task.id); }, }, '×'), ]), ), ), ], ); ``` Two template things worth noting. `each(source, options, render)` takes a `track` — the stable identity the renderer uses to reuse, move and remove nodes — and an optional `empty` branch. Pass the reader itself (`tasks`) rather than `() => tasks()`. When a binding must format or call a method, use a generator and `yield*`. The logic factory is now three lines. That's the point: **behaviour lives on the state, not around it.** ## Control flow: the Angular equivalents Craft templates are TypeScript, so control flow is made of functions rather than syntax. Each Angular block has a counterpart: | Angular | Craft | | ---------- | -------------------------------------------------- | | `@for` | `each(source, { track, empty }, render)` | | `@empty` | the `empty` option of `each` | | `@if` | `ifBlock(condition, whenTrue, whenFalse?)` | | `@switch` | `matchBlock.exhaustive(source, key, handlers)` | | `@defer` | `defer(loader, options)` | `matchBlock.exhaustive` is the closest thing to `@switch`, and it is stricter: it matches on a **discriminant key** of a union and the handler map must cover every member — a missing case is a compile error, which `@switch` cannot give you. ```typescript matchBlock.exhaustive(() => tasksQuery.exceptions().loader, 'code', { TASK_NOT_FOUND: () => p('This task no longer exists.'), TASK_FORBIDDEN: () => p('You do not have access to it.'), }); ``` ### Why not a plain ternary or `switch`? Because a raw TypeScript conditional **collapses**. The template type ends up holding the *result* of the branch, not the fact that a branch existed: ```typescript // works at runtime, but the contract is now opaque tasks.isEmpty() ? p('Nothing to do') : ul(/* … */); ``` `ifBlock` and `matchBlock` keep the condition **and both branches** in the node contract. That is what lets you assert, at compile time, that an element renders *only* when a condition holds, or that a label renders for every item of a non-empty list — see [Type-level tests](/guide/testing/type-level). With a ternary those assertions have nothing to inspect. The renderer also uses the block structure to update surgically instead of rebuilding the subtree. ::: tip When a ternary is fine For a leaf value — a class name, a piece of text, an attribute — a ternary is the right tool. The rule concerns **structure**: whenever a branch decides whether an element exists, reach for `ifBlock` or `matchBlock`. ::: `ifBlock` takes a **named** reactive value as its condition (a primitive ref, or a value marked with `markYieldableValue`), because that name is what the visibility contract records. ## Reusing behaviour across components An insertion factors logic out of a **primitive**. Its counterpart for **components** is a directive: `craftDirective` decorates both a component's logic factory and its template, and you attach it with `.pipe(...)`: ```typescript export const Card = craftComponent( 'Card', {}, (user: Input) => ({ user: deepYieldable(user) }), ({ user }) => div(user.name), ).pipe(InteractivePermissions); ``` The directive can add to the context the template receives — here a `permissions` object the component never had to declare — and directives compose left to right. That is how a tooltip, focus management or interaction analytics get added to several components without any of them knowing about it. The full pattern — writing a directive, what it can require from its host, and how styles compose — is on [Directives and `.pipe(...)`](/guide/components/directives). See also [Customization](/guide/components/customization) for the three layers of component customization, and [Encapsulated styles](/guide/components/styles). ## Every exception a component picks up must be handled If a component's factory — or one of its providers — can raise a `craftException`, that code becomes part of the component's contract. It has to be dealt with, and the compiler is the one that says so: ```typescript export const Restricted = MyComponent.pipe( catchBlock.exhaustive({ NO_ACCESS: () => p('You do not have access to this data.'), }), ); ``` `catchBlock.exhaustive` is the one you want most of the time: it renders a **fallback**. When the failure happens in the factory or a provider — before the template exists — the fallback simply renders alone. Handle it here and the code disappears from the contract. Leave it and it flows up to the route, where `handleExceptions` **must** cover it — a reachable code with no handler doesn't compile, and neither does a handler for a code nothing can produce. ::: warning Where the error actually lands today The compile-time enforcement is at the **route** (`assertExhaustiveRouteExceptions`). The component `.pipe(...)` overload is currently kept permissive to avoid excessive TypeScript instantiation depth, so an unhandled code there is caught by runtime dispatch instead. Practical consequence: a component rendered outside any route gets no compile-time reminder — handle its codes explicitly. The whole rule is on [An unhandled exception doesn't just disappear](/guide/concepts/exceptions). ::: `matchBlock.exhaustive` is the sibling for rendering from an exception *value* or signal. Reach for `catchTag.exhaustive` only when the reaction is pure logic — a toast, a log — and produces no DOM. ## Several insertions at once One insertion function gets crowded fast. Split it and compose with `insertStatePipe`: ```ts import { insertStatePipe, craftComputed, craftService, state } from '@craft-ng/core'; export const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const tasks = yield* state( 'tasks', [] as Task[], insertStatePipe( ({ update }) => ({ add: (title: string) => update((c) => [...c, newTask(title)]), }), ({ state }) => ({ remaining: craftComputed(function* () { return (yield* state()).filter((t) => !t.done).length; }), isEmpty: craftComputed(function* () { return (yield* state()).length === 0; }), }), ), ); return tasks; }, ); ``` Each function in the pipe receives the same context and contributes its own slice. This is what makes behaviour **reusable**: an insertion is just a function, so it can be extracted, parameterised and shared. ::: tip That's what "insertions" are The library ships ready-made ones — storage persistence, optimistic updates, pagination placeholders, forms. They are the exact same shape as the functions you just wrote. See [Insertions](/guide/concepts/insertions). ::: ## What you gained State that carries its own behaviour, a template that only renders, and a composition mechanism that scales past the first three methods. [← 1. Your first state](/learn/01-first-state) [3. Move logic out of the component →](/learn/03-service) --- --- url: https://ng-angular-stack.github.io/craft/learn/03-service.md --- # 3. Move logic out of the component **Goal:** turn your task state into a service other components can use. ## From component factory to `craftService` The factory body moves out almost unchanged — it was already a generator: ```ts import { craftComputed, craftService, state } from '@craft-ng/core'; export const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const tasks = yield* state('tasks', [] as Task[], ({ state, update }) => ({ add: (title: string) => update((current) => [...current, newTask(title)]), toggle: (id: string) => update((current) => current.map((t) => (t.id === id ? { ...t, done: !t.done } : t)), ), remaining: craftComputed(function* () { return (yield* state()).filter((t) => !t.done).length; }), })); return tasks; }, ); ``` A service is the same shape as a component's logic factory: a generator that yields what it needs and returns a context. The only additions are a **name** and a **scope**. ## Using it The component now yields the service instead of declaring the state: ```typescript export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* TaskList(); return { tasks }; }, ({ tasks }) => [ /* unchanged */ ], ); ``` `craftService` returns a helper named after the service — here `TaskList`. There is no `injectTaskList` and no class to import. ## Picking a scope `scope` is the one decision to make. Four you will actually use: | Scope | Instance | Use it when | | ----------- | -------------------------- | ---------------------------------------------------------- | | `function` | fresh on every injection | the service belongs to a single component (**start here**) | | `toProvide` | one per `provideX()` mount | a parent, or a route, shares it with children | | `global` | one for the whole app | genuinely app-wide state | | `abstract` | none — a contract | the implementation is decided elsewhere | Default to `function`. It needs no provider and it says out loud "this instance is not shared". Move to `toProvide` the day a child component needs the *same* instance, and provide it at the component or the route: ```typescript export const Tasks = craftComponent( 'Tasks', { providers: [provideTaskList()] }, function* () { const tasks = yield* TaskList(); return { tasks }; }, ({ tasks }) => [ /* … */ ], ); ``` ::: warning `toProvide` fails at runtime, not compile time Angular does not error when a provider is missing. That is exactly the hole the [route DI check](/learn/09-routing) closes — and that [architecture tests](/guide/testing/architecture#assertroutediproofs) keep in place. ::: The two remaining scopes (`manuallyProvidedAtRoot`, and the details of `abstract`) are covered in [Service scopes](/guide/app/service-scopes). ## Parameterising an instance A service can take **inputs**: the factory's first parameter is an object the call site supplies. Changing inputs are yieldable readers (`CraftServiceInput`) — yield them so the input-to-service edge stays in the graph: ```typescript export const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* (inputs: { projectId: CraftServiceInput }) { const tasks = yield* state('tasks', [] as Task[] /* … */); const projectId = yield* inputs.projectId(); return tasks; }, ); ``` ```typescript const tasks = yield* TaskList({ projectId: currentProjectId }); ``` Inputs are how you get several configured instances out of one `function`-scoped service, instead of duplicating it. ## Giving the service its own providers The service config also takes `providers`, for dependencies that should be scoped to this service rather than to whoever mounts it: ```typescript export const { TaskList } = craftService( { name: 'TaskList', scope: 'function', providers: [provideTaskApi()], }, function* () { const api = yield* TaskApi(); // … }, ); ``` Note this is a different thing from `provideTaskList()`, which is the helper *other* code uses to mount a `toProvide` service. ::: tip There is more to both Inputs interact with the property shortcuts (`X.property()` is deliberately blocked when a service has inputs, so a missing dependency can't hide behind a default — `X.OmitInputs.property()` opts out). Providers can also be declared per primitive, and abstract services turn "who provides this" into a decision of the mounting site. All of it is on [craftService](/guide/app/craft-service) and [Shaping the public API](/guide/app/expose-api) — come back once the tutorial is done. ::: ## Exposing less than everything A service returns whatever it wants to be public. Here `TaskList` returns the whole `tasks` ref. If a consumer only needs one property, it can say so: ```typescript const remaining = yield* TaskList.remaining(); ``` The dependency graph then records that only `remaining` was used — which makes tests smaller, and is why [step 10](/learn/10-testing) is short. ## What you gained Logic that is reusable, injectable and testable, declared as a function with a name and a scope — no `@Injectable`, no constructor. [← 2. Derive instead of duplicate](/learn/02-derive) [4. Compose services →](/learn/04-compose) --- --- url: https://ng-angular-stack.github.io/craft/learn/04-compose.md --- # 4. Compose services **Goal:** understand `yield*` — the one idea the whole library is built on. This is the step that makes everything else obvious. Take your time here. ## The problem `yield*` solves Classic Angular injection hides the dependency graph: ```typescript class TaskList { private api = inject(TaskApi); // invisible from the outside } ``` Nothing in `TaskList`'s type says it needs `TaskApi`. The compiler cannot tell you when you forget to provide it, and a test cannot tell you what to mock. Craft makes the same call **visible in the type**, by yielding it: ```typescript export const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const api = yield* TaskApi(); // ← tracked const tasks = yield* state('tasks', [] as Task[], /* … */); return tasks; }, ); ``` Now `TaskList`'s type carries `TaskApi` as a dependency. Everything downstream — the DI check on routes, the testing register, the dependency snapshot — reads that type. ## Why a generator? A generator is just a function that can hand control back to its caller at each `yield`. Craft uses it as a **collection channel**: each `yield*` reports "I need this" to the runtime driving the factory, which resolves it and folds it into the graph. You don't manage that channel yourself. In practice the whole rule is: > Every named entity yields what it does not own. A factory, a computed, a > method — each one records **its** dependencies with `yield*`. ```typescript const api = yield* TaskApi(); // a service const tasks = yield* state('tasks', []); // a primitive ``` ::: warning A primitive is single-use Each `state(...)` / `query(...)` call produces one generator, consumed exactly once. Don't store one and `yield*` it twice. ::: ## Composing two services ```typescript const { TaskApi } = craftService( { name: 'TaskApi', scope: 'global' }, () => ({ // raw fetch, only to keep this example about composition — // see the note below fetchAll: () => fetch('/api/tasks').then((r) => r.json()), }), ); const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const api = yield* TaskApi(); const tasks = yield* state('tasks', [] as Task[], ({ set }) => ({ // For this demo only; we'll later see why this belongs in a mutation instead. load: function* () { return yield* set(yield* api.fetchAll()); }, })); return tasks; }, ); const { TaskStats } = craftService( { name: 'TaskStats', scope: 'function' }, function* () { const tasks = yield* TaskList(); return { done: craftComputed('done', function* () { return (yield* tasks()).filter((t) => t.done).length; }), }; }, ); ``` Note the factory of `TaskApi` is a plain arrow — a service with no dependencies doesn't need to be a generator. `TaskStats` does not own `TaskList`. The computed yields `tasks` so **that** read is recorded on `done`, not silently closed over from the factory. ::: warning Don't call `fetch` directly in real code It is used here only to keep the example about composition. HTTP goes through **`CraftHttpClient`**, which is yieldable — so the request is tracked like any other dependency, it is mockable at the [browser boundary](/guide/testing/browser-boundaries) in tests, and above all it is what turns a failed response into a typed `craftException` you can handle. A raw `fetch` gives you none of that: no tracking, no boundary, and a rejected promise instead of a declared failure. [Step 5](/learn/05-load-data) uses `CraftHttpClient` for real, and [step 6](/learn/06-mutate-data) shows the exceptions it produces. The `craft-ng/prefer-craft-http-client` ESLint rule flags direct `HttpClient` usage for the same reason. ::: ## Taking only what you need `TaskStats` only reads the array. Say so, and the graph records only that: ```typescript const { TaskStats } = craftService( { name: 'TaskStats', scope: 'function' }, function* () { const fetchAll = yield* TaskApi.fetchAll(); // one property // … }, ); ``` A test for `TaskStats` then has to mock `fetchAll` and nothing else. ## What you gained The mental model: **declare with a name, drive with `yield*`, derive the rest.** Every remaining step is a variation on it — `query` yields, `mutation` yields, guards yield, route providers yield. ::: tip Going deeper `craftGen` lets you write a standalone generator outside a service — useful for guards and route helpers. See [Generators](/guide/concepts/generators). ::: [← 3. Move logic out of the component](/learn/03-service) [5. Load server data →](/learn/05-load-data) --- --- url: https://ng-angular-stack.github.io/craft/learn/05-load-data.md --- # 5. Load server data **Goal:** replace the hand-rolled `load()` from step 4 with `query`, and get loading, error and exception state for free. ## The query primitive ```ts import { CraftHttpClient, craftService, query } from '@craft-ng/core'; export const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const tasksQuery = yield* query('tasksQuery', { // The initial params value immediately triggers the loader. params: () => ({ done: false }), loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/tasks?done=${params.done}`, success: response(), })); }, }); return tasksQuery; }, ); ``` Three things to read here. **`params`** is reactive. When what it returns changes, the loader re-runs. It can be a signal, a function, or a generator that yields other services. **`loader`** is a generator, so it can `yield*` — here `CraftHttpClient`, which is the craft-tracked HTTP client. A plain `async` function works too when there is nothing to yield. **The result** is a ref carrying the full async state: ```typescript tasksQuery.value(); // Task[] | undefined — never throws tasksQuery.isLoading(); // boolean tasksQuery.status(); // 'idle' | 'loading' | 'resolved' | 'exception' tasksQuery.exception(); // craftException | undefined ``` ::: tip `value()` is safe to read in templates and computed signals: it returns `undefined` when the query has no resolved value. ::: ## In the template `ifBlock` / `matchBlock` are the structural conditionals (see [step 2](/learn/02-derive#control-flow-the-angular-equivalents)). For a first pass a ternary chain reads fine — just remember it makes the branch invisible to the [type-level assertions](/guide/testing/type-level): ```typescript import { craftComponent, each, li, p, ul } from '@craft-ng/component'; export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* TaskList(); return { tasks }; }, ({ tasks }) => tasks.isLoading() ? p('Loading…') : tasks.hasException() ? p('Could not load tasks.') : ul( each( () => tasks.value() ?? [], { track: (task) => task.id }, (task) => li(task.title), ), ), ); ``` When the branches depend on an exception **code** rather than a boolean, reach for `matchBlock.exhaustive(...)` — the compiler then checks you covered every code: ```typescript matchBlock.exhaustive(() => tasks.exceptions().loader, 'code', { TASKS_FORBIDDEN: () => p('You do not have access to this list.'), TASKS_NOT_FOUND: () => p('This list no longer exists.'), }); ``` See [Exceptions as values](/guide/concepts/exceptions). ## Triggering it yourself `params` re-runs the loader automatically. When the trigger is a user action instead, use `method`: ```ts import { CraftHttpClient, craftService, query } from '@craft-ng/core'; export const { TaskSearch } = craftService( { name: 'TaskSearch', scope: 'function' }, function* () { const { searchQuery } = yield * query('searchQuery', { method: (term: string) => term, loader: function* ({ params: term }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/tasks?q=${term}`, success: response(), })); }, }); return { searchQuery }; }, ); ``` ## Adding derived values Same insertion mechanism as step 2 — third argument: ```typescript const { tasksQuery } = yield * query( 'tasksQuery', { /* … */ }, ({ value, isLoading }) => ({ count: craftComputed(function* () { return (yield* value())?.length ?? 0; }), isEmpty: craftComputed(function* () { return !(yield* isLoading()) && (yield* value())?.length === 0; }), }), ); yield* tasksQuery.count(); ``` ## About the flicker There isn't one: when `params` change, the previous value stays on screen until the new one resolves. That is the **default**, so paginating never blanks the list. If you actually want the value cleared while loading, opt out explicitly: ```typescript query('tasksQuery', { params: () => ({ page: page() }), preservePreviousValue: () => false, loader: /* … */, }); ``` ## What you gained Server state with the same shape as local state — named, insertable, tracked — and no manual `isLoading` flag. ::: details Beyond the basics Parallel queries per identifier, business exceptions raised from `params`, typed HTTP exception matchers, and reacting to mutations are all on [query](/guide/state/server-state). ::: [← 4. Compose services](/learn/04-compose) [6. Write server data →](/learn/06-mutate-data) --- --- url: https://ng-angular-stack.github.io/craft/learn/06-mutate-data.md --- # 6. Write server data **Goal:** create a task on the server, and make the list update before the request even comes back. ## The mutation primitive `mutation` is `query`'s counterpart for writes. Same shape, triggered explicitly. ```ts import { CraftHttpClient, craftService, mutation } from '@craft-ng/core'; export const { TaskWrites } = craftService( { name: 'TaskWrites', scope: 'function' }, function* () { const { createTask } = yield * mutation('createTask', { method: (payload: { title: string }) => payload, loader: function* ({ params }) { return yield* CraftHttpClient.post(({ response }) => ({ url: '/api/tasks', body: params, success: response(), })); }, }); return { createTask }; }, ); ``` `method` is the entry point: it takes what the caller passes and returns what the loader receives as `params`. It is also where you can reject input before any request happens (see below). ## Making the list react The interesting part is not the mutation, it's wiring it to the query. That's an insertion — `insertReactOnMutation`: ```ts import { CraftHttpClient, craftService, insertReactOnMutation, mutation, query, } from '@craft-ng/core'; export const { TaskSync } = craftService( { name: 'TaskSync', scope: 'function' }, function* () { const { createTask } = yield* mutation('createTask', { method: (payload: { title: string }) => payload, loader: function* ({ params }) { return yield* CraftHttpClient.post(({ response }) => ({ url: '/api/tasks', body: params, success: response(), })); }, }); const { tasksQuery } = yield * query( 'tasksQuery', { params: () => ({ done: false }), loader: function* () { return yield* CraftHttpClient.get(({ response }) => ({ url: '/api/tasks', success: response(), })); }, }, insertReactOnMutation(createTask, { reload: { onMutationSuccess: true }, }), ); return { createTask, tasksQuery }; }, ); ``` The query now reloads itself whenever `createTask` succeeds. No subscription, no event bus, no manual `refetch()` call at the call site. ## Optimistic updates Reloading costs a round-trip. `optimisticPatch` applies the change immediately and reverts it if the mutation fails: ```typescript insertReactOnMutation(renameTask, { optimisticPatch: { title: ({ mutationParams }) => mutationParams.title, }, reload: { onMutationException: true }, }); ``` While `renameTask` is in flight, `tasksQuery.value()` already shows the new title. If it throws, the query reloads to get the truth back. ## Rejecting bad input You rarely want to send a request you know will fail. Return a `craftException` from `method` and the loader never runs: ```typescript import { craftException } from '@craft-ng/core'; const createTask = yield* mutation('createTask', { method: (payload: { title: string }) => payload.title.trim().length === 0 ? craftException({ code: 'TITLE_REQUIRED' }, { received: payload.title }) : payload, loader: /* … */, }); yield* createTask.mutate({ title: ' ' }); createTask.hasException(); // true createTask.exceptions().params?.TITLE_REQUIRED; ``` Note the shape: `exceptions()` is split by **origin** — `params` for what your `method` rejected, `loader` for what the request produced. Both are typed from the codes you declared, so the compiler knows `TITLE_REQUIRED` exists and that `TITLE_TOO_LONG` doesn't. ### Or let a schema do it Hand-written guards get long as soon as there are several fields. Declare a schema instead and the primitive validates the argument for you: ```typescript import { z } from 'zod'; const CreateTaskSchema = z.object({ title: z.string().trim().min(1).max(80), }); const createTask = yield* mutation('createTask', { methodSchema: CreateTaskSchema, method: (payload) => payload, // already validated and typed by the schema loader: /* … */, }); ``` `methodSchema` validates what `mutate(...)` receives, and `method` then gets the schema's **output** value — so a coercion or a `.trim()` in the schema is reflected in the type. Any library implementing `StandardSchemaV1` works — Zod, Valibot, Effect, or a hand-written `{ '~standard': … }` object. None of them becomes a dependency of `@craft-ng`. Queries have the same hooks for their reactive params (`paramsSchema`) and their result (`loaderSchema`). **Use a schema** when the shape itself is the rule, **a `craftException` from `method`** when the rule is business logic — "this title already exists in the current project" is not something a schema can know. See [Schema validation](/guide/state/schema-validation). ::: tip Exceptions as values A craft *exception* is a value you declared and expect to handle. An *error* is the unexpected kind. Keeping the two apart is what makes the exhaustiveness checks later possible — see [Exceptions](/guide/concepts/exceptions). ::: ## What you gained A write path that owns its loading and failure state, and a declarative link between writes and reads. [← 5. Load server data](/learn/05-load-data) [7. Put state in the URL →](/learn/07-url-state) --- --- url: https://ng-angular-stack.github.io/craft/learn/07-url-state.md --- # 7. Put state in the URL **Goal:** make the "show done tasks" filter and the page number survive a refresh and a copy-pasted link — without syncing anything by hand. ## `queryParams` is a state that lives in the URL ```ts import { craftService, queryParams } from '@craft-ng/core'; export const { TaskFilters } = craftService( { name: 'TaskFilters', scope: 'function' }, function* () { const numberCodec = { decode: (value: string) => parseInt(value, 10), encode: (value: number) => String(value), }; const booleanCodec = { decode: (value: string) => value === 'true', encode: (value: boolean) => String(value), }; const filters = yield* queryParams( 'filters', { state: { page: { fallbackValue: 1, codec: numberCodec }, showDone: { fallbackValue: false, codec: booleanCodec }, }, }, ({ set, patch, reset }) => ({ set, patch, reset }), ); return filters; }, ); ``` Reading and writing look like any other state — the URL follows: ```typescript filters(); // { page: 1, showDone: false } filters.page(); // 1 filters.patch({ showDone: true }); // navigates to ?showDone=true filters.reset(); ``` And `?page=3&showDone=true` becomes `{ page: 3, showDone: true }` on load. There is no effect to write, no subscription to the `ActivatedRoute`, no `skipLocationChange` dance. ## Codecs are mandatory, and that's on purpose A URL only holds strings. Every parameter must declare how it converts both ways: ```typescript { fallbackValue: 1, codec: { decode, encode } } ``` `fallbackValue` is what you get when the parameter is absent — so the state type is never `undefined`. The decoded type is your application type; the encoded one is what appears in the URL. It works the same for dates, enums, arrays (`value.split(',')`) and JSON blobs. Codecs are synchronous, because they run inside the reactive URL computation. When a `decode` throws, the parameter keeps its fallback and the failure surfaces instead of corrupting your state: ```typescript if (filters.hasException()) { filters.exceptions().parse.page?.code; // 'QueryParamDecodeError' } ``` ## Feeding the query Now connect it to step 5 — the query's `params` reads the URL state: ```typescript const tasksQuery = yield* query('tasksQuery', { params: () => ({ page: filters.page(), done: filters.showDone() }), loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/tasks?page=${params.page}&done=${params.done}`, success: response(), })); }, }); ``` Clicking "next page" now changes the URL, which re-runs the loader, which re-renders the list. One direction of data flow, and the back button works. ## Custom methods, same as always ```typescript queryParams( 'filters', { /* … */ }, ({ state, patch }) => ({ nextPage: function* () { const current = yield* state(); return yield* patch({ page: current.page + 1 }); }, previousPage: function* () { const current = yield* state(); return yield* patch({ page: current.page - 1 }); }, setPageSize: function* (pageSize: number) { return yield* patch({ pageSize, page: 1 }); }, }), ); ``` ## What you gained Shareable, refresh-proof UI state, with no synchronisation code. ::: details Declaring query params on the route itself `queryParams` can live directly in a `craftRoutes(...)` entry, so the parameters belong to the route rather than to a component, and can then be retrieved through dependency injection. We'll see this after step 9, which introduces routes. See [queryParams](/guide/state/url-state) for the full reference. ::: [← 6. Write server data](/learn/06-mutate-data) [8. Build a form →](/learn/08-forms) --- --- url: https://ng-angular-stack.github.io/craft/learn/08-forms.md --- # 8. Build a form **Goal:** a "new task" form with validation and a typed submit — derived from state, not declared next to it. ## A form is a state There is no `FormBuilder` here. You start from the state you already know, and `insertForm` derives the form from it: ```ts import { craftService, state } from '@craft-ng/core'; import { cRequired, cMaxLength, insertForm, insertFormAttributes, insertNoopTypingAnchor, insertSelectFormTree, } from '@craft-ng/core'; export const { TaskForm } = craftService( { name: 'TaskForm', scope: 'function' }, function* () { const taskForm = yield* state( 'taskForm', { title: '', notes: '' }, insertForm( insertSelectFormTree( 'title', insertNoopTypingAnchor, insertFormAttributes(() => ({ validators: [cRequired(), cMaxLength(80)], })), ), insertSelectFormTree( 'notes', insertNoopTypingAnchor, insertFormAttributes(() => ({ validators: [] })), ), ), ); return taskForm; }, ); ```*the form is this shape, and here is what each field requires.* The field tree, the validity, and the exception types are all derived from the state type — you never restate them. ```typescript const form = taskForm.form(); const title = form.selectTitle(); title()().exceptions.list; // typed list of this field's exceptions title()().exceptions.byValidator['cRequired']; ``` `insertSelectFormTree` is lazy. Calling `selectTitle()` materializes the branch and registers its validators. Use the returned selected field for DOM binding; reading the raw `form.title` field does not activate the branch insertions. ::: warning `insertNoopTypingAnchor` It adds no behaviour. It is a TypeScript anchor that the inference needs to type the field and its exceptions. Every `insertSelectFormTree` needs one — it's a known wart, not a step you can skip. ::: ## Validators Built-ins cover the usual ground: `cRequired`, `cEmail`, `cMin` / `cMax`, `cMinLength` / `cMaxLength`, `cPattern`. Custom ones use `cValidate`, and `cAsyncValidate` for server-side checks. Details on [Validation](/guide/forms/validation). For rules that cover the complete value, add one Standard Schema insertion: ```typescript insertForm( insertFormSchema(taskSchema), /* field insertions */ ); ``` Schema issues are projected onto fields by path. The form keeps its input value; if the schema transforms values, apply that schema again as the mutation's `methodSchema` at submit time. Attributes are derived too, so conditional UI is a function, not an effect: ```typescript insertFormAttributes(() => ({ validators: [cRequired()], disable: () => createTask.isLoading(), hidden: () => !showAdvanced(), })); ``` ## Submitting Submission is wired to the mutation you wrote in step 6 — that is the whole declaration: ```typescript insertFormSubmit(createTask); ``` ```typescript form({ submit: () => taskForm.form().submit() }, [ /* fields */ ]); ``` The form now knows when it is submitting (`form().submitting()`), whether a submit was attempted (`form().hasAttemptedSubmit()`), and — the point — **which exceptions submission can produce**, inferred from the mutation: ```typescript taskForm.form().submitExceptions(); ``` If your mutation declares a `TITLE_ALREADY_EXISTS` exception, that code is in the union. Rename it and the compiler tells you where you were handling it. ## Reshaping submit exceptions Server codes are rarely what the UI wants to show. Refine them in an ordered pipeline: ```typescript insertFormSubmit(createTask, { exceptions: [ ({ omit }) => omit(['TITLE_ALREADY_EXISTS']), ({ submitCraftResource }) => { const clash = submitCraftResource.exceptions()?.loader ?.TITLE_ALREADY_EXISTS; if (!clash) return undefined; return craftException({ code: 'PICK_ANOTHER_TITLE' }, clash.payload); }, ], }); ``` Returning an array replaces the list; returning one exception appends it. ::: warning `success` is not a "then" callback The config also accepts `success`, but it runs **inside the derivation of the submit exception list** and its return value is appended to that list. It exists to raise an exception the server reported with a 200 — not to run side effects. Resetting the form, navigating or showing a toast from there means mutating state inside a computation, and it re-runs whenever the exceptions recompute. Drive those from your own code after `submit()`, or from the mutation. ::: ## What you gained A form whose validity, field tree and error types are consequences of your state and your mutation — so they cannot drift out of sync with them. ::: details Nested and parallel forms Sub-forms with `insertSubFormField`, several independent forms over the same state, and the full validator reference are on [Forms](/guide/forms/). ::: [← 7. Put state in the URL](/learn/07-url-state) [9. Wire up routing →](/learn/09-routing) --- --- url: https://ng-angular-stack.github.io/craft/learn/09-routing.md --- # 9. Wire up routing **Goal:** put the tasks page behind a route, and make a missing provider a **compile error** instead of a blank screen. The headline is this: **navigation only accepts routes that exist**. Not a `string` you hope is right — a value checked against the paths your app actually declares. A typo, a removed route, a missing param: all compile errors, at the call site. This is where the dev tooling earns its keep. ## Declare the route A Craft component is mounted with `loadCraftComponent(...)`, spread into the route: ```ts import { loadCraftComponent } from '@craft-ng/component'; import { craftRoutes } from '@craft-ng/core'; export const { appRoutes } = craftRoutes('app', [ { path: 'tasks', ...loadCraftComponent(({ withRetry }) => withRetry(import('./tasks/tasks')).then( ({ default: component }) => component, ), ), }, ]); ``` `withRetry` wraps the dynamic import, so a chunk that fails to download is retried instead of dead-ending the navigation. Keep the import specifier literal — a computed one can't be statically discovered by the bundler. ## Register the paths Declaring the collection's paths is what makes navigation type-safe across the app: ```typescript declare module '@craft-ng/core' { interface CraftRouterRoutesRegistry { App: typeof appRoutes.META_PATHS; } } ``` From here on, every navigation target is checked against that registry. ## Navigating Two ways, both checked against the registry above. **As a link**, with the `CraftRouterLink` directive: ```ts import { a, craftComponent } from '@craft-ng/component'; import { CraftRouterLink } from '@craft-ng/core'; export const TasksLink = craftComponent( 'TasksLink', {}, () => ({}), () => a({ craftRouterLink: { to: 'tasks' } }, 'Tasks').pipe(CraftRouterLink), ); ``` **Imperatively**, by yielding the router: ```ts import { craftComponent } from '@craft-ng/component'; import { CraftRouter, craftMethod } from '@craft-ng/core'; export const TaskOpener = craftComponent( 'TaskOpener', {}, function* () { const router = yield* CraftRouter(undefined, ({ navigate }) => ({ navigate })); const goToTask = craftMethod('goToTask', function* (taskId: string) { void router.navigate({ to: 'tasks/:taskId', params: { taskId } }); }); return { goToTask }; }, () => [], ); ``` The target is `{ to, params?, queryParams? }`, and all of it is checked: ```typescript router.navigate({ to: 'taks' }); // ✗ not a known path router.navigate({ to: 'tasks/:taskId' }); // ✗ params.taskId is missing router.navigate({ to: 'tasks/:taskId', params: { id: '1' } }); // ✗ wrong param router.navigate({ to: 'tasks/:taskId', params: { taskId: '1' } }); // ✓ ``` Note that `navigate` comes from **yielding** `CraftRouter`, not from injecting it — so the dependency is tracked and the route check can see it. ## The check that pays for all of this Each route component gets its own check: `RouteCheckedDI` compares what the component needs against what is actually available at that path, and `CanRun` turns a mismatch into a TypeScript error. The `tasks` route created above remains visible as the source of truth; the check below validates that route's component and its `path: 'tasks'` context. An AI can also create this Craft NG routing boilerplate very well — including the lazy import, retry handling, route registry and DI check — from the component and path you provide. Declare one local alias for your app's context, then one `CanRun` per route: ```ts import type { ActivatedRoute, Router } from '@angular/router'; import type { CanRun, ComponentDepsOf, RouteCheckedDI } from '@craft-ng/core'; type AppRouteCheckedDI< Component, RouteInputs extends string = never, Context extends string = 'app route component', > = RouteCheckedDI< ComponentDepsOf, 'CraftRouter', Router | ActivatedRoute, Context, RouteInputs >; type _CanRunTasks = CanRun< AppRouteCheckedDI< (typeof import('./tasks/tasks'))['default'], never, 'path: "tasks"' > >; ``` The alias fixes the ambient context once — what the app provides by name (`'CraftRouter'`) and by value (`Router | ActivatedRoute`). Each route then supplies three things: the component, the **route inputs** it may bind (a path param like `'taskId'`, or `never`), and a label used in error messages. A mismatch reads like this: ``` The TaskList service is not provided in path: "tasks" Input "taskId" is not provided in path: "tasks" ``` Remember step 3, where `toProvide` was flagged as failing only at runtime? This is what closes that hole — **provided the proof stays in the file**. A `CanRun` alias that nobody references still compiles; [architecture tests](/guide/testing/architecture#assertroutediproofs) are what turn omitting it into a failing suite. ::: tip Why one check per route `RouteCheckedDI` validates a single component with no recursion between routes, so the cost is flat: a file with two hundred routes costs two hundred independent checks and never hits TypeScript's instantiation ceiling. See [Scaling routes](/guide/routing/scaling). ::: ## Prove the exceptions are handled Guards, matchers and resolvers can raise a `craftException` — and so can a **component's own factory or providers**, whose unhandled codes flow up into the route. One call asserts that every reachable code has a handler, and that no handler exists for a code nothing produces: ```typescript assertExhaustiveRouteExceptions(appRoutes); ``` The ESLint rule `craft-ng/require-assert-exhaustive-route-exceptions` adds it for you. A component can also handle its own codes with `.pipe(catchTag.exhaustive(...))`, which removes them from the route's union — see [An unhandled exception doesn't just disappear](/guide/concepts/exceptions). Everything else is on [Route exception handling](/guide/routing/exception-handling). ## Wire it into the app ```ts import { craftAppConfig } from '@craft-ng/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; export const appConfig = craftAppConfig({ routingDeps: appRoutes.META_DATA, providers: [provideRouter(appRoutes.toRoutes(), withComponentInputBinding())], }); ``` `toRoutes()` hands Angular the real routes; `META_DATA` hands the compile-time graph to `craftAppConfig`. ## Make the DI contract enforceable The proofs above look ceremonial: unused type aliases, a `CanRun` wrapper, a cascade that does not descend into `loadChildren`, a separate check for pending and error screens, another for `app.config`. Each piece is small; omitting one is silent. TypeScript still compiles. Architecture tests collapse that checklist into a single assertion. `assertRouteDiProofs` walks the static graph and fails unless every routed component — including lazy child collections — and every `craftAppConfig` error screen is hooked to an armed mapper. TypeScript still judges whether a dependency is provided; the architecture suite judges whether that judgement was invoked. Add it next to `e2e/`, in `architecture/`, then run it in CI. Full setup: [Architecture rules](/guide/testing/architecture). ## What the user sees while a route loads A guard or a resolver that does real work leaves the app frozen on the previous page. Swap `provideRouter` for `provideCraftRouter` and render `CraftRouterOutlet()` instead of ``, and the URL commits immediately while the chain runs behind it: ```ts import { craftAppConfig, provideCraftRouter, withTransitionTimings } from '@craft-ng/core'; import { withComponentInputBinding } from '@angular/router'; export const appConfig = craftAppConfig({ routingDeps: appRoutes.META_DATA, providers: [ provideCraftRouter( appRoutes.toRoutes(), withComponentInputBinding(), withTransitionTimings({ stayMs: 300, blankMs: 300, pendingMinMs: 500 }), ), ], }); ``` Those three numbers are the whole waiting story, and they exist so a fast navigation shows **nothing at all**: | Phase | What is on screen | | ----------------------------------- | ------------------------------------------------- | | `0 → stayMs` | the previous page — most navigations resolve here | | `stayMs → +blankMs` | a blank surface: something is coming | | beyond, for `pendingMinMs` at least | the pending component (a spinner, a skeleton) | `pendingMinMs` is the anti-flicker floor: once the loader appears it stays put, so it can't flash for 40ms. ### Changing the pending component The default spinner is replaceable globally: ```typescript provideCraftRouter( appRoutes.toRoutes(), withPendingComponent(MyBrandedSpinner), ); ``` …or per route, which is where it gets interesting — a skeleton shaped like the page it is standing in for reads far better than a spinner: ```typescript { path: 'tasks', ...loadCraftComponent(/* … */), pendingComponent: () => import('./tasks/tasks-skeleton'), stayMs: 150, // this route is slower: get to the skeleton sooner blankMs: 0, // and skip the blank phase entirely } ``` Route-level values override the global ones, so you tune only the routes that need it. ::: tip See it running The `slow-page` demo exists for exactly this: two deliberately slow steps (~1.5s each) so you can watch the stay → blank → loader phases play out. The first visit is slow, a revisit is instant thanks to the query cache, and a "clear cache" button replays it. Source: [slow-page.routes.ts](https://github.com/ng-angular-stack/ng-craft/blob/main/apps/demo/src/app/examples/routes/slow-page/slow-page.routes.ts). Full details — the phase diagram, per-route overrides, view transitions and the DI check on skeletons — are on [Non-blocking navigation](/guide/routing/pending-ui). ::: ## Let the CLI write it Hand-writing these pieces gets old. The CLI does it for you and the output stays ordinary, editable TypeScript: ```shell npx craft route add /tasks --create-component tasks/tasks ``` It picks the right collection, creates a lazy routes file per feature, adds the loader, the check block and the registry entry, then runs ESLint and `tsc`. Use `--dry-run` first. ## What you gained Routing where a forgotten provider, a misspelled input, an unhandled exception or a route pointing at nothing stops the build instead of reaching production — and architecture tests keep those proofs from quietly disappearing. ::: details The parts you'll want later Route-scoped providers, guards as bare generators and centralised exception handling all live under [Routing](/guide/routing/setup). Splitting a growing collection across lazy child files is [Scaling routes](/guide/routing/scaling). Architecture tests that keep the DI proofs armed are [Architecture rules](/guide/testing/architecture). ::: [← 8. Build a form](/learn/08-forms) [10. Test what you wrote →](/learn/10-testing) --- --- url: https://ng-angular-stack.github.io/craft/learn/10-testing.md --- # 10. Test what you wrote **Goal:** test `TaskList` and the `Tasks` component without guessing what to mock — the dependency graph tells you. ## The idea Most test setups let you forget a dependency and find out at runtime. Craft inverts it: you pass a **register** covering the whole graph, and the compiler refuses to run the test until every node is accounted for. Each node is one of four things: `'real'`, its own `provideX(...)`, a mock object, or `'notReached'`. ## Testing a service Here is the service under test — the one from [step 4](/learn/04-compose), with its scope changed to `toProvide` so it has a `provideTaskStats()` to mount in the test: ```ts import { craftComputed, craftService, state } from '@craft-ng/core'; export const { TaskStats, provideTaskStats } = craftService( { name: 'TaskStats', scope: 'toProvide' }, function* () { const tasks = yield* TaskList(); return { done: craftComputed('done', function* () { return (yield* tasks()).filter((task) => task.done).length; }), }; }, ); ``` It depends on one thing, `TaskList`, and exposes one thing, `done`. The test mirrors that exactly: ```ts const { sut, mocks } = await setupCraftServiceTestingByRegister(TaskStats, { // the SUT itself, mounted through its own provider TaskStats: provideTaskStats(), // its only dependency, replaced by a mock TaskList: { $self: vi.fn(function* () { return [ { id: '1', title: 'a', done: true }, { id: '2', title: 'b', done: false }, ]; }), }, }); expect(craftUse(sut.done())).toBe(1); expect(mocks.TaskList).toBeDefined(); ``` `sut` is the service under test; `mocks` gives you back the mocks you supplied, already typed, so `mocks.TaskList.$self` is assertable. ::: tip Which register entry to use `provideX()` for a `toProvide` or `manuallyProvidedAtRoot` service, `'real'` for a reachable `global` or `function` one, a plain object to mock it, and `'notReached'` for a branch this test never touches. ::: `$self` is the service's own returned value — the ref itself, as opposed to a property hanging off it. ## Why the register is small Because of step 4. `TaskStats` yielded only what it needed, so the register only asks for that. Had it yielded the whole `TaskApi`, the register would demand `TaskApi` too. **Precise yields make short tests** — that's the payoff for the `yield*` discipline. ## Testing a component Here is the component under test, from steps 2 and 3 — a factory that yields `TaskList`, and a template that renders it: ```ts import { craftComponent, each, h1, li, ul } from '@craft-ng/component'; export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* TaskList(); return { tasks }; }, ({ tasks }) => [ h1(function* () { return `Tasks — ${yield* tasks.remaining()} left`; }), ul( each( tasks, { track: (task) => task.id }, (task) => li(task.title), ), ), ], ); ``` Those two halves are tested **independently**: the factory produces a context without touching the DOM, and the template renders a context without running the factory. The logic test runs the factory only — no DOM: ```ts const { context, mocks, destroy } = await setupCraftComponentLogicTest.byRegister(Tasks, { register: { TaskList: { $self: () => [{ id: '1', title: 'a', done: false }], remaining: () => 1, }, }, }); expect(context.tasks.remaining()).toBe(1); destroy(); ``` The template test does the opposite — it renders with a context you hand it, and never runs the factory: ```ts const test = await setupCraftComponentTemplateTest.byRegister(Tasks, { context: { tasks: Object.assign( function* () { return [{ id: '1', title: 'Write tests', done: false }]; }, { remaining: function* () { return 1; }, add: () => undefined, toggle: () => undefined, remove: () => undefined, }, ), }, register: {}, }); expect(test.nativeElement.textContent).toContain('Tasks — 1 left'); test.destroy(); ``` That separation is why component tests stay fast: you only pay for the DOM when the DOM is what you're asserting on. ## Finding elements Template tests expose `locator(tag, criteria)` rather than raw CSS selectors: ```typescript const removeButton = test.locator('button', { 'data-testid': 'remove' }); removeButton?.click(); ``` ## Proving it at the type level Some of what craft guarantees isn't observable at runtime at all — it's in the types. Those get their own kind of test, resolved by the compiler with no `TestBed`, no DOM and no factory: ```typescript type TasksTemplateTest = SetupTestComponentTemplate; ``` The resolver walks elements, directives, `each`, `defer` and child components, and a child missing from the tuple becomes a type diagnostic. Companion assertions — `TemplateHasElement`, `TemplateHasElementWithProps`, `TemplateHasYieldableEvent`, `TemplateRendersStateWhen` — check that the template really renders what you think, including event argument types. This is how you pin down a template contract that a runtime test would only catch by accident. Full reference: [Type-level tests](/guide/testing/type-level). ## Tests that stay close to reality Mocking everything makes tests that pass while the app is broken. `boundaryOnly` keeps the real graph and lets you replace only what actually touches the outside world — the services marked `browserBoundary: true` (HTTP, storage, location): ```typescript const { sut } = await setupCraftServiceTestingByRegister(TaskList, register, { boundaryOnly: true, }); ``` Everything in between runs for real. See [Browser boundaries](/guide/testing/browser-boundaries). ## Architecture of the whole app The register proves one service's graph is complete. Architecture rules prove invariants **across** services: this feature must not depend on that one, this HTTP endpoint is owned once, this `craftUnique` storage key appears once. They live next to `e2e/`, analyze TypeScript without booting Angular, and are ordinary Vitest assertions on a typed graph. Look a node up, walk its edges, assert. A precise rule — HTTP may only be called from a `browserBoundary` service — is an `it()`: ```typescript it('only browser-boundary services call HTTP', () => { const boundaryIds = new Set( graph.services({ browserBoundary: true }).map((node) => node.id), ); const leaked = graph .usingHttp() .filter((node) => node.kind === 'service' && !boundaryIds.has(node.id)); expect(leaked.map((node) => node.label)).toEqual([]); }); ``` Anything you can see on the graph is a rule you can write: folder lanes, exclusive feature branches, a method that must not both be called and write a `source$`. Built-in helpers cover unique `craftUnique` identities, unique HTTP verb+URL, pure `craftComputed`, no `depends-on` cycles, `assertPathBoundaries`, `noExclusiveLink`, `assertMutationHasReactOn`, `assertPersistedPrimitiveHasUnique`, `assertInsertSelectUnique`, `assertCraftEffectNoNetwork`, `assertCraftEffectNoImperativeSync`, and the route DI proofs from [step 9](/learn/09-routing). Those proofs (`CanRun`, `RouteCheckedDI`) are unused type aliases — omit one and the project still compiles. `assertRouteDiProofs` fails the suite unless every routed component and every `app.config` error screen stays hooked to an armed mapper. TypeScript still judges injection; the architecture suite judges whether that judgement was invoked. Full setup: [Architecture rules](/guide/testing/architecture). Why that graph is not Nx's project graph: [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx). The demo app already imports the helpers. From the repository root: ```shell npx nx architecture demo ``` ## What you gained Tests whose setup is derived from the real dependency graph, so "I forgot to mock that" becomes a compile error — and architecture rules on that same graph, so the app can be taught its boundaries. [← 9. Wire up routing](/learn/09-routing) [Where to go next →](/learn/next) --- --- url: https://ng-angular-stack.github.io/craft/learn/next.md --- # Where to go next You have the whole mental model: **declare with a name, yield what you do not own, derive the rest.** Everything below is a variation on it. ## Fill the gaps in what you built | You have | Next thing worth adding | | ----------------------- | ------------------------------------------------------------------------------------ | | A query and a mutation | [Persistence](/guide/state/persistence) — storage persistence as an insertion (localStorage by default) | | A list | [Collections](/guide/state/collections) — entity storage, selectors, updates | | A form | [Validation](/guide/forms/validation) — custom and async validators | | Routes | [Route guards](/guide/routing/guards) and [Route providers](/guide/routing/route-providers) | | A running app | [Non-blocking navigation](/guide/routing/pending-ui) — pending UI instead of a freeze | ## Concepts worth a dedicated read * [The mental model](/guide/concepts/mental-model) — the design principles behind the API you just used * [Exceptions as values](/guide/concepts/exceptions) — declared failures, exhaustively handled * [Insertions](/guide/concepts/insertions) — writing your own and composing them * [Typed insertion pipes](/guide/concepts/insertion-pipes) — readable composition for each primitive * [Generators](/guide/concepts/generators) — `craftGen` outside a service ## Teach the app its boundaries The graph you just tested is also a map you can constrain. [Architecture rules](/guide/testing/architecture) are ordinary Vitest assertions on the static Craft graph: unique identities, unique HTTP, pure `craftComputed`, folder lanes, exclusive feature branches — and any neighbourhood you can look up is a rule you can write. Nx still owns the workspace graph (imports, affected, cache); Craft judges the app. [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx) is the split. Setup is in that guide. The demo suite already runs them: ```shell npx nx architecture demo ``` ## When your app grows * [Service scopes](/guide/app/service-scopes) — when `function` stops being enough * [Scaling routes](/guide/routing/scaling) — splitting collections before TypeScript's instantiation ceiling bites * [Lazy services](/guide/app/lazy-services) and [App start](/guide/app/app-start) * [Observability](/guide/advanced/observability) — logging and tracing that follow the dependency graph ## Reference Looking for one symbol? The [API index](/reference/) lists every export with a one-line description and a link. ## See it running [Examples](/resources/examples) points at the demo application, which exercises most of the above end to end. Importing Craft into an app that an agent will edit? Point it at [coding agents](/resources/ai-agents) — `llms.txt`, the `@craft-ng/mcp` server, and the Agent Skills. [← 10. Test what you wrote](/learn/10-testing) --- --- url: https://ng-angular-stack.github.io/craft/guide.md --- # Guide The guide is organised by **what you are trying to do**. If you are starting out, the [Learn path](/learn/) is a better entry point — it introduces the same material one idea at a time. ## Start here Four pages carry most of the weight. Reading them in this order is worth an afternoon: 1. [The mental model](/guide/concepts/mental-model) — the principles the API follows, and [what craft adds to Angular](/guide/concepts/vs-angular) if you want the inventory first 2. [Which primitive should I use?](/guide/concepts/choose-primitive) — the five-way decision you make constantly 3. [Anatomy of a primitive](/guide/concepts/primitive-anatomy) — the shape all five share 4. [Generators and `yield*`](/guide/concepts/generators) — the tracking channel everything is built on 5. [Insertions](/guide/concepts/insertions) — how behaviour is composed ## By topic ### Managing state [Local state](/guide/state/local-state) · [query](/guide/state/server-state) · [Mutations](/guide/state/mutations) · [queryParams](/guide/state/url-state) · [asyncProcess](/guide/state/async-process) · [Collections](/guide/state/collections) · [Persistence](/guide/state/persistence) · [Selecting](/guide/state/select) · [Reacting to mutations](/guide/state/react-on-mutation) · [Schema validation](/guide/state/schema-validation) ### Structuring the app [craftService](/guide/app/craft-service) · [Service scopes](/guide/app/service-scopes) · [Shaping the public API](/guide/app/expose-api) · [Abstract services](/guide/app/abstract-services) · [Integrating existing Angular code](/guide/app/integrate-existing) · [App start](/guide/app/app-start) · [Lazy services](/guide/app/lazy-services) ### Recommended approaches [Inject at the point of use](/guide/patterns/inject-at-point-of-use) ### Routing and type-safe DI [Setup](/guide/routing/setup) · [CLI automation](/guide/routing/automation) · [ESLint rules](/guide/routing/eslint-rules) · [Route providers](/guide/routing/route-providers) · [Guards](/guide/routing/guards) · [Exception handling](/guide/routing/exception-handling) · [Pending UI](/guide/routing/pending-ui) · [Route load errors](/guide/routing/route-load-errors) · [Scaling routes](/guide/routing/scaling) ### Components and templates [Components](/guide/components/) · [Fine-grained reactivity](/guide/components/fine-grained-reactivity) · [Directives and `.pipe(...)`](/guide/components/directives) · [Customization](/guide/components/customization) · [Content projection](/guide/components/content-projection) · [Encapsulated styles](/guide/components/styles) · [Accessibilité](/guide/components/accessibility) ### Forms [Overview](/guide/forms/) · [Validators](/guide/forms/validation) · [Submitting](/guide/forms/submit) · [Nested forms](/guide/forms/nested) ### Testing [Services](/guide/testing/services) · [Components](/guide/testing/components) · [Type-level tests](/guide/testing/type-level) · [Browser boundaries](/guide/testing/browser-boundaries) · [Architecture rules](/guide/testing/architecture) · [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx) ### Reactivity utilities [craftComputed](/guide/reactivity/craft-computed) · [craftEffect](/guide/reactivity/craft-effect) · [craftMethod](/guide/reactivity/craft-method) · [source$](/guide/reactivity/source) · [on$](/guide/reactivity/on) ### Going further [Program operators](/guide/advanced/program-operators) · [Pattern matching](/guide/advanced/pattern-matching) · [Observability](/guide/advanced/observability) · [Coding agents](/resources/ai-agents) ## Looking for one symbol? The [API index](/reference/) lists every export with a one-line description. --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/mental-model.md --- # The mental model Three words describe everything `@craft-ng` does: **declare, yield, derive.** You declare state with a name. Every named entity — a factory, a computed, a method — pulls in with `yield*` what it does not own, so the compiler can see that entity's dependencies. Everything else — validity, loading flags, form trees, error unions — is derived rather than restated. This page is the *why*. If you want the *how*, the [Learn path](/learn/) walks the same ideas through a working app. ## Declare State is declared where it is used, close to the component or service that owns it, with a name that the tooling can see: ```typescript const counter = yield* state('counter', 0, ({ update }) => ({ increment: () => update((value) => value + 1), })); ``` The name isn't a label — it tags the injector (`state:counter`) and is how the primitive shows up in logs, snapshots and observability. Five primitives cover every home a value can have: memory, server (read and write), URL, and async action. They share one shape, so learning one teaches the other four — see [Anatomy of a primitive](/guide/concepts/primitive-anatomy). ## Yield Classic injection hides the dependency graph. `inject(TaskApi)` is invisible from the outside, so the compiler cannot tell you when a provider is missing, and a test cannot tell you what to mock. Yielding makes the same call visible **in the type** of the entity that yielded it: ```typescript const api = yield* TaskApi(); ``` A factory that yields `TaskApi` has `TaskApi` in its graph. A computed that reads another primitive must yield that primitive too — otherwise the dependency is invisible on the computed, even if the surrounding factory already yielded it: ```typescript const doneCount = craftComputed('doneCount', function* () { return (yield* tasks()).filter((task) => task.done).length; }); ``` `tasks` does not belong to `doneCount`. Closing over `tasks()` would read the value and skip the graph. Everything downstream — the route DI check, the testing register, the dependency snapshot — reads the type of **that** entity. And because you can yield *part* of a service — `yield* TaskApi.fetchAll()` — the graph records only what you actually used, which is what keeps test setups small. That is the whole trade: one keyword, in exchange for a dependency graph the compiler can check. See [Generators and `yield*`](/guide/concepts/generators). ## Derive The third principle is the one that removes the most code: **if a value is a function of another value, don't store it — derive it.** * Derived values are `craftComputed`, inside an insertion, and they `yield*` the readers they depend on. * Loading and error state is derived by the async primitives, not tracked by hand. * A form's field tree, validity and error types are derived from its state and its mutation ([Forms](/guide/forms/)). * Route exceptions are derived into a union the compiler forces you to handle exhaustively ([Exceptions](/guide/concepts/exceptions)). The payoff is that derived things cannot drift out of sync with their source. The cost is that you have to resist keeping a second copy "just for the template". ## What follows from this ### Composition instead of configuration Behaviour is added by **insertions** — plain functions that receive a primitive's internals and return what to expose. Storage persistence, optimistic updates and forms are all the same shape as one you'd write yourself. Storage persistence uses the backend selected through DI: ```typescript const { myState } = state( 'myState', 0, insertStoragePersister(craftUnique({ storeName: 'myStore', key: 'myState', })), ); const { myQuery } = query( 'myQuery', { params: () => 1, loader: /* … */ }, insertStoragePersister(craftUnique({ storeName: 'myStore', key: 'myUserQuery', })), ); ``` Compose several with the primitive-specific helpers in [Typed insertion pipes](/guide/concepts/insertion-pipes). Keep [`craftPipe`](/guide/concepts/insertions) for universal or nested compositions. ### Methods or events, your choice A method can be called directly, or bound to a source and driven by an event. Both coexist in the same declaration: ```typescript const resetSource$ = source$('resetSource$'); const counter = yield* state('counter', 0, ({ set, update }) => ({ increment: () => update((v) => v + 1), // called reset: on$(resetSource$, () => set(0)), // driven by an event, not exposed })); ``` This is what makes one `resetSource$.emit()` reset several independent states at once, without any of them knowing about the others: ```typescript const search = yield* state('search', '', ({ set }) => ({ set, reset: on$(resetSource$, () => set('')), })); const page = yield* state('page', 1, ({ set, update }) => ({ increment: () => update((v) => v + 1), reset: on$(resetSource$, () => set(1)), })); ``` See [`on$`](/guide/reactivity/on). ### Granular state, granular tests Small, focused states isolate change. Combined with partial yields, a consumer depends on exactly what it reads — and a test provides exactly that, no more. ### Services as functions A service is a factory with a name and a scope, not a class: [`craftService`](/guide/app/craft-service) for the ones you write, [`toCraftService`](/guide/app/integrate-existing) for existing Angular dependencies. Both participate in the same typed composition and the same testing workflow. ```typescript const { UserProfile } = craftService( { name: 'UserProfile', scope: 'global' }, function* () { const api = yield* UserApi(); const userId = yield* state('userId', '5', ({ set }) => ({ set })); const updateEmail = yield* mutation('updateEmail', { method: (payload: { id: string; email: string }) => payload, loader: function* ({ params }) { return yield* api.updateEmail(params); }, }); const user = yield* query( 'user', { params: userId, loader: function* ({ params }) { return yield* api.getUser(params); }, }, insertReactOnMutation(updateEmail, { optimisticPatch: { email: ({ mutationParams }) => mutationParams.email }, reload: { onMutationException: true }, }), ); return { userId, user, updateEmail }; }, ); ``` ### Signals, not RxJS 100% signal-based. RxJS is optional and only appears where you ask for it. ### Declarative code is legible code The three principles add up to something that is rarely stated outright: the app becomes **declared data** rather than control flow to be reconstructed. Two consequences follow, and both are worth more than they look. **Observability stops being instrumentation.** Because every dependency resolution and every crafted function passes through one system, that system is where you wrap them — structured logs through a yieldable `Console`, correlation ids across the graph, per-service timing, snapshots of the live dependency tree — with no change to business code. Retrofitting the same thing onto imperative code means touching every call site. See [Observability](/guide/advanced/observability). **And what a tool can read, a tool can help with.** The dependency graph, the reachable exceptions and the route contract are all declared, so an agent — or a future WebMCP-style integration — can reason about the app without inferring it from execution. The same property that makes the compiler able to check your providers makes the codebase tractable to something that isn't you. ### Exceptions are values, errors are surprises A craft *exception* is a failure you declared and expect to handle; an *error* is the unexpected kind. Keeping them apart is what allows the compiler to check that you handled every declared case. This is **error-as-value**: a declared failure is *returned*, not thrown, so it propagates through types rather than escaping through the stack. A `try/catch` tells you nothing about what it might catch; a returned `craftException` carries its code and payload all the way to whoever handles it — and the compiler knows if nobody does. See [Exceptions as values](/guide/concepts/exceptions). ## See Also * [What craft adds to Angular](/guide/concepts/vs-angular) — the full inventory * [Which primitive should I use?](/guide/concepts/choose-primitive) * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) * [Learn: the guided path](/learn/) --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/vs-angular.md --- # What craft adds to Angular Craft is not a replacement for Angular — it runs on Angular's DI, signals and router. This page is the honest list of what you get **on top**, and what it costs. If you want the reasoning rather than the inventory, read [The mental model](/guide/concepts/mental-model). ## The compiler catches more | | Angular | Craft | | --- | --- | --- | | Missing provider | runtime `NullInjectorError` | **compile error** at the route | | Missing / misnamed component input | runtime, or silently `undefined` | **compile error** at the call site | | Navigating to a route that doesn't exist | runtime 404 | **compile error** | | Missing route param | runtime | **compile error** | | Unhandled declared failure | nothing — you find out in production | **compile error** (exhaustiveness) | | Handling a failure that can't happen | dead code nobody notices | **compile error** | | Forgetting to mock a dependency in a test | test passes for the wrong reason | **compile error** | | A template stops rendering an element | silent | **compile error**, if asserted | That column is the whole point. Everything below exists to make it possible. ## Dependencies are visible in the type `inject(X)` hides the graph: nothing in a consumer's type says `X` is needed. `yield* X()` puts it in the type, which is what lets the route check, the test register and the dependency snapshot all read the same source of truth. You can also yield **part** of a service — `yield* X.someProperty()` — so the graph records exactly what you used, and a test provides exactly that. ## State primitives, not patterns Angular gives you `signal`, `computed`, `resource` and leaves the composition to you. Craft ships five primitives with one shape — [`state`, `query`, `mutation`, `queryParams`, `asyncProcess`](/guide/concepts/choose-primitive) — each carrying its own status, exceptions and derived values. * **URL state as a primitive.** `queryParams` makes the query string the source of truth, with typed codecs. No `ActivatedRoute` subscription, no synchronisation effect. * **Declarative read/write links.** `insertReactOnMutation` connects a mutation to the queries it invalidates, including optimistic updates with automatic rollback — instead of calling `refetch()` from the call site. * **Composable behaviour.** Insertions (storage persistence, pagination placeholders, entity collections) are plain functions of the same shape as the ones you write. ## Services are functions A `craftService` is a factory with a name and a scope: no class, no decorator, no constructor. Scopes are explicit (`function`, `toProvide`, `global`, `manuallyProvidedAtRoot`, `abstract`) rather than inferred from where you happened to put `providedIn`. `abstract` services turn "who implements this" into a decision of the mounting site — a route, a feature config, a test — with the compiler enforcing that someone did. ## Components are functions * Inputs and outputs are **factory parameters**, so binding errors are type errors. * Templates are TypeScript: `each`, `ifBlock`, `matchBlock`, `defer` instead of `@for` / `@if` / `@switch` / `@defer`. `matchBlock.exhaustive` is stricter than `@switch` — a missing case doesn't compile. * No host element is inserted. Styles are scoped with CSS `@scope`, `:scope` being the component's own root. * **Directives compose with `.pipe(...)`** and decorate *both* the logic factory and the template, so behaviour and markup travel together. * **Content projection is a rendering context**, not a component category, with DOM contracts (`RequiredContent`) checked statically. ## Exceptions are values A declared failure is a `craftException` **returned**, not thrown — so it flows through types instead of escaping through the stack. * Read them by origin: `exceptions().params` (rejected before the request) vs `exceptions().loader` (produced by it). * A component's unhandled codes flow up into its route's union. * `assertExhaustiveRouteExceptions(routes)` proves every reachable code is handled, and that no handler is dead. Errors — the unexpected kind — stay separate and reach the global error component. See [Exceptions as values](/guide/concepts/exceptions). ## Routing * Type-safe navigation, params and query params. * **Non-blocking navigation**: the URL commits immediately and a pending component appears only if the wait is real, with a stay → blank → loader timeline you configure. * Route-scoped providers built from the route's **own** params, data and guarded values. * `withRetry` on lazy chunks, plus a dedicated route-load-error path for a stale deploy or a flaky network — the failure mode where Angular gives you a dead navigation. * Guards are bare generators, composable and parameterisable. ## Testing * Registers derived from the real dependency graph: forgetting a mock is a compile error. * `boundaryOnly` keeps the whole app graph real and mocks only what touches the platform, so a passing test means something. * Components split into a logic test (no DOM) and a template test (no factory). * [Type-level tests](/guide/testing/type-level) assert the template contract itself — that an element renders only under a condition, that a binding is the one you think. * [Architecture rules](/guide/testing/architecture) assert the shape of the app on the static Craft graph — exclusive features, unique HTTP, `craftUnique`, and armed route DI proofs. That graph is not Nx's project graph; see [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx). ## Because it's declarative, you get observability for free This one is a consequence rather than a feature. Because every dependency resolution, every primitive and every crafted function goes through one system, that system is also where you can wrap them all: * structured logging through a yieldable `Console` you can override once; * correlation ids propagated across the graph; * app snapshots of the live dependency tree; * per-service timing and tracing, via `provideServiceYieldWrapper` / `provideFnWrapper`, without touching business code. Retrofitting that onto imperative code means instrumenting every call site. Here it is one provider. See [Observability](/guide/advanced/observability). The same property is what makes an app **legible to a machine** — an agent, or a future WebMCP-style integration — because the dependency graph, the reachable exceptions and the route contract are all declared data rather than control flow to be inferred. ## Tooling * `craft route add` / `craft route split` scaffold typed routes; the output is ordinary editable TypeScript. * ESLint rules that enforce the architecture and autofix most of it (`no-angular-inject`, `prefer-craft-service`, `prefer-craft-http-client`, `require-cascade-route-di-check`, …). * Codemods to migrate an existing Angular app progressively. ## What it costs Being fair about the trade: * **A new vocabulary.** Generators, insertions, scopes and yields are unfamiliar before they are useful. * **Type-checking time.** Deep inference is not free; large route collections need splitting ([Scaling routes](/guide/routing/scaling)). * **Unused proofs compile.** The route DI checks are type aliases TypeScript does not require you to keep. [Architecture tests](/guide/testing/architecture#assertroutediproofs) make omitting them a hard failure. * **Error messages.** A failed inference deep in a type can read badly. * **Experimental.** The library and this documentation both still move between minor versions — see the [migration notes](/resources/migration). ## See Also * [The mental model](/guide/concepts/mental-model) * [Learn: the guided path](/learn/) * [Which primitive should I use?](/guide/concepts/choose-primitive) --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/choose-primitive.md --- # Which primitive should I use? There are five primitives. They share the same shape — a name, a configuration, optional insertions — and differ only in **where the value comes from** and **what triggers it**. ## The decision | Where does the value live? | Use | | ---------------------------------------- | ---------------------------------------------- | | In memory, you own it | [`state`](/guide/state/local-state) | | On a server, read | [`query`](/guide/state/server-state) | | On a server, written | [`mutation`](/guide/state/mutations) | | In the URL's query string | [`queryParams`](/guide/state/url-state) | | Nowhere — it's an action with a lifecycle | [`asyncProcess`](/guide/state/async-process) | ## The same table, by symptom **"I need a value the user can change."** → `state`. It is the default. Reach for anything else only when the value's home is somewhere other than memory. **"I need to display data from an API."** → `query`. It re-runs when its `params` change and carries `isLoading` / `status` / `exception` for you. Don't put a `query` result into a `state` — that's two sources of truth. **"I need to send something to an API."** → `mutation`. Triggered explicitly with `.mutate(...)`. Connect it back to the read side with [`insertReactOnMutation`](/guide/state/react-on-mutation) rather than reloading by hand. **"This filter should survive a refresh and be shareable."** → `queryParams`. The URL becomes the source of truth; your query's `params` read from it. **"I need to run an async thing and know if it's running."** → `asyncProcess`. Use it for operations that are not a server read or write: a file export, a share sheet, a delay, a Web API call. ## Things that are *not* a primitive * **Derived values** — use Angular's `computed` inside an insertion. Craft doesn't replace signal derivation, it hosts it. * **Reusable logic across primitives** — that's an [insertion](/guide/concepts/insertions), not a primitive. * **A group of primitives with a name and a scope** — that's a [`craftService`](/guide/app/craft-service). ## What they have in common Whichever you pick, the mechanics are identical: the name comes first, the result is the primitive reference itself, `yield*` drives it inside any craft generator, and the last argument is an insertion. That shared shape is one page: **[Anatomy of a primitive](/guide/concepts/primitive-anatomy)**. Read it once and every primitive page becomes just its own specifics. Advanced patterns that need to write a primitive from DI — wrappers, registries, WebMCP tools, seeding a query result — use the [injectable runtime context](/guide/concepts/primitive-anatomy#injectable-runtime-context). ## See Also * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) * [Learn: your first state](/learn/01-first-state) * [Insertions](/guide/concepts/insertions) --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/primitive-anatomy.md --- # Anatomy of a primitive The five primitives — `state`, `query`, `mutation`, `queryParams`, `asyncProcess` — share one shape. Learn it once here; each primitive's page then only covers what is specific to it. ## The shape ```typescript primitive(name, config, insertion?); ``` * **`name`** — always first, always a string literal. * **`config`** — what the primitive needs to do its job (an initial value, a loader, a codec map…). This is the part that differs between primitives. * **`insertion`** — optional, adds methods and computed values to the result. ## Naming is not decoration The name tags the primitive's injector — `state:tasks`, `query:userQuery` — and is what identifies it in logs, snapshots and the observability tooling. Two primitives with the same name in the same scope are two different things wearing one label, and the tooling cannot tell them apart. ## Driving it with `yield*` A primitive does not run itself. Inside any generator host — a `craftComponent` logic factory, a `craftService` factory, a `craftComputed`, a `craftMethod`, `craftGen`, a route helper — `yield*` is the driver. The entity that yields records the dependency on **its** graph: ```typescript const tasks = yield* state('tasks', []); ``` `yield*` also folds whatever the primitive depends on into the enclosing dependency tree, which is what the route DI check and the test registers read. ::: tip `craftUse` is for Angular interop In an Angular `@Component` class there is no generator to yield from, so you drive the primitive with `craftUse(state('tasks', []))` in a class field instead. Same primitive, same result — but a class field is the end of the graph, so there is nothing to track into. ::: ## It resolves to the primitive reference Every named primitive returns its reference directly: ```typescript const tasks = yield* state('tasks', []); ``` A factory arrow can return a single primitive directly. `craftService` drives it and exposes the primitive reference itself: ```typescript const { MyService } = craftService( { name: 'MyService', scope: 'global' }, () => state('counter', 0), ); ``` When a factory exposes several primitives, wrap the record with `craftYieldRecord`. It yields each primitive generator and keeps the record keys in the returned value: ```typescript import { craftComputed, craftService, craftYieldRecord, query, state, type CraftServiceInput, } from '@craft-ng/core'; const { UserQuery } = craftService( { name: 'UserQueryWithState', scope: 'global' }, (inputs: { userId: CraftServiceInput }) => craftYieldRecord({ userQuery: query('userQuery', { params: function* () { return yield* inputs.userId(); }, loader: ({ params }) => ApiService.getItemById(params), }), refresh: state('refresh', 0, ({ update }) => ({ increment: () => update((value) => value + 1), })), }), ); ``` Use the direct return for one primitive and `craftYieldRecord` for a record of primitives. Inside a generator factory, the equivalent explicit form remains available: `const userQuery = yield* query(...)`. ## Insertions add to the result The last argument receives the primitive's internals and returns what to expose: ```typescript state('counter', 0, ({ state, update }) => ({ increment: () => update((value) => value + 1), isEven: craftComputed(function* () { return (yield* state()) % 2 === 0; }), })); ``` Compose several with the primitive-specific helpers described in [Typed insertion pipes](/guide/concepts/insertion-pipes). An insertion can also be a `function*`, in which case it can `yield*` services. A derived value or generator method must yield readers it does not own — including this primitive's `state()` / `update()` when the member is a generator. Keep [`craftPipe`](/guide/concepts/insertions) for universal or nested compositions. ## Scoped providers Every primitive config accepts `providers`, for dependencies that should be scoped to this primitive alone rather than to the whole service: ```typescript query('userQuery', { providers: [provideUserApiService()], loader: function* () { return yield* UserApiService.get(); }, }); ``` ## Injectable runtime context Everyday insertions already receive `set`, `update`, and `patch` as arguments. Keep using that. Each primitive also **provides those same writes through Angular DI** on every insertion method. Wrappers, registries, tests, WebMCP tools, and other advanced patterns can recover them without being passed the insertion context — for example to seed a query result, patch a mutation value, or drive a `state` from a [`provideFnWrapper`](/guide/advanced/observability#providefnwrapper). That is also the surface a WebMCP client uses to inspect and mutate a live primitive: `get` / `set` / `update` / `patch` on a query result, a `state`, a mutation, an `asyncProcess`, or `queryParams`, without editing TypeScript or reloading the page. The helpers return `undefined` outside an insertion-method injection context. Use the one that matches the primitive, or the generic helper and branch on `kind`: | Primitive | Helper | | -------------- | ------------------------------------------- | | `state` | `injectStateMethodRuntimeContext()` | | `query` | `injectQueryMethodRuntimeContext()` | | `mutation` | `injectMutationMethodRuntimeContext()` | | `queryParams` | `injectQueryParamsMethodRuntimeContext()` | | `asyncProcess` | `injectAsyncProcessMethodRuntimeContext()` | | any of them | `injectPrimitiveMethodRuntimeContext()` | The context is the same shape everywhere: ```typescript { kind: 'state' | 'query' | 'mutation' | 'queryParams' | 'asyncProcess'; get(): unknown; set(value: unknown): unknown; update(updater: (current: unknown) => unknown): unknown; patch(updater: (current: unknown) => object): unknown; originalSource: string; } ``` `patch` merges objects. Use `update` to replace arrays or primitives. Nested [`insertSelect`](/guide/state/select) methods receive the selected slice, not the root. ```typescript import { injectQueryMethodRuntimeContext, provideFnWrapper, } from '@craft-ng/core'; provideFnWrapper( 'Warning: dependency injection here is not type-safe and may fail at runtime', function* (factory, thisArg, args) { const query = injectQueryMethodRuntimeContext(); const result = yield* factory.apply(thisArg, args); query?.patch((current) => ({ ...current, viewed: true })); return result; }, ); ``` `query`, `mutation`, `asyncProcess`, and `queryParams` also publish the **primitive value itself** — not only its methods — through `providePrimitiveResourceRuntimeObserver`. Register it on the primitive's `providers` (or higher). The observer runs at creation; keep the context if you need to write later. Grouped resources take an optional `id` equivalent to `.select(id)`. `state` has no resource observer: only the method context. ```typescript import { providePrimitiveResourceRuntimeObserver, query, type PrimitiveResourceRuntimeContext, } from '@craft-ng/core'; let usersRuntime: PrimitiveResourceRuntimeContext | undefined; const users = yield* query('users', { providers: [ providePrimitiveResourceRuntimeObserver((context) => { if (context.kind === 'query') { usersRuntime = context; } }), ], params: () => true, loader: function* () { return yield* UserApi.list(); }, }); usersRuntime?.set([{ id: 'stub', name: 'Preview' }]); ``` The Angular `InjectionToken` behind these helpers is not part of the public API. Inject the helpers; do not look up the token yourself. ## Reading a value that may have failed The async primitives (`query`, `mutation`, `asyncProcess`) expose one value reader: * `value()` — never throws, returns `undefined` when no value is available. ::: tip Pass `query.value` to a template binding. Inside a generator, `yield* query.value()`. ::: ## Pitfalls **A primitive invocation is single-use.** Each call produces one generator, to be consumed exactly once. Storing one and `yield*`-ing it twice does not give you two primitives — it fails. **It must run in an injection context.** A field initialiser, a constructor, a craft factory. Called outside one, a primitive returns only its configuration under `_config` instead of a live ref — which usually surfaces later as a confusing "not a function" error. **Methods bound to a source with `on$` are not exposed on the result.** They work internally, driven by the source, and do not appear on the ref. **Don't inject the runtime context from feature insertions.** The insertion already receives typed `set` / `update` / `patch` as arguments. The injectable helpers are untyped and exist for wrappers, registries, WebMCP tools, and other advanced patterns. ## See Also * [Which primitive should I use?](/guide/concepts/choose-primitive) * [Insertions](/guide/concepts/insertions) * [Generators and `yield*`](/guide/concepts/generators) * [Observability](/guide/advanced/observability) — `provideFnWrapper` as a consumer of the runtime context --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/generators.md --- # Generators and `yield*` `yield*` is the one mechanism the whole library rests on. This page explains what it actually does, then covers `craftGen`, which lets you write a tracked generator outside a service. ## Why a generator at all Angular's `inject(TaskApi)` is invisible from the outside: nothing in the consumer's type says the dependency exists. The compiler can't catch a missing provider, and a test can't tell you what to mock. A generator gives the runtime a channel. Each `yield*` reports "I need this", the driver resolves it, and the dependency is recorded **in the type**: ```typescript const { TaskList } = craftService( { name: 'TaskList', scope: 'function' }, function* () { const api = yield* TaskApi(); // tracked const tasks = yield* state('tasks', []); // tracked return tasks; }, ); ``` Everything downstream reads that type: the route DI check, the testing register, the dependency snapshot. ## The rule > Every named entity yields what it does not own. A factory, a `craftComputed`, > a `craftMethod`, a generator insertion member — each one records **its** > dependencies with `yield*`. Owning means: the primitive internals handed to **this** insertion (`state`, `set`, `update`, `patch`). Everything else — another primitive, a service, a sibling method, an input, a nested resource reader — is yielded. ```typescript const counter = yield* state('counter', 0, ({ state, update }) => ({ increment: () => update((value) => value + 1), doubled: craftComputed(function* () { return (yield* state()) * 2; }), })); const stats = craftComputed('stats', function* () { return (yield* counter()) + (yield* counter.doubled()); }); ``` `increment` may return `update(...)` directly: it is not a generator, and the insertion wrapper consumes the write. `doubled` does not own `state()`, so it yields it. `stats` does not own `counter`, so it yields both readers. In a Craft template, pass the reader or the method instead of wrapping a synchronous call: ```typescript p(counter); button({ click: counter.increment }, '+'); ``` `craftUse(...)` is the Angular-interop path: in a `@Component` class there is no generator to yield from, so a class field drives the primitive with it instead. A class field is the end of the graph, which is why `craftUse` has nothing to track. Use it in tests and other synchronous boundaries too: `craftUse(counter.increment())`. Yield only what you use: `yield* TaskApi.fetchAll()` records one property instead of the whole service, which is what keeps test registers small. See [Shaping the public API](/guide/app/expose-api). The `craft-ng/require-yieldable-reactive-read`, `craft-ng/require-yieldable-insertion-write` and `craft-ng/require-yieldable-template-method` ESLint rules enforce this. See [ESLint rules](/guide/routing/eslint-rules). ## `craftGen` — a tracked generator outside a service Build reusable generator factories that can be composed with `yield*` and that short-circuit through typed `craftException` values. `craftGen(factory)` wraps a generator factory and returns an invoker you delegate to with `yield*`. It keeps the inner generator model intact: * dependency yields still flow to the outer driver; * the success value is returned through `yield*`; * `craftException(...)` results are converted into a `CraftGenShortCircuit`; * the reachable exception codes remain visible at the type level. That makes it the right tool for reusable route logic — role checks, feature flags, onboarding gates. ### The common case ```typescript import { craftException, craftGen } from '@craft-ng/core'; export const roleGuard = craftGen(function* (...roles: Role[]) { const { user } = yield* Auth(undefined, ({ user }) => ({ user })); const currentUser = yield* user(); if (!currentUser) { return craftException({ code: 'NOT_AUTHENTICATED' }); } return roles.includes(currentUser.role) ? true : craftException({ code: 'FORBIDDEN_ROLE' }); }); export const noPizzeriaGuard = craftGen(function* () { const { pizzeria } = yield* Auth(undefined, ({ pizzeria }) => ({ pizzeria })); return (yield* pizzeria()) ? craftException({ code: 'HAS_PIZZERIA' }) : true; }); ``` Used from a route: ```typescript canActivate: function* () { yield* roleGuard(ROLES.PIZZERIA_ADMIN); yield* noPizzeriaGuard(); return true; }, ``` ### Why it matters Without it, reusable guards turn into copy-pasted generator blocks with repeated branching and ad hoc exception handling. `craftGen` lets you: * parameterise one guard and reuse it across routes; * keep the route logic readable by composing with `yield*`; * preserve exhaustiveness, because every reachable exception code stays typed; * keep route dependency tracking intact, because the yielded dependencies still surface to the surrounding route. In practice this is the difference between "a guard that works" and "a guard you can safely reuse and evolve". ### How it behaves * A normal return value comes back from `yield*` unchanged. * A returned `craftException` makes the wrapper throw `CraftGenShortCircuit`. * Yielded dependencies are relayed unchanged to the caller. * When you compose several guards, the first exception wins. ## Pitfalls **A primitive invocation is single-use.** Each call produces one generator, to be consumed exactly once — don't store one and `yield*` it twice. **Mixing `inject` into a craft factory.** It works at runtime and is invisible to every check that makes this library worth using. The `craft-ng/no-angular-inject` ESLint rule exists for this. ## See Also * [Route guards](/guide/routing/guards) * [ESLint rules](/guide/routing/eslint-rules) — `require-yieldable-reactive-read` and siblings * [Program operators](/guide/advanced/program-operators) — `catchTag` and `retry` * [Exceptions as values](/guide/concepts/exceptions) --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/insertions.md --- # Insertions An insertion is a function that receives a primitive's internals and returns what to expose on it. It is how behaviour gets attached to state — and how it gets reused. **Use one** whenever a primitive needs methods, computed values, or a ready-made behaviour like storage persistence. Every primitive accepts one insertion directly. For several insertions, prefer the typed helper for that primitive; see [Typed insertion pipes](/guide/concepts/insertion-pipes). Use `craftPipe` when you need a universal pipe or an explicit nested context. ## The common case The library's insertions and the ones you write are the same shape, so they compose in the same pipe: ```typescript import { craftUnique, insertStoragePersister, insertPaginationPlaceholderData, insertReactOnMutation, insertQueryPipe, insertStatePipe, query, } from '@craft-ng/core'; const users = yield* query( 'users', { params: pagination, identifier: (params) => `${params.page}-${params.pageSize}`, loader: function* ({ params }) { return yield* ApiService.getDataList(params); }, }, insertQueryPipe( insertStoragePersister(craftUnique({ storeName: 'app', key: 'users', })), insertPaginationPlaceholderData({ initialValue: [] as User[] }), insertReactOnMutation(deleteUser, { filter: ({ mutationIdentifier, queryResource }) => !!queryResource.value()?.some((u) => u.id === mutationIdentifier), optimisticUpdate: ({ queryResource, mutationIdentifier }) => removeOne({ entities: queryResource.value(), id: mutationIdentifier, }), }), ), ); ``` The typed helper supplies the query context to each member and keeps the primitive call free of context plumbing. ::: tip A single insertion needs no pipe Pass it directly: ```typescript const user = yield* query('user', config, insertStoragePersister({ … })); ``` ::: ## Writing your own There is nothing special about a library insertion. Yours is a function of the same shape: ```typescript const counter = yield* state( 'counter', 0, insertStatePipe( ({ update, set }) => ({ increment: () => update((c) => c + 1), reset: () => set(0), }), ({ state }) => ({ isOdd: craftComputed(function* () { return (yield* state()) % 2 === 1; }), }), ), ); ``` Extract it to a named function the moment two primitives want the same behaviour — that is the whole extension mechanism. A member can also be a `function*`, in which case it can `yield*` services and those dependencies fold into the enclosing graph. A `craftComputed` or generator method must yield every reader it does not own — including this primitive's `state()` / `update()` / sibling methods on `insertions`. ## What piping guarantees Piping is strictly equivalent to attaching members one by one: * members run **left to right**; * each member sees the previous members' outputs on `context.insertions`; * the outputs are the **intersection** of all members' — on a key conflict, the rightmost wins at runtime; * tracked dependencies are the **union** of all members', so `ExtractDeps` sees every one; * each member is **wrapped individually**, so correlation-id tracking and app snapshots observe them separately. ## Nesting Pipes nest freely, including inside `insertSelect` — each level re-passes its own context: ```typescript const board = yield* state( 'board', { ui: { activeColor: 'black' }, grid: createInitialGrid() }, insertStatePipe( insertStoragePersister(craftUnique({ storeName: 'app', key: 'board', })), () => ({ resetAll$: source$('resetAll$') }), insertSelect('grid', (gridContext) => craftPipe( gridContext, ({ state, update }) => ({ addRow: () => update((grid) => [...grid, createNextRow(grid)]), rowIndexes: craftComputed(function* () { return (yield* state()).map((_row, index) => index); }), }), insertSelect('row', ({ update }) => ({ /* … */ })), ), ), ), ); ``` ## Pitfalls **Choosing the wrong pipe.** Use the primitive-specific helper for a direct composition. `craftPipe` still requires an explicit context and is the right choice for universal or nested compositions. **Two members exporting the same key.** The rightmost wins silently at runtime. Name your outputs so they don't collide. ::: details Why the context is explicit It is what makes one universal pipe possible for all five primitives. The outer `(context) => …` lambda is contextually typed *by the primitive*, so TypeScript knows the exact context shape before it resolves the `craftPipe` call. Inline lambdas keep full contextual typing, higher-order factories like `insertReactOnMutation(...)` match as before, and the primitive's `Exceptions` inference is never degraded. ::: ## See Also * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) * [Injectable runtime context](/guide/concepts/primitive-anatomy#injectable-runtime-context) — recovering `set` / `update` / `patch` from DI, including for WebMCP * [Selecting](/guide/state/select) — `insertSelect` and nested insertions * [Reacting to mutations](/guide/state/react-on-mutation) --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/insertion-pipes.md --- # Typed insertion pipes Each primitive accepts one insertion directly. When a primitive needs several insertions, use the pipe named after that primitive: | Primitive | Typed pipe | | -------------- | ------------------------ | | `state` | `insertStatePipe` | | `query` | `insertQueryPipe` | | `mutation` | `insertMutationPipe` | | `queryParams` | `insertQueryParamsPipe` | | `asyncProcess` | `insertAsyncProcessPipe` | The typed pipe keeps the primitive call readable and gives every member the correct contextual type. Members run from left to right, and each member can read the outputs of the members before it through `insertions`. ## State ```typescript import { craftComputed, insertStatePipe, state } from '@craft-ng/core'; const { counter } = yield * state( 'counter', 0, insertStatePipe( ({ update }) => ({ increment: () => update((value) => value + 1), }), ({ state, insertions }) => ({ isOdd: craftComputed(function* () { return (yield* state()) % 2 === 1; }), incrementAndReport: function* () { yield* insertions.increment(); return yield* state(); }, }), ), ); ``` ## Query ```typescript import { insertStoragePersister, insertQueryPipe, query, } from '@craft-ng/core'; const { users } = yield * query( 'users', { params: () => ({ page: 1 }), loader: ({ params }) => api.getUsers(params), }, insertQueryPipe( insertStoragePersister(craftUnique({ storeName: 'app', key: 'users', })), ({ resource }) => ({ reloadUsers: function* () { return yield* resource.reload(); }, }), ), ); ``` ## Mutation ```typescript import { insertMutationPipe, mutation } from '@craft-ng/core'; const { saveUser } = yield * mutation( 'saveUser', { method: (user: User) => user, loader: ({ params }) => api.saveUser(params), }, insertMutationPipe( ({ resource }) => ({ reload: function* () { return yield* resource.reload(); }, }), ({ insertions }) => ({ reloadTwice: function* () { yield* insertions.reload(); yield* insertions.reload(); }, }), ), ); ``` ## URL state ```typescript import { craftComputed, insertQueryParamsPipe, queryParams } from '@craft-ng/core'; const { filters } = yield * queryParams( 'filters', { state: { page: { fallbackValue: 1 }, search: { fallbackValue: '' }, }, }, insertQueryParamsPipe( ({ state }) => ({ hasSearch: craftComputed(function* () { return (yield* state()).search.length > 0; }), }), ({ state, patch }) => ({ nextPage: function* () { const current = yield* state(); return yield* patch({ page: current.page + 1 }); }, }), ), ); ``` ## Async process ```typescript import { insertAsyncProcessPipe, asyncProcess } from '@craft-ng/core'; const { search } = yield * asyncProcess( 'search', { method: (term: string) => term, loader: ({ params }) => api.search(params), }, insertAsyncProcessPipe( () => ({ source: 'search-box' as const }), ({ insertions }) => ({ prefixTerm: (term: string) => `${insertions.source}:${term}`, }), ), ); ``` ## When to use `craftPipe` Use a single insertion directly when there is no composition: ```typescript state('counter', 0, ({ update }) => ({ increment: () => update((value) => value + 1), })); ``` Keep [`craftPipe`](/guide/concepts/insertions) for universal compositions that need an explicit context, especially nested insertions such as `insertSelect`: ```typescript state('board', initialBoard, (context) => craftPipe( context, insertSelect('grid', (gridContext) => craftPipe(gridContext, ({ update }) => ({ reset: () => update(() => []), })), ), ({ state }) => ({ rowCount: craftComputed(function* () { return (yield* state()).grid.length; }), }), ), ); ``` The typed pipes delegate to `craftPipe`, so their runtime semantics remain the same: insertion outputs are merged left to right, generator insertions are driven, and each member keeps its own observability wrapper. --- --- url: https://ng-angular-stack.github.io/craft/guide/concepts/exceptions.md --- # Exceptions as values A declared failure is a **value you return**, not something you throw. It travels through types instead of escaping through the stack — so the compiler can see it, follow it, and tell you when nobody handled it. That is the whole idea, and it rests on a line most codebases leave blurry: * an **exception** is a failure you declared, expect, and intend to handle — "this email is taken", "the session expired", "that id is malformed"; * an **error** is everything else — the unexpected kind, which should surface loudly rather than be silently absorbed. A `try/catch` tells you nothing about what it might catch. A returned `craftException` carries its code and payload all the way to whoever handles it, and the set of reachable codes is a **type** — which is what makes exhaustive checking possible at all. ## Declaring one ```typescript import { craftException } from '@craft-ng/core'; craftException({ code: 'TITLE_REQUIRED' }, { received: payload.title }); ``` The first argument carries the `code` (and an optional `scope`); the second is a free-form **payload**, whose type flows all the way to whoever handles it. ## Where they come from An exception is a **returned value**, not a thrown one. Return it from the place that detects the failure and the rest of the pipeline stops on its own: ```typescript const createTask = yield* mutation('createTask', { // rejected before any request is sent — the loader never runs method: (payload: { title: string }) => payload.title.trim().length === 0 ? craftException({ code: 'TITLE_REQUIRED' }, { received: payload.title }) : payload, loader: function* ({ params }) { return yield* CraftHttpClient.post(({ response }) => ({ url: '/api/tasks', body: params, success: response(), // recognised from the response exceptions: [ function* ({ status }) { if (!(yield* status(409))) return; return craftException({ code: 'TITLE_ALREADY_EXISTS' }); }, ], })); }, }); ``` Guards, matchers and resolvers raise them the same way — see [Route guards](/guide/routing/guards). ## Propagating through a shared utility The interesting case isn't one primitive failing — it's a rule that lives in one place and travels. Wrap it in a [`craftGen`](/guide/concepts/generators) and it becomes a reusable unit that **short-circuits its callers**: ```typescript import { craftException, craftGen, craftUntilSettled } from '@craft-ng/core'; // one business rule, declared once export const loadReport = craftGen(function* () { const reportRef = yield* Report(); const report = yield* craftUntilSettled(reportRef); return report.totalUsers === 0 ? craftException({ code: 'REPORT_EMPTY' }) : report; }); ``` Consumers just `yield*` it. If the rule rejects, everything after the yield is skipped — no `if (result.isError)` at each level: ```typescript const { ReportFacade } = craftService( { name: 'ReportFacade', scope: 'global' }, function* () { const report = yield* loadReport(); // narrowed: never the exception return { total: report.totalUsers }; }, ); ``` `report` is the success value only. The exception left through the generator channel, and — this is the point — **`REPORT_EMPTY` is now part of `ReportFacade`'s reachable codes.** It keeps travelling up until someone deals with it. ### Stopping the propagation Two ways, and the difference matters: **Recover locally** with `catchTag`, and the code **leaves the union** — nobody upstream has to know about it: ```typescript resolve: craftResolve(function* () { return yield* loadReport().pipe( catchTag('REPORT_EMPTY', function* () { return { totalUsers: 0, generatedAt: null }; }), ); }); ``` **Let it reach the route**, and `handleExceptions` must have a handler for it — the compiler says so. That's the right choice when the failure should change what the user sees, rather than being papered over with a default value. ::: tip Composition rule When several utilities are composed, the **first exception wins** — the rest of the program doesn't run. See [Program operators](/guide/advanced/program-operators) for `catchTag` and `retry`. ::: Working example: the `slow-page` demo raises `REPORT_EMPTY` from a `craftGen` resolver and recovers it locally, so the route never declares a handler for it — [slow-page.routes.ts](https://github.com/ng-angular-stack/ng-craft/blob/main/apps/demo/src/app/examples/routes/slow-page/slow-page.routes.ts). ## Reading them Every async primitive exposes its exceptions **split by origin**, and typed from the codes you declared: ```typescript createTask.hasException(); // boolean createTask.exceptions().params?.TITLE_REQUIRED; // rejected by `method` createTask.exceptions().loader?.TITLE_ALREADY_EXISTS; // produced by the request ``` The origin matters: `params` means nothing left the browser, `loader` means the server was involved. The union is closed, so the compiler knows `TITLE_ALREADY_EXISTS` exists and that `TITLE_TOO_LONG` doesn't. `queryParams` follows the same shape with a `parse` origin for decode failures. ## Handling them Where you handle an exception depends on how far it needs to travel: | The failure concerns… | Handle it… | | --------------------------- | ------------------------------------------------------------------------ | | One primitive's own UI | Read `exceptions()` where you render it | | A form's submission | [`insertFormSubmit`](/guide/forms/submit) — reshape the mutation's codes | | Whether a route can render | [Route exception handling](/guide/routing/exception-handling) | | Nothing in particular | Let it be an error — the global error component catches it | ## An unhandled exception doesn't just disappear This is the rule that ties the whole system together, and it is easy to miss. When a component's factory — or one of its providers — can raise a `craftException`, that code becomes part of the component's **initialization exceptions**. It stays attached to the component until something handles it. Most of the time what you want is a **fallback to render**, which is `catchBlock.exhaustive`: ```typescript const Restricted = MyRestrictedComponent.pipe( withProviders([provideRestrictedData(/* … */)]), catchBlock.exhaustive({ NO_ACCESS: () => p('You do not have access to this data.'), }), ); ``` Here the failure comes from a provider, before the template exists — so there is no source block to preserve and the fallback renders alone. When the source *does* exist and should stay visible, use the object form: ```typescript catchBlock.exhaustive({ NO_ACCESS: { render: () => p('Restricted'), showSource: true, position: 'after' }, }); ``` Reach for `catchTag.exhaustive` only when the reaction is **logic** and produces no DOM — logging it, notifying a service: ```typescript catchTag.exhaustive({ NO_ACCESS: function* () { yield* ToastService.show(() => 'No access'); }, }); ``` Either way, handling a code at the component **removes it** from the component's contract and from the route's. Whatever you don't handle is **residual**, and it flows up into the route's exception union — where `handleExceptions` must cover it: ``` component factory + providers ↓ (codes not handled by .pipe) residual exceptions ↓ route exception union ── handleExceptions must be exact ``` At the route, the check is exhaustive **in both directions**: a reachable code with no handler is a type error, and a handler for a code nothing can produce is a type error too. ::: warning Where the compile error actually appears Today the enforcement is at the **route**, not at the component. The variadic component `.pipe(...)` overload is deliberately kept permissive to avoid excessive TypeScript instantiation depth, so an unhandled code there is rejected by **runtime** dispatch rather than by the compiler. The compile-time proof is [`assertExhaustiveRouteExceptions(routes)`](/guide/routing/exception-handling#exhaustiveness). Practical consequence: a component rendered outside any route — in a test, or nested inside another component — gets no compile-time reminder. Handle its codes explicitly. ::: Three utilities do the handling, and which one you want depends on whether the result is logic or DOM: | Utility | Handles in | Produces | | ----------------------- | ---------- | --------------------------------------------- | | `catchBlock.exhaustive` | template | a fallback around a source block — **the default choice** | | `matchBlock.exhaustive` | template | a fallback rendered from an exception value or signal | | `catchTag.exhaustive` | logic | nothing renderable — call a service, log, … | Details on [Customization](/guide/components/customization#choosing-an-exception-utility). ## Why exhaustiveness is worth the ceremony Because the set of reachable codes is a **type**, the compiler can compare it against the set you handled. At the route level that comparison is an assertion you place once per collection: ```typescript assertExhaustiveRouteExceptions(demoRoutes); ``` A code that can be produced but isn't handled is a compile error. So is a handler for a code nothing produces. Add a `craftException` to a guard six months from now and the routes file tells you exactly which routes must decide what to do about it. The assertion itself is an unused call unless it stays in the file. [Architecture tests](/guide/testing/architecture#assertroutediproofs) fail if a collection omits it. ## Pitfalls **Throwing instead of returning.** A thrown value is an *error*: it bypasses the typed union and lands in the global error path. Return the `craftException`. **Reusing one code for two meanings.** The code is the identity the handlers match on. Two different failures deserve two codes, with payloads carrying the detail. ## See Also * [Route exception handling](/guide/routing/exception-handling) * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` keeps the exhaustiveness assert in place * [query](/guide/state/server-state) — typed HTTP exception matchers * [Form exception handling](/guide/forms/exceptions) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/local-state.md --- # Local state `state` holds a value you own, in memory, as a signal — with its methods and derived values attached to it rather than scattered around it. **Use it when** the value's home is your application: a form draft, a selection, a toggle, a counter. **Not when** the value lives on a server ([`query`](/guide/state/server-state)), in the URL ([`queryParams`](/guide/state/url-state)), or is the result of an async action ([`asyncProcess`](/guide/state/async-process)). ## The common case ```typescript import { craftComputed, state } from '@craft-ng/core'; const counter = yield* state('counter', 0, ({ state, update, set }) => ({ increment: () => update((value) => value + 1), decrement: () => update((value) => value - 1), reset: () => set(0), isEven: craftComputed(function* () { return (yield* state()) % 2 === 0; }), })); yield* counter(); // 0 yield* counter.increment(); yield* counter.isEven(); // false yield* counter.reset(); ``` The insertion context gives you `state` (the current value as a yieldable reader), `set` and `update`. Non-generator methods may return `update(...)` directly — the insertion wrapper consumes the write. `isEven` yields `state()` because the computed does not own that reader. In a template, pass the reader or the method: `p(counter)`, `button({ click: counter.increment }, '+')`. At a synchronous boundary, `craftUse(counter.increment())`. ::: tip New to the shape? The name, the destructuring, the `yield*` driver and the single-use rule are the same for all five primitives — see [Anatomy of a primitive](/guide/concepts/primitive-anatomy). ::: ## Deriving from another reader The initial value can be a Craft reader, in which case the state follows it: ```typescript const origin = yield* state('origin', 5); const doubled = yield* state( 'doubled', craftComputed('originDoubled', function* () { return (yield* origin()) * 2; }), ); yield* doubled(); // 10 ``` ## Composing several insertions One insertion function gets crowded. Split it and compose with `insertStatePipe`: ```typescript import { craftComputed, insertStatePipe, state } from '@craft-ng/core'; const counter = yield* state( 'counter', 0, insertStatePipe( ({ update, set }) => ({ increment: () => update((current) => current + 1), reset: () => set(0), }), ({ state }) => ({ isOdd: craftComputed(function* () { return (yield* state()) % 2 === 1; }), }), ), ); yield* counter.increment(); yield* counter.isOdd(); // true ``` Each function receives the same context and contributes its own slice. See [Insertions](/guide/concepts/insertions). ## Driving it from events Bind a method to a [`source$`](/guide/reactivity/source) with [`on$`](/guide/reactivity/on) when the trigger is an event rather than a call: ```typescript const increment = source$('increment'); const reset = source$('reset'); const myState = yield* state('myState', 0, ({ update, set }) => ({ onIncrement: on$(increment, () => update((v) => v + 1)), onReset: on$(reset, () => set(0)), })); increment.emit(); // after yield* / craftUse, myState is 1 reset.emit(); // after yield* / craftUse, myState is 0 ``` Like every craft primitive, a source is **named**, and the name must match the variable it is assigned to — the `craft-ng/craft-source-name-match` ESLint rule enforces it and autofixes it. Note that `onIncrement` and `onReset` are **not** exposed on `myState`. Methods bound to a source work internally only. ## Yielding dependencies An insertion can be a `function*`, so it can pull in services: ```typescript yield* state('counter', 0, function* ({ state }) { const log = yield* Console.log; return { logValue: function* () { yield* log(`State value: ${yield* state()}`); }, }; }); ``` Prefer yielding a craft service over calling Angular's `inject` — yielding is what makes the dependency visible to the route DI check and to test registers. ## Pitfalls **Don't duplicate derived state.** If a value is a function of another, it is a `craftComputed` inside an insertion that `yield*`s its readers, or a `state` whose second argument is that source — not a second `state` kept in sync by an effect. [`assertCraftEffectNoImperativeSync`](/guide/testing/architecture#assertcrafteffectnoimperativesync) fails the architecture suite when an effect writes another primitive. **Keep slices granular.** One `state` per coherent concern. A single object holding five unrelated things makes every consumer depend on all five. ::: details Advanced — scoping providers to one state Use the object form with `$self` when a state needs its own provider scope: ```typescript const counter = yield* state( 'counter', { $self: function* () { return yield* CounterPreferences.initialValue(); }, providers: [provideCounterPreferences(), provideCounterAnalytics()], }, ({ update }) => ({ increment: function* () { yield* CounterAnalytics.track('increment'); return yield* update((value) => value + 1); }, }), ); ``` ::: ::: tip Advanced — injectable writes Insertion methods also provide `injectStateMethodRuntimeContext()`, which recovers `get`, `set`, `update`, and `patch` from DI. Use it from wrappers, WebMCP tools, and other advanced patterns — everyday insertions already receive those methods as arguments. See [Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context). ::: ## See Also * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) * [Insertions](/guide/concepts/insertions) * [craftService](/guide/app/craft-service) — packaging state behind a reusable boundary --- --- url: https://ng-angular-stack.github.io/craft/guide/state/server-state.md --- # query `query` fetches data and owns its whole lifecycle — loading, resolved, exception — re-running itself when its inputs change. **Use it when** you display data that lives on a server. **Not when** you write to the server ([`mutation`](/guide/state/mutations)) or run a one-off async action that isn't a fetch ([`asyncProcess`](/guide/state/async-process)). ::: warning One source of truth Don't copy a query's result into a `state`. The query *is* the state. Don't reload it from a `craftEffect` either — put the inputs in `params` so the loader re-runs when they change. ::: ## The common case ```typescript import { CraftHttpClient, craftComputed, craftUse, query, settled } from '@craft-ng/core'; const { userQuery } = yield * query('userQuery', { params: () => ({ userId: currentUserId() }), loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/users/${params.userId}`, success: response(), })); }, }); ``` `params` is reactive: when what it returns changes, the loader runs again. The result carries the full async state: ```typescript userQuery.value(); // User | undefined — never throws userQuery.isLoading(); // boolean userQuery.status(); // 'idle' | 'loading' | 'resolved' | 'exception' userQuery.exception(); // craftException | undefined ``` ::: tip `value()` is safe to read in templates and computed signals: it returns `undefined` when the query has no resolved value. ::: ## Reading only settled data Use `settledValue` when a template or derived computation requires a real value. It suspends to the nearest `pendingBlock` while the first value is unavailable, propagates query exceptions to a `catchBlock`, and keeps the previous value during a reload. ```typescript const userName = craftComputed('userName', function* () { return (yield* settled(userQuery)).name; }); const user = craftUse(userQuery.settledValue()); ``` Insertion contexts keep the existing fallback behaviour of `state()`. Use `settledState()` when `yield*` (or `craftUse`) should return a non-nullable value and suspend until the current resource is available. ## Triggering it yourself When the trigger is a user action rather than a reactive input, use `method` instead of `params`: ```typescript const { searchQuery } = yield * query('searchQuery', { method: (term: string) => term, loader: function* ({ params: term }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/search?q=${term}`, success: response>(), })); }, }); // In a tracked generator, consume the trigger with yield*. yield * searchQuery.call('angular'); ``` From an ordinary UI callback, the imperative form remains valid: `click: () => searchQuery.call(term)`. Do not put either form in a `craftEffect` dependency graph; use reactive `params` for data loading. ## Adding derived values Same insertion mechanism as any primitive: ```typescript const { todosQuery } = yield * query( 'todosQuery', { params: () => ({ completed: showCompleted() }), loader: async ({ params }) => (await fetch(`/api/todos?completed=${params.completed}`)).json(), }, ({ value, isLoading }) => ({ count: craftComputed(function* () { return (yield* value())?.length ?? 0; }), isEmpty: craftComputed(function* () { return !(yield* isLoading()) && (yield* value())?.length === 0; }), }), ); yield* todosQuery.count(); ``` An insertion can also be a `function*` when it needs to yield services. ## Enriching every item in a list When a query returns an array, `insertQuerySelect` attaches an insertion to each selected item. The selector keeps the item type, so derived values can use its properties without casting: ```typescript import { computed } from '@angular/core'; import { CraftHttpClient, insertQuerySelect, query } from '@craft-ng/core'; type User = { id: string; firstName: string; lastName: string; role: 'admin' | 'member'; }; const { usersQuery } = yield * query( 'usersQuery', { params: () => ({ teamId: currentTeamId() }), loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/teams/${params.teamId}/users`, success: response(), })); }, }, insertQuerySelect('user', ({ state }) => ({ displayName: craftComputed(function* () { const user = yield* state(); return `${user.firstName} ${user.lastName}`; }), roleLabel: craftComputed(function* () { return (yield* state()).role === 'admin' ? 'Administrator' : 'Member'; }), })), ); // `selectUser` targets one item in the returned array. const firstUser = usersQuery.selectUser(0); yield* firstUser?.displayName(); // 'Ada Lovelace' yield* firstUser?.roleLabel(); // 'Administrator' ``` The same pattern supports selecting a nested object property with `insertQuerySelect`, while preserving the selected property's type. ## Avoiding the flicker when inputs change **This is already the default.** When `params` change, the previous value stays visible until the new one resolves, so a paginated list never blanks out mid-navigation. You only touch the option to turn it **off**: ```typescript query('postsQuery', { params: () => ({ page: currentPage() }), preservePreviousValue: () => false, // clear the value while loading loader: async ({ params }) => (await fetch(`/api/posts?page=${params.page}`)).json(), }); ``` ::: tip Not consulted for parallel queries With an `identifier`, each key keeps its own resource, so there is no "previous value" to preserve — the option is ignored on that path. ::: ## Reacting to a mutation Rather than reloading by hand after a write, declare the link: ```typescript import { insertQueryPipe, insertReactOnMutation, insertStoragePersister, } from '@craft-ng/core'; const userQuery = yield* query( 'userQuery', { params: () => ({ userId: currentUserId() }), loader: /* … */, }, insertQueryPipe( insertReactOnMutation(updateUserMutation, { // apply the change immediately, before the server answers optimisticPatch: { name: ({ mutationParams }) => mutationParams.name, email: ({ mutationParams }) => mutationParams.email, }, // and go get the truth back if the mutation failed reload: { onMutationException: true }, }), insertStoragePersister(craftUnique({ storeName: 'demo-app', key: 'user-query', })), ), ); ``` Full options on [Reacting to mutations](/guide/state/react-on-mutation). ## Exceptions `exceptions()` is split by **origin** and typed from the codes you declared — `params` for what your `method` rejected before any request, `loader` for what the request produced: ```typescript import { craftException, query } from '@craft-ng/core'; const { userQuery } = yield * query('userQuery', { method: (value: string) => value.length < 3 ? craftException( { code: 'SEARCH_TERM_TOO_SHORT' }, { min: 3, received: value.length }, ) : value, loader: async ({ params }) => params === 'forbidden' ? craftException({ code: 'USER_ACCESS_FORBIDDEN' }, { id: params }) : { id: params, name: 'John Doe' }, }); yield * userQuery.call('ab'); userQuery.hasException(); // true userQuery.exceptions().params?.SEARCH_TERM_TOO_SHORT; yield * userQuery.call('forbidden'); userQuery.exceptions().loader?.USER_ACCESS_FORBIDDEN; ``` Returning a `craftException` from `method` means the loader never runs — you don't send a request you already know will fail. ## Pitfalls **No value is available yet.** Check `hasValue()` or handle the `undefined` result while the query is loading or in exception. **`params` must be cheap and pure.** It runs inside a reactive computation; side effects belong in the loader. ::: details Advanced — parallel queries by identifier `identifier` keeps one resource per key, so several runs coexist instead of replacing each other: ```typescript const userId = signal(undefined); const { userQuery } = yield * query('userQuery', { params: userId, identifier: (id) => id, loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/users/${params}`, success: response(), })); }, }); userId.set(1); userId.set(2); userQuery.select('1').value(); // user 1 userQuery.select('2').value(); // user 2 ``` ::: ::: details Advanced — typed HTTP exceptions Loader exceptions are matched declaratively: each matcher yields predicates on the response and returns a `craftException` when it recognises the failure. ```typescript loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/users/${params}`, success: response(), exceptions: [ function* ({ status, code, content }) { if (!(yield* status(400))) return; if (!(yield* code('PASSWORD_REQUIRED'))) return; if (!(yield* content('Password is required'))) return; return craftException({ code: 'PASSWORD_REQUIRED', scope: 'UsersFeatureForDependencies', }); }, function* ({ body, header }) { const payload = yield* body<{ errors?: Array<{ field: 'password' }>; }>(); if (!payload.errors?.some((error) => error.field === 'password')) return; if (!(yield* header('x-error-kind', 'validation'))) return; return craftException({ code: 'VALIDATION_HEADER_ERROR', scope: 'UsersFeatureForDependencies', }); }, ], })); } ``` Working source: [exceptions demo](https://github.com/ng-angular-stack/ng-craft/blob/main/apps/demo/src/app/examples/primitives/exceptions/exceptions.ts). ::: ::: details Advanced — yielding dependencies from `params` `params` can be a generator, and so can an insertion: ```typescript const { userQuery } = yield * query( 'userQuery', { providers: [provideUserService(), provideUserApiService()], params: function* () { return yield* UserService.userId(); }, loader: function* ({ params: userId }) { return yield* UserApiService.get(userId); }, }, function* () { const queryTools = yield* QueryTools(); return { queryKey: `${queryTools.prefix()}:details` }; }, ); ``` ::: ::: tip Advanced — injectable writes Insertion methods provide `injectQueryMethodRuntimeContext()`, and the query value itself is published to `providePrimitiveResourceRuntimeObserver`. Both expose `get`, `set`, `update`, and `patch`, so wrappers, WebMCP tools, and other advanced patterns can seed or replace a result without going through the insertion callback. See [Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context). ::: ## See Also * [Mutations](/guide/state/mutations) — the write side * [Reacting to mutations](/guide/state/react-on-mutation) * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/mutations.md --- # Mutations `mutation` is `query`'s counterpart for writes: same shape, triggered explicitly, owning its own loading and failure state. **Use it when** you send something to a server — POST, PUT, PATCH, DELETE. **Not when** you read ([`query`](/guide/state/server-state)) or run an async action that isn't a server write ([`asyncProcess`](/guide/state/async-process)). ## The common case ```typescript import { CraftHttpClient, mutation } from '@craft-ng/core'; const { createUser } = yield * mutation('createUser', { method: (payload: { name: string; email: string }) => payload, loader: function* ({ params: user }) { return yield* CraftHttpClient.post(({ response }) => ({ url: '/api/users', body: user, success: response(), })); }, }); // In a tracked generator, consume the trigger with yield*. yield * createUser.mutate({ name: 'John', email: 'john@example.com' }); createUser.isLoading(); createUser.value(); // never throws createUser.exception(); ``` `method` is the entry point: it takes what the caller passes and returns what the loader receives as `params`. It is also where you reject bad input before any request happens. ::: tip `value()` is safe to read in templates and computed signals: it returns `undefined` when the mutation has no resolved value. ::: ## Connecting it to the read side A mutation on its own leaves your list stale. Declare the link on the query rather than reloading by hand: ```typescript insertReactOnMutation(createUser, { reload: { onMutationSuccess: true } }); ``` That, plus optimistic updates, is on [Reacting to mutations](/guide/state/react-on-mutation). ## Triggering from an event Use a [`source$`](/guide/reactivity/source) as the trigger instead of calling `.mutate(...)`: ```typescript const deleteUserSource = source$<{ name: string; email: string; id: string }>(); const { deleteUser } = yield * mutation('deleteUser', { method: on$(deleteUserSource, (payload) => payload), loader: function* ({ params: user }) { return yield* CraftHttpClient.delete(({ response }) => ({ url: '/api/users', body: user, success: response(), })); }, }); deleteUserSource.emit({ name: 'John', email: 'john@example.com', id: '5' }); ``` ## Rejecting bad input, and reading exceptions `exceptions()` is split by **origin** — `params` for what `method` rejected before any request, `loader` for what the request produced — and typed from the codes you declared: ```typescript const { deleteUser } = yield * mutation('deleteUser', { method: (payload: { userId: string }) => payload.userId.length < 18 ? craftException( { code: 'INVALID_ID' }, { min: 18, received: payload.userId.length }, ) : payload.userId, loader: function* ({ params }) { return yield* CraftHttpClient.delete(({ response }) => ({ url: '/api/user', body: params, success: response(), exceptions: [ function* ({ status }) { if (!(yield* status(403))) return; return craftException( { code: 'USER_ACCESS_FORBIDDEN' }, { payload: params }, ); }, ], })); }, }); yield * deleteUser.mutate({ userId: 'ab' }); deleteUser.hasException(); // true deleteUser.exceptions().params?.INVALID_ID; yield * deleteUser.mutate({ userId: '12345-12344_27365453-2625434357282827' }); deleteUser.exceptions().loader?.USER_ACCESS_FORBIDDEN; ``` Returning a `craftException` from `method` means the loader never runs. ## Pitfalls **One in-flight run replaces the previous one** unless you declare an `identifier` (below). Deleting three rows at once without one gives you the state of the last delete only. **No value is available yet.** Check `hasValue()` or handle the `undefined` result while the mutation is loading or in exception. ::: details Advanced — parallel mutations by identifier `identifier` keeps one resource per key, so each row tracks its own state: ```typescript const { deleteUser } = yield * mutation('deleteUser', { method: (payload: { name: string; email: string; id: string }) => payload, identifier: ({ id }) => id, loader: function* ({ params: user }) { return yield* CraftHttpClient.delete(({ response }) => ({ url: '/api/users', body: user, success: response(), })); }, }); yield * deleteUser.mutate({ name: 'John', email: 'john@example.com', id: '5' }); deleteUser.select('5')?.isLoading(); deleteUser.select('5')?.exception(); deleteUser.select('5')?.value(); ``` ::: ::: details Advanced — yielding dependencies `method`, `loader` and the insertion can all be generators, and `providers` scopes dependencies to this mutation alone: ```typescript const { saveUser } = yield * mutation('saveUser', { providers: [provideMutationLogger(), provideUserApiService()], method: function* (user: { id: string; name: string }) { yield* MutationLogger.log(`mutate:${user.id}`); return user; }, loader: function* ({ params }) { return yield* UserApiService.save(params); }, }); ``` Inside `craftMutations(...)`, `providers` stays on each `mutation(name, ...)` config, not on the wrapper: ```typescript const userFeature = craft( { name: 'userFeature', providedIn: 'root' }, craftMutations(() => ({ saveUser: mutation('saveUser', { providers: [provideMutationLogger(), provideUserApiService()], method: function* (user: { id: string; name: string }) { yield* MutationLogger.log(`mutate:${user.id}`); return user; }, loader: function* ({ params }) { return yield* UserApiService.save(params); }, }).saveUser, })), ); ``` ::: ::: tip Advanced — injectable writes Insertion methods provide `injectMutationMethodRuntimeContext()`, and the mutation value itself is published to `providePrimitiveResourceRuntimeObserver`. Both expose `get`, `set`, `update`, and `patch` for wrappers, WebMCP tools, and other advanced patterns. See [Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context). ::: ## See Also * [query](/guide/state/server-state) — the read side * [Reacting to mutations](/guide/state/react-on-mutation) * [Submitting a form](/guide/forms/submit) — wiring a form to a mutation --- --- url: https://ng-angular-stack.github.io/craft/guide/state/url-state.md --- # queryParams `queryParams` is a state whose home is the URL's query string. Reading and writing look like any other state; the address bar follows, and so does the back button. **Use it when** the value should survive a refresh and be shareable by copying the link: filters, pagination, a selected tab. **Not when** the value is ephemeral or private — that's [`state`](/guide/state/local-state). ::: tip No synchronisation code There is no effect to write and no `ActivatedRoute` subscription. If you find yourself syncing a `state` with the URL, you want this primitive instead. ::: ## The common case ```typescript import { queryParams } from '@craft-ng/core'; const numberCodec = { decode: (value: string) => parseInt(value, 10), encode: (value: number) => String(value), }; const booleanCodec = { decode: (value: string) => value === 'true', encode: (value: boolean) => String(value), }; const pagination = yield* queryParams( 'pagination', { state: { page: { fallbackValue: 1, codec: numberCodec }, showArchived: { fallbackValue: false, codec: booleanCodec }, }, }, ({ set, update, patch, reset }) => ({ set, update, patch, reset }), ); pagination(); // { page: 1, showArchived: false } pagination.page(); // 1 pagination.patch({ showArchived: true }); // navigates to ?showArchived=true pagination.set({ page: 4, showArchived: false }); pagination.update((current) => ({ ...current, page: current.page + 1 })); pagination.reset(); ``` `?page=3&showArchived=true` becomes `{ page: 3, showArchived: true }` on load. ## Codecs are mandatory A URL only holds strings, so every parameter declares how it converts both ways. The decoded type is your application type; the encoded one is what appears in the address bar. `fallbackValue` is what you get when the parameter is absent — which is why the state type is never `undefined`. Codecs stay synchronous because they run inside the reactive URL computation. `@craft-ng/core` deliberately doesn't depend on a validation library: supply a small `{ decode, encode }` pair directly, or adapt one from the library you already use. ```typescript // arrays tags: { fallbackValue: [], codec: { decode: (value) => value.split(',').filter(Boolean), encode: (value) => value.join(','), }, }, // plain strings q: { fallbackValue: '', codec: { decode: String, encode: String } }, ``` The same pattern covers dates, enums and JSON-encoded objects. ## Custom methods ```typescript yield* queryParams( 'pagination', { state: { page: { fallbackValue: 1, codec: numberCodec } }, }, ({ state, patch }) => ({ nextPage: function* () { const current = yield* state(); return yield* patch({ page: current.page + 1 }); }, previousPage: function* () { const current = yield* state(); return yield* patch({ page: current.page - 1 }); }, setPageSize: function* (pageSize: number) { return yield* patch({ pageSize, page: 1 }); }, }), ); ``` ## Feeding a query The point of URL state is usually to drive a fetch. Read it from the query's `params`: ```typescript yield* query('tasksQuery', { params: () => ({ page: pagination.page() }), loader: /* … */, }); ``` One direction of data flow: click → URL → loader → view. ## Decode failures A `decode` that throws keeps the fallback value rather than corrupting your state, and surfaces the failure: ```typescript if (mode.hasException()) { mode.exceptions().list; mode.exceptions().parse.mode?.code; // 'QueryParamDecodeError' mode.exceptions().parse.mode?.payload; } ``` An encode failure raises `QueryParamEncodeError` before router navigation starts. ## Pitfalls **Every parameter needs a `codec`** — there is no implicit string passthrough. **Methods bound to a source with `on$` are not exposed** on the result, same as every primitive. ::: details Advanced — declaring query params on the route Query parameters can live in the route rather than in a component, so they belong to the URL definition itself: ```typescript export const { demoRoutes, injectDemoQueryParamsQueryParams } = craftRoutes( 'demo', [ { path: 'query-params', ...loadCraftComponent(({ withRetry }) => withRetry(import('./qp-list-with-pagination')).then( ({ default: component }) => component, ), ), queryParams: function* () { const pagination = yield* queryParams( 'pagination', { state: { page: { fallbackValue: 1, codec: numberCodec }, pageSize: { fallbackValue: 4, codec: numberCodec }, }, }, ({ patch, state }) => ({ nextPage: function* () { const current = yield* state(); return yield* patch({ page: current.page + 1 }); }, previousPage: function* () { const current = yield* state(); return yield* patch({ page: current.page - 1 }); }, updatePageSize: function* (pageSize: number) { return yield* patch({ pageSize, page: 1 }); }, }), ); return pagination; }, }, ], ); ``` Working source: [exception-query-params.ts](https://github.com/ng-angular-stack/ng-craft/blob/main/apps/demo/src/app/examples/primitives/exceptions/exception-query-params.ts). ::: ::: details Advanced — yielding dependencies The insertion can be a generator, so a rule can come from a service: ```typescript yield* queryParams( 'pagination', { state: { page: { fallbackValue: 1, codec: numberCodec } } }, function* ({ patch, state }) { const maxPage = yield* PaginationRules.maxPage(); return { nextPage: function* () { const current = yield* state(); if (current.page >= maxPage()) return; return yield* patch(({ page }) => ({ page: page + 1 })); }, }; }, ); ``` ::: ::: tip Advanced — injectable writes Insertion methods provide `injectQueryParamsMethodRuntimeContext()`, and the URL state itself is published to `providePrimitiveResourceRuntimeObserver`. Both expose `get`, `set`, `update`, and `patch` for wrappers, WebMCP tools, and other advanced patterns. See [Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context). ::: ## See Also * [Local state](/guide/state/local-state) — for non-URL state * [query](/guide/state/server-state) — consuming URL state from a loader * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/async-process.md --- # asyncProcess `asyncProcess` runs an async operation and tracks its status, for work that is neither a server read nor a server write. **Use it when** you need to know whether something asynchronous is running: a debounced search, a share sheet, a file export, a delay, a browser API call. **Not when** you fetch ([`query`](/guide/state/server-state)) or write ([`mutation`](/guide/state/mutations)) — those give you caching, params reactivity and mutation wiring on top. ## The common case ```typescript import { asyncProcess, craftComputed } from '@craft-ng/core'; const { delay } = yield * asyncProcess('delay', { method: (successResult: string) => successResult, loader: async ({ params: successResult }) => { await new Promise((resolve) => setTimeout(resolve, 300)); return successResult; }, }); // In a tracked generator, consume the trigger with yield*. yield * delay.method('success'); delay.status(); // 'idle' | 'loading' | 'resolved' | 'exception' delay.isLoading(); delay.hasValue(); delay.value(); // never throws ``` ::: warning `method` always takes exactly one parameter Pass an object when you need several values. ::: ## Wrapping a browser API This is the case `asyncProcess` exists for — turning a promise-returning native API into something with an observable status: ```typescript const { shareContent } = yield * asyncProcess( 'shareContent', { method: (payload: { title: string; url: string }) => payload, loader: function* ({ params }) { return (yield* BrowserNavigator.share(params)) as Promise; }, }, ({ resource }) => ({ isMenuOpen: craftComputed(function* () { return (yield* resource.status()) === 'loading'; }), }), ); yield * shareContent.method({ title: 'Hello AI!', url: 'https://example.com' }); yield* shareContent.isMenuOpen(); ``` Yielding the browser API through a service — rather than touching `navigator` directly — is also what makes it mockable in tests. See [Browser boundaries](/guide/testing/browser-boundaries). ## Triggering from an event Use a [`source$`](/guide/reactivity/source) when the process should run on an event rather than on a call — which is also where debouncing belongs: ```typescript import { on$, source$ } from '@craft-ng/core'; const searchSource = source$('searchSource'); const { delayedSearch } = yield * asyncProcess('delayedSearch', { method: on$(searchSource, (term) => term), loader: async ({ params: term }) => { await new Promise((resolve) => setTimeout(resolve, 300)); return term; }, }); searchSource.emit('query text'); // runs automatically delayedSearch.source; // ReadonlySource delayedSearch.status(); ``` ## Exceptions Split by origin, exactly like `query` and `mutation` — `params` for what `method` rejected, `loader` for what the operation produced: ```typescript const { loadUser } = yield * asyncProcess('loadUser', { method: (value: string) => value.length < 3 ? craftException( { code: 'SEARCH_TERM_TOO_SHORT' }, { min: 3, received: value.length }, ) : value, loader: async ({ params }) => params === 'blocked' ? craftException({ code: 'USER_ACCESS_FORBIDDEN' }, { id: params }) : { id: params, name: 'John Doe' }, }); yield * loadUser.method('ab'); loadUser.hasException(); // true loadUser.exceptions().params?.SEARCH_TERM_TOO_SHORT; yield * loadUser.method('blocked'); loadUser.exceptions().loader?.USER_ACCESS_FORBIDDEN; ``` ## Pitfalls **`method` needs its one parameter**, even when you have nothing to pass. `value()` is safe to read in templates and computed signals — it returns `undefined` when the process has no resolved value. **Reaching for it to fetch data.** If it's an HTTP read, `query` gives you reactive `params` and mutation wiring you'd otherwise rebuild by hand. ::: details Advanced — parallel runs by identifier `identifier` keeps one resource per key so several runs coexist: ```typescript const { debouncedById } = yield * asyncProcess('debouncedById', { method: (payload: { successResult: string; id: string }) => payload, identifier: ({ id }) => id, loader: async ({ params: { successResult } }) => { await new Promise((resolve) => setTimeout(resolve, 300)); return successResult; }, }); yield * debouncedById.method({ id: '1', successResult: data1 }); yield * debouncedById.method({ id: '2', successResult: data2 }); debouncedById.select('1')?.value(); // data1 debouncedById.select('2')?.value(); // data2 ``` ::: ::: details Advanced — yielding dependencies `method` and `loader` can be generators, and `providers` scopes dependencies to this process alone: ```typescript const { loadProfile } = yield * asyncProcess('loadProfile', { providers: [provideAsyncLogger(), provideProfileGateway()], method: function* (userId: string) { yield* AsyncLogger.log(`load:${userId}`); return userId; }, loader: function* ({ params }) { return yield* ProfileGateway.load(params); }, }); ``` ::: ::: tip Advanced — injectable writes Insertion methods provide `injectAsyncProcessMethodRuntimeContext()`, and the process value itself is published to `providePrimitiveResourceRuntimeObserver`. Both expose `get`, `set`, `update`, and `patch` for wrappers, WebMCP tools, and other advanced patterns. See [Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context). ::: ## See Also * [Which primitive should I use?](/guide/concepts/choose-primitive) * [Browser boundaries](/guide/testing/browser-boundaries) — mocking native APIs * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/select.md --- # Selecting a sub-state `insertSelect` targets a nested part of a state and attaches insertions **to that part**, so the logic lives next to the data it operates on rather than at the top of a deeply nested object. **Use it when** a state is a tree and a method only concerns one branch: a cell in a grid, a row in a table, one section of a settings object. **Not when** the whole state is the subject — a plain insertion is simpler. One API covers both shapes: the parent can be an **object** or an **array**, and you don't switch helpers based on which. ::: info `state` only This insertion works with the `state` primitive. ::: ```typescript import { insertSelect, insertStatePipe, insertStoragePersister, state, } from '@craft-ng/core'; ``` ## The common case — selecting an object property ```typescript const board = yield* state( 'board', { cell: { color: 'white', paintCount: 0, }, }, insertSelect('cell', ({ update, state }) => ({ paint: () => update((cell) => ({ ...cell, color: 'black', paintCount: cell.paintCount + 1, })), paintCountStr: function* () { return `Painted ${(yield* state()).paintCount} times`; }, })), ); yield* board.selectCell().paint(); yield* board.selectCell().paintCountStr(); // "Painted 1 times" ``` ## Selecting into an array ```typescript const cells = yield* state( 'cells', [{ color: 'white', paintCount: 0 }], insertSelect('cell', ({ update }) => ({ paint: () => update((cell) => ({ ...cell, color: 'black', paintCount: cell.paintCount + 1, })), })), ); const cell = cells.selectCell(0); if (cell) yield* cell.paint(); console.log(cells.selectCell(0)?.paintCount); // 1 ``` ## Yielding dependencies ```typescript insertSelect('cell', function* ({ patch }) { const color = yield* ColorService(); return { paint: () => patch(() => ({ color, })), }; }); ``` The dependencies are tracked at the primitive level. ## Pitfalls **Only object properties can be selected.** On an object state, targeting a property that is not itself an object — a `string`, `number` or `boolean` — is not supported yet, and currently breaks type inference rather than failing cleanly. An improvement is planned. **A select takes a single nested insertion**, like any primitive. Use `craftPipe` for more than one nested insertion (below). ::: tip Nested typing needs no anchor Use `craftPipe` when composing nested `insertSelect` levels because each level has its own explicit context. The historical `insertNoopTypingAnchor` workaround is not needed here — it remains necessary for the [form-tree helpers](/guide/forms/nested). ::: ## Attaching several insertions Like the primitives, `insertSelect` accepts a **single** nested insertion. To attach several, re-pass the selected context through [craftPipe](/guide/concepts/insertions): ```ts state( 'board', { grid: createInitialGrid() }, insertSelect('grid', (gridContext) => craftPipe( gridContext, ({ state, update }) => ({ addRow: () => update((grid) => [...grid, createNextRow(grid)]), }), insertSelect('row', ({ update }) => ({ // ... })), ), ), ); ``` `insertSelect` also composes as a **member** of a pipe: ```ts state('cells', initialCells, insertStatePipe( insertStoragePersister(craftUnique({ storeName: 'app', key: 'cells', })), insertSelect('cell', ({ update }) => ({ paint: () => update((cell) => ({ ...cell, painted: true })), })), )); ``` ::: details Working examples — pixel art Two demos built almost entirely on nested selects: * [Pixel Art (1D grid)](https://github.com/ng-angular-stack/ng-craft/blob/main/apps/demo/src/app/examples/primitives/pixel-art/pixel-art.ts) * [Pixel Art Matrix (2D grid)](https://github.com/ng-angular-stack/ng-craft/blob/main/apps/demo/src/app/examples/primitives/pixel-art-matrix/pixel-art-matrix.ts) ::: ## See Also * [Insertions](/guide/concepts/insertions) — composing several on one primitive * [Local state](/guide/state/local-state) * [Collections](/guide/state/collections) — for entity lists specifically * [Architecture rules](/guide/testing/architecture) — `assertInsertSelectUnique` when two selects share a key on one host --- --- url: https://ng-angular-stack.github.io/craft/guide/state/react-on-mutation.md --- # Reacting to mutations `insertReactOnMutation` declares the link between a write and the reads it affects: patch the query optimistically, reload it, or both — without calling `refetch()` from the mutation's call site. **Use it when** a mutation makes some query stale. **Not when** the two are unrelated — a reaction that fires on every write is just a hidden coupling. ```typescript import { insertReactOnMutation } from '@craft-ng/core'; ``` ## The common case ```typescript const updateUser = yield* mutation('updateUser', { method: (user: User) => user, loader: function* ({ params: user }) { return yield* CraftHttpClient.patch(({ response }) => ({ url: `/api/users/${user.id}`, body: user, success: response(), })); }, }); const queryRef = yield* query( 'queryRef', { params: () => '5', loader: async ({ params }) => ({ id: params, name: 'John' }), }, insertReactOnMutation(updateUser, { patch: { name: ({ mutationParams: { name } }) => name, }, }), ); ``` Three levers, combinable: | Option | Effect | | ------------------ | ------------------------------------------------------------ | | `patch` | Apply a field-by-field change once the mutation resolves | | `optimisticPatch` | Apply it **immediately**, before the server answers | | `optimisticUpdate` | Same, but you compute the whole new value | | `reload` | Re-run the loader — `onMutationSuccess` / `onMutationException` / `onMutationResolved` | | `filter` | Only react when this predicate passes | The usual pairing is an optimistic change plus `reload: { onMutationException: true }` — show the result instantly, and go get the truth back if the write failed. ## Targeting the right parallel query With `identifier`, several query instances coexist. Use `filter` so the reaction only touches the one the mutation concerns: ```typescript const queryRef = yield* query( 'queryRef', { params: userId, identifier: (userId) => userId, loader: function* ({ params }) { return yield* CraftHttpClient.get(({ response }) => ({ url: `/api/users/${params}`, success: response(), })); }, }, insertReactOnMutation(updateUser, { filter: ({ queryIdentifier, mutationParams }) => mutationParams.id === queryIdentifier, patch: { name: ({ mutationParams: { name } }) => name, }, }), ); ``` ## Several reactions on one query A query accepts a single insertion, so compose them with [`insertQueryPipe`](/guide/concepts/insertion-pipes) to keep this composition readable: ```typescript import { insertQueryPipe, insertReactOnMutation, insertStoragePersister, } from '@craft-ng/core'; const { users } = query( 'users', { params: pagination, identifier: (params) => `${params.page}-${params.pageSize}`, loader: function* ({ params }) { return yield* ApiService.getDataList(params); }, }, insertQueryPipe( insertStoragePersister(craftUnique({ storeName: 'app', key: 'users', })), insertReactOnMutation(deleteUser, { filter: ({ mutationIdentifier, queryResource }) => !!queryResource.value()?.some((u) => u.id === mutationIdentifier), optimisticUpdate: ({ queryResource, mutationIdentifier }) => removeOne({ entities: queryResource.value(), id: mutationIdentifier, }), reload: { onMutationException: true }, }), insertReactOnMutation(deleteUser, { // reload the current page when it becomes empty filter: ({ queryResource }) => queryResource.value()?.length === 0, reload: { onMutationResolved: true }, }), insertReactOnMutation(bulkDelete, { filter: ({ queryResource }) => (queryResource.value()?.length ?? 0) > 0, optimisticUpdate: ({ queryResource, mutationParams }) => removeMany({ entities: queryResource.value(), ids: mutationParams }), }), ), ); ``` ## Pitfalls **Optimistic without a fallback.** `optimisticPatch` / `optimisticUpdate` show a change that has not happened yet. Pair them with `reload: { onMutationException: true }` so a failed write is corrected rather than silently left on screen. **Forgetting `filter` on parallel queries.** Without it, a mutation on one entity patches every cached instance. `queryResource.value()` returns `undefined` when the query is in exception; handle that case inside a `filter`. ## See Also * [query](/guide/state/server-state) — the read side * [Mutations](/guide/state/mutations) — the write side * [Insertions](/guide/concepts/insertions) — composing several reactions * [Architecture rules](/guide/testing/architecture) — `assertMutationHasReactOn` flags a mutation no query reacts to --- --- url: https://ng-angular-stack.github.io/craft/guide/state/collections.md --- # Collections `insertEntities` generates typed collection methods — add, remove, update, upsert — directly on a primitive holding an array of entities, including arrays nested inside an object. **Use it when** a state, query or queryParams holds a list you mutate by id. **Not when** the list is read-only, or when the operation concerns one nested branch rather than the collection — that is [`insertSelect`](/guide/state/select). ## Import ```typescript import { insertEntities } from '@craft-ng/core'; import { addOne, addMany, removeOne, removeMany, setOne, setMany, setAll, updateOne, updateMany, upsertOne, upsertMany, removeAll, } from '@craft-ng/core'; ``` ## Overview `insertEntities` bridges entity utility functions with reactive primitives by: * **Adding methods** - Automatically generates typed methods from entity utilities * **Path support** - Works with nested properties using dot notation * **Custom identifiers** - Supports custom ID selectors beyond default `id` property * **Parallel queries** - Enables entity manipulation in query instances with `select` parameter * **Type inference** - Full TypeScript support with automatic method name generation ::: warning This API currently promotes state imperative change. I am planning to improve this in the future, in order to keep state as much as I can declarative. ::: ## Entity Utilities The following entity utility functions can be used with `insertEntities`: | Utility | Description | | ------------ | ----------------------------------------- | | `addOne` | Adds a single entity to the end | | `addMany` | Adds multiple entities to the end | | `setOne` | Replaces or adds an entity by ID | | `setMany` | Replaces or adds multiple entities by ID | | `setAll` | Replaces the entire collection | | `updateOne` | Partially updates an entity by ID | | `updateMany` | Partially updates multiple entities by ID | | `upsertOne` | Updates if exists, otherwise adds | | `upsertMany` | Updates multiple if exist, otherwise adds | | `removeOne` | Removes a single entity by ID | | `removeMany` | Removes multiple entities by ID | | `removeAll` | Clears the entire collection | ## Signature ```typescript function insertEntities(config: { methods: EntityHelperFns; identifier?: IdSelector; path?: Path; // For nested arrays in objects }): Insertion; ``` ## Parameters ### `methods` Array of entity utility functions to expose as methods on the state/query. ### `identifier` (optional) Custom function to extract the unique identifier from entities. Defaults to: * For objects with `id` property: `(entity) => entity.id` * For primitives (string/number): `(entity) => entity` ### `path` (optional) Dot-notation path to a nested array property. When provided, method names are prefixed with the camelCase path. **Example:** `path: 'catalog.products'` → methods like `catalogProductsAddOne()` ## Method Naming * **Without path**: Method names match utility function names (e.g., `addOne`, `removeMany`) * **With path**: Method names are prefixed with camelCase path (e.g., `productsAddOne`, `catalogProductsRemoveMany`) ## The common case ```typescript import { state, insertEntities, addOne, addMany, removeOne, } from '@craft-ng/core'; const { tags } = state( 'tags', [] as string[], insertEntities({ methods: [addOne, addMany, removeOne], }), ); // Add single tag tags.addOne({ entity: 'typescript' }); console.log(tags()); // ['typescript'] // Add multiple tags tags.addMany({ newEntities: ['angular', 'signals'] }); console.log(tags()); // ['typescript', 'angular', 'signals'] // Remove tag tags.removeOne({ id: 'typescript' }); console.log(tags()); // ['angular', 'signals'] ``` ::: details More examples — nested paths, queries, CRUD, URL state #### Managing objects with default ID ```typescript import { state, insertEntities, addOne, setOne, removeOne, } from '@craft-ng/core'; interface Product { id: string; name: string; price: number; } const { products } = state( 'products', [] as Product[], insertEntities({ methods: [addOne, setOne, removeOne], }), ); // Add product products.addOne({ entity: { id: '1', name: 'Laptop', price: 999 }, }); // Replace or update product products.setOne({ entity: { id: '1', name: 'Laptop Pro', price: 1299 }, }); console.log(products()); // [{ id: '1', name: 'Laptop Pro', price: 1299 }] // Remove product products.removeOne({ id: '1' }); console.log(products()); // [] ``` #### Using custom identifier ```typescript import { state, insertEntities, setOne, removeOne } from '@craft-ng/core'; interface User { uuid: string; name: string; email: string; } const { users } = state( 'users', [] as User[], insertEntities({ methods: [setOne, removeOne], identifier: (user) => user.uuid, }), ); users.setOne({ entity: { uuid: 'abc-123', name: 'Alice', email: 'alice@example.com' }, }); users.setOne({ entity: { uuid: 'abc-123', name: 'Alice Smith', email: 'alice@example.com' }, }); console.log(users()); // [{ uuid: 'abc-123', name: 'Alice Smith', email: 'alice@example.com' }] users.removeOne({ id: 'abc-123' }); console.log(users()); // [] ``` #### Working with nested arrays using path ```typescript import { state, insertEntities, addMany, removeOne } from '@craft-ng/core'; interface Catalog { total: number; products: Array<{ id: string; name: string }>; } const { catalog } = state( 'catalog', { total: 0, products: [], } as Catalog, insertEntities({ methods: [addMany, removeOne], path: 'products', }), ); // Methods are prefixed with "products" catalog.productsAddMany({ newEntities: [ { id: '1', name: 'Item 1' }, { id: '2', name: 'Item 2' }, ], }); console.log(catalog()); // { total: 0, products: [{ id: '1', name: 'Item 1' }, { id: '2', name: 'Item 2' }] } catalog.productsRemoveOne({ id: '1' }); console.log(catalog()); // { total: 0, products: [{ id: '2', name: 'Item 2' }] } ``` #### Deep nested path with dot notation ```typescript import { state, insertEntities, addMany } from '@craft-ng/core'; interface State { catalog: { featured: { products: Array<{ id: string; name: string }>; }; }; } const { store } = state( 'store', { catalog: { featured: { products: [], }, }, } as State, insertEntities({ methods: [addMany], path: 'catalog.featured.products', }), ); // Method is prefixed with camelCase: catalogFeaturedProducts store.catalogFeaturedProductsAddMany({ newEntities: [{ id: '1', name: 'Featured Item' }], }); console.log(store().catalog.featured.products); // [{ id: '1', name: 'Featured Item' }] ``` #### Using with query primitive ```typescript import { query, insertEntities, addMany, removeOne } from '@craft-ng/core'; interface Product { id: string; name: string; } const { productsQuery } = query( 'productsQuery', { params: () => 'all', loader: async () => { const response = await fetch('/api/products'); return response.json() as Product[]; }, }, insertEntities({ methods: [addMany, removeOne], }), ); // After query loads, manipulate the cached data await productsQuery.load(); // Add optimistic product productsQuery.addMany({ newEntities: [{ id: 'temp-1', name: 'New Product' }], }); // Remove product from cache productsQuery.removeOne({ id: 'temp-1' }); ``` #### Working with parallel queries ```typescript import { query, insertEntities, addOne } from '@craft-ng/core'; const { userQuery } = query( 'userQuery', { params: () => 'userId', identifier: (params) => params, // Track multiple query instances loader: async ({ params }) => { const response = await fetch(`/api/users/${params}/posts`); return response.json(); }, }, insertEntities({ methods: [addOne], }), ); // Manipulate specific query instance with select parameter userQuery.addOne({ select: 'user-123', // Target specific query instance entity: { id: 'post-1', title: 'New Post' }, }); ``` #### Update operations ```typescript import { state, insertEntities, updateOne, updateMany } from '@craft-ng/core'; interface Todo { id: string; title: string; completed: boolean; } const { todos } = state( 'todos', [ { id: '1', title: 'Learn Angular', completed: false }, { id: '2', title: 'Build app', completed: false }, ] as Todo[], insertEntities({ methods: [updateOne, updateMany], }), ); // Update single todo todos.updateOne({ update: { id: '1', changes: { completed: true }, }, }); console.log(todos()[0].completed); // true // Update multiple todos todos.updateMany({ updates: [ { id: '1', changes: { title: 'Learn Angular Signals' } }, { id: '2', changes: { completed: true } }, ], }); ``` #### Upsert operations ```typescript import { state, insertEntities, upsertOne, upsertMany } from '@craft-ng/core'; interface Settings { key: string; value: string; } const { settings } = state( 'settings', [{ key: 'theme', value: 'dark' }] as Settings[], insertEntities({ methods: [upsertOne, upsertMany], identifier: (setting) => setting.key, }), ); // Updates existing or adds new settings.upsertOne({ entity: { key: 'theme', value: 'light' }, }); console.log(settings()); // [{ key: 'theme', value: 'light' }] settings.upsertMany({ newEntities: [ { key: 'theme', value: 'auto' }, { key: 'language', value: 'en' }, ], }); console.log(settings()); // [ // { key: 'theme', value: 'auto' }, // { key: 'language', value: 'en' } // ] ``` #### Complete CRUD example ```typescript import { state, insertEntities, addOne, setOne, updateOne, removeOne, setAll, } from '@craft-ng/core'; interface Task { id: string; title: string; completed: boolean; priority: 'low' | 'medium' | 'high'; } const { tasks } = state( 'tasks', [] as Task[], insertEntities({ methods: [addOne, setOne, updateOne, removeOne, setAll], }), ); // Create tasks.addOne({ entity: { id: '1', title: 'Review code', completed: false, priority: 'high', }, }); // Read - use tasks() to access the array // Update tasks.updateOne({ update: { id: '1', changes: { completed: true }, }, }); // Replace tasks.setOne({ entity: { id: '1', title: 'Review and merge code', completed: true, priority: 'high', }, }); // Delete tasks.removeOne({ id: '1' }); // Replace all tasks.setAll({ newEntities: [ { id: '2', title: 'New task', completed: false, priority: 'medium' }, ], }); ``` #### Using with queryParams ```typescript import { queryParams, insertEntities, addOne, removeOne } from '@craft-ng/core'; const { filters } = queryParams( 'filters', { state: { selectedIds: { fallbackValue: [] as string[], codec: { decode: (value) => value.split(',').filter(Boolean), encode: (value) => (value as string[]).join(','), }, }, }, }, insertEntities({ methods: [addOne, removeOne], path: 'selectedIds', }), ); // Methods update queryParams state and URL filters.selectedIdsAddOne({ entity: 'item-1' }); // URL: ?selectedIds=item-1 filters.selectedIdsAddOne({ entity: 'item-2' }); // URL: ?selectedIds=item-1,item-2 filters.selectedIdsRemoveOne({ id: 'item-1' }); // URL: ?selectedIds=item-2 ``` ::: ## Pitfalls **Declaring every utility "just in case".** `methods` decides the generated API; listing all twelve gives every consumer twelve methods to ignore. List what you use. **Picking the wrong operation.** `add` appends blindly, `set` replaces by id, `upsert` does whichever applies. Choosing `addOne` where you meant `upsertOne` produces duplicates that only show up with real data. **Mutating the array directly.** The generated methods are immutable updates; bypassing them breaks change detection. **Deep `path` values.** If the path is getting long, the state shape is probably the problem — consider flattening it. ## Type Safety `insertEntities` provides full type inference: ```typescript interface Product { id: string; name: string; price: number; } const { products } = state( 'products', [] as Product[], insertEntities({ methods: [addOne, updateOne], }), ); // ✅ TypeScript knows entity must be Product products.addOne({ entity: { id: '1', name: 'Item', price: 100 } }); // ❌ TypeScript error - missing required properties products.addOne({ entity: { id: '1' } }); // ✅ TypeScript knows changes are Partial products.updateOne({ update: { id: '1', changes: { price: 120 } }, }); // ❌ TypeScript error - invalid property products.updateOne({ update: { id: '1', changes: { invalid: true } }, }); ``` ## See Also * [Collection utilities](/guide/state/collections-utils) — the underlying functions * [Selecting a sub-state](/guide/state/select) — for one branch rather than the list * [Insertions](/guide/concepts/insertions) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/collections-utils.md --- # Collection utilities The immutable array helpers behind [`insertEntities`](/guide/state/collections) — `addOne`, `updateMany`, `upsertOne` and friends. The shapes are the ones NgRx Entity popularised. **Use them directly when** you manipulate an array outside a primitive: inside an `optimisticUpdate`, a loader, or a plain computed. **Otherwise** let [`insertEntities`](/guide/state/collections) generate the methods for you — same functions, attached to your state. This page is a reference: scan the API list below, or jump to the [usage example](#usage-example). ## Types ### IdSelector A function type that extracts the identifier from an entity. ```typescript type IdSelector = (entity: T) => K; ``` ### Update A type for partial updates containing an id and the changes to apply. ```typescript type Update = { id: K; changes: Partial; }; ``` ## Optional Identifier For entities that have an `id` property, the `identifier` parameter is **optional**. The functions will automatically use the `id` property. For entities without an `id` property, you must provide a custom `identifier` function. ```typescript // Entity with id property - identifier is optional interface User { id: number; name: string; } const users: User[] = [{ id: 1, name: 'Alice' }]; removeOne({ id: 1, entities: users }); // ✅ OK - no identifier needed // Entity without id property - identifier is required interface Product { sku: string; name: string; } const products: Product[] = [{ sku: 'A1', name: 'Widget' }]; removeOne({ id: 'A1', entities: products, identifier: (p) => p.sku }); // ✅ OK ``` ## Usage Example ```typescript import { addOne, addMany, updateOne, removeOne, upsertOne, } from '@anthropic/craft'; interface User { id: number; name: string; email: string; } let users: User[] = []; // Add a single user users = addOne({ entity: { id: 1, name: 'Alice', email: 'alice@example.com' }, entities: users, }); // Add multiple users users = addMany({ newEntities: [ { id: 2, name: 'Bob', email: 'bob@example.com' }, { id: 3, name: 'Charlie', email: 'charlie@example.com' }, ], entities: users, }); // Update a user (no identifier needed - User has id property) users = updateOne({ update: { id: 1, changes: { name: 'Alice Updated' } }, entities: users, }); // Upsert a user (update if exists, add if not) users = upsertOne({ entity: { id: 4, name: 'David', email: 'david@example.com' }, entities: users, }); // Remove a user users = removeOne({ id: 2, entities: users }); ``` ## API reference ### removeAll Removes all elements from the list. ```typescript function removeAll(): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; const result = removeAll(); // [] ``` *** ### addOne Adds an element to the end of the list. ```typescript function addOne({ entity, entities }: { entity: T; entities: T[] }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; const result = addOne({ entity: { id: 2, name: 'Bob' }, entities: users, }); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] ``` *** ### addMany Adds multiple elements to the end of the list. ```typescript function addMany({ newEntities, entities, }: { newEntities: T[]; entities: T[]; }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; const result = addMany({ newEntities: [ { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ], entities: users, }); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }] ``` *** ### setAll Replaces the entire list with new elements. ```typescript function setAll({ newEntities }: { newEntities: T[] }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; const result = setAll({ newEntities: [{ id: 2, name: 'Bob' }] }); // [{ id: 2, name: 'Bob' }] ``` *** ### setOne Replaces an element if it exists (based on id), otherwise adds it. If the entity has an `id` property, the `identifier` is optional. ```typescript // With identifier (required for entities without id property) function setOne(params: { entity: T; entities: T[]; identifier: IdSelector; }): T[]; // Without identifier (for entities with id property) function setOne(params: { entity: T; entities: T[]; identifier?: IdSelector; }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; // Without identifier (User has id property) const result1 = setOne({ entity: { id: 1, name: 'Alice Updated' }, entities: users, }); // [{ id: 1, name: 'Alice Updated' }] // With custom identifier const products = [{ sku: 'A1', name: 'Widget' }]; const result2 = setOne({ entity: { sku: 'A1', name: 'Widget Updated' }, entities: products, identifier: (p) => p.sku, }); // [{ sku: 'A1', name: 'Widget Updated' }] ``` *** ### setMany Replaces or adds multiple elements (based on id). If the entity has an `id` property, the `identifier` is optional. ```typescript function setMany(params: { newEntities: T[]; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; // Without identifier const result = setMany({ newEntities: [ { id: 1, name: 'Alice Updated' }, { id: 2, name: 'Bob' }, ], entities: users, }); // [{ id: 1, name: 'Alice Updated' }, { id: 2, name: 'Bob' }] ``` *** ### updateOne Partially updates an existing element. Does nothing if the element is not found. If the entity has an `id` property, the `identifier` is optional. ```typescript function updateOne(params: { update: Update; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice', email: 'alice@example.com' }]; // Without identifier const result = updateOne({ update: { id: 1, changes: { name: 'Alice Updated' } }, entities: users, }); // [{ id: 1, name: 'Alice Updated', email: 'alice@example.com' }] ``` *** ### updateMany Partially updates multiple existing elements. If the entity has an `id` property, the `identifier` is optional. ```typescript function updateMany(params: { updates: Update[]; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; // Without identifier const result = updateMany({ updates: [ { id: 1, changes: { name: 'Alice Updated' } }, { id: 2, changes: { name: 'Bob Updated' } }, ], entities: users, }); // [{ id: 1, name: 'Alice Updated' }, { id: 2, name: 'Bob Updated' }] ``` *** ### upsertOne Updates an element if it exists (merging properties), otherwise adds it. If the entity has an `id` property, the `identifier` is optional. ```typescript function upsertOne(params: { entity: T; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice', email: 'alice@example.com' }]; // Update existing (merges properties) - without identifier const result1 = upsertOne({ entity: { id: 1, name: 'Alice Updated' }, entities: users, }); // [{ id: 1, name: 'Alice Updated', email: 'alice@example.com' }] // Add new const result2 = upsertOne({ entity: { id: 2, name: 'Bob' }, entities: users, }); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] ``` *** ### upsertMany Updates multiple elements if they exist, otherwise adds them. If the entity has an `id` property, the `identifier` is optional. ```typescript function upsertMany(params: { newEntities: T[]; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [{ id: 1, name: 'Alice' }]; // Without identifier const result = upsertMany({ newEntities: [ { id: 1, name: 'Alice Updated' }, { id: 2, name: 'Bob' }, ], entities: users, }); // [{ id: 1, name: 'Alice Updated' }, { id: 2, name: 'Bob' }] ``` *** ### removeOne Removes an element by its id. If the entity has an `id` property, the `identifier` is optional. ```typescript function removeOne(params: { id: K; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; // Without identifier const result = removeOne({ id: 1, entities: users }); // [{ id: 2, name: 'Bob' }] // With custom identifier for entities without id const products = [{ sku: 'A1', name: 'Widget' }]; const result2 = removeOne({ id: 'A1', entities: products, identifier: (p) => p.sku, }); // [] ``` *** ### removeMany Removes multiple elements by their ids. If the entity has an `id` property, the `identifier` is optional. ```typescript function removeMany(params: { ids: K[]; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ]; // Without identifier const result = removeMany({ ids: [1, 2], entities: users }); // [{ id: 3, name: 'Charlie' }] ``` *** ### map Applies a transformation function to all elements. ```typescript function map({ mapFn, entities, }: { mapFn: (entity: T) => T; entities: T[]; }): T[]; ``` **Example:** ```typescript const users = [ { id: 1, name: 'alice' }, { id: 2, name: 'bob' }, ]; const result = map({ mapFn: (u) => ({ ...u, name: u.name.toUpperCase() }), entities: users, }); // [{ id: 1, name: 'ALICE' }, { id: 2, name: 'BOB' }] ``` *** ### mapOne Applies a transformation function to a single element by its id. If the entity has an `id` property, the `identifier` is optional. ```typescript function mapOne(params: { id: K; mapFn: (entity: T) => T; entities: T[]; identifier?: IdSelector; // Optional if T has id }): T[]; ``` **Example:** ```typescript const users = [ { id: 1, name: 'alice' }, { id: 2, name: 'bob' }, ]; // Without identifier const result = mapOne({ id: 1, mapFn: (u) => ({ ...u, name: u.name.toUpperCase() }), entities: users, }); // [{ id: 1, name: 'ALICE' }, { id: 2, name: 'bob' }] ``` *** ### computedTotal Returns the total count of entities. ```typescript function computedTotal({ entities }: { entities: T[] }): number; ``` **Example:** ```typescript const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; const total = computedTotal({ entities: users }); // 2 ``` *** ### computedIds Returns all ids from the entities list. If the entity has an `id` property, the `identifier` is optional. ```typescript function computedIds(params: { entities: T[]; identifier?: IdSelector; // Optional if T has id }): K[]; ``` **Example:** ```typescript const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; // Without identifier const ids = computedIds({ entities: users }); // [1, 2] // With custom identifier const products = [{ sku: 'A1', name: 'Widget' }]; const skus = computedIds({ entities: products, identifier: (p) => p.sku, }); // ['A1'] ``` ## See Also * [Collections](/guide/state/collections) — generating these as methods on a primitive * [Reacting to mutations](/guide/state/react-on-mutation) — the usual place to call them by hand --- --- url: https://ng-angular-stack.github.io/craft/guide/state/persistence.md --- # Persistence `insertStoragePersister` saves a primitive's value through the configured storage backend and restores it on the next visit — with expiry, background revalidation and a validation hook, so stale or corrupt entries don't leak into your app. **Use it when** a value should survive a reload: a draft, a preference, a list you'd rather show instantly than fetch again. **Not when** the value is sensitive, or when it must be correct rather than fast — a restored value is by definition a value from the past. Works with `state()`, `query()`, `mutation()` and `asyncProcess()`. ```typescript import { craftUnique, insertStoragePersister } from '@craft-ng/core'; ``` Configure the storage backend once in `appConfig`. The default application selection remains `localStorage`; a child route, feature or test can select `sessionStorage` instead. ```typescript import { LocalStoragePersister, SessionStoragePersister, provideLocalStoragePersister, provideSessionStoragePersister, provideStoragePersister, } from '@craft-ng/core'; providers: [ provideLocalStoragePersister(), provideSessionStoragePersister(), provideStoragePersister(function* () { return yield* LocalStoragePersister(); }), ]; ``` The `StoragePersister` provider is required by `craftAppConfig` and follows the normal Angular DI hierarchy. A child route, feature or test can override the active backend: ```typescript providers: [ provideStoragePersister(function* () { return yield* SessionStoragePersister(); }), ]; ``` ## The common case ```typescript const { myState } = state( 'myState', 0, insertStoragePersister(craftUnique({ storeName: 'myApp', key: 'myState', })), ); const { myQuery } = query( 'myQuery', { params: () => 'test', loader: async () => { return { data: 'testData' }; }, }, insertStoragePersister(craftUnique({ storeName: 'myApp', key: 'myQuery', })), ); ``` ## Options The identity (`storeName` + `key`) is the first argument, wrapped in `craftUnique` so the static graph can guarantee it appears only once. Options are the second argument. | Option | Type | Default | Description | | ------------------------------------------ | ----------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cacheTime` | `number` | `300000` | Time in ms after which cached data is deleted from the configured storage backend (garbage collection). Set to `0` to disable expiration. | | `staleTime` | `number` | `undefined` | Time in ms after which cached data is considered stale. The cached value is still restored immediately, but a background `reload()` is triggered (SWR pattern). Must be less than `cacheTime`. | | `validate` | `(value: unknown) => boolean` | `undefined` | Called on the deserialized value before restoring it. Return `false` to discard the entry and load fresh. Useful when the data model has changed. | | `waitForParamsSrcToBeEqualToPreviousValue` | `boolean` | `true` | If `true`, waits for the params signal to stabilize before trying to restore the cache. Useful when params start as `undefined`. Not applicable to `state()`. | ## cacheTime vs staleTime | | Data deleted? | Reload triggered? | | ------------------------ | ------------------------------------- | ------------------------------ | | **`cacheTime`** exceeded | Yes — entry removed from the configured backend | No | | **`staleTime`** exceeded | No — data is still restored | Yes — `reload()` in background | `cacheTime` always takes priority: if `cacheTime` is exceeded, the entry is discarded entirely, regardless of `staleTime`. ## SWR Pattern (staleTime) Use `staleTime` to display cached data immediately while silently refreshing in the background — the same pattern used by SWR and TanStack Query. ```typescript const { userQuery } = query( 'userQuery', { params: () => currentUserId(), loader: async ({ params }) => fetchUser(params), }, insertStoragePersister(craftUnique({ storeName: 'myApp', key: 'user', }), { cacheTime: 10 * 60_000, // delete from the configured backend after 10 min staleTime: 60_000, // show cached + reload in background after 1 min, }), ); // On page load: // - If cache is < 1 min old → status: 'local', no reload // - If cache is 1–10 min old → status: 'loading', value still visible (SWR) // - If cache is > 10 min old → entry deleted, loads fresh ``` ## Validation Use `validate` to guard against corrupt or outdated data in the configured storage backend (e.g. after a model change or manual user edit). Works with Zod or any type guard. ```typescript import { z } from 'zod'; const UserSchema = z.object({ id: z.string(), name: z.string() }); type User = z.infer; const { userQuery } = query( 'userQuery', { params: () => currentUserId(), loader: async ({ params }) => fetchUser(params), }, insertStoragePersister(craftUnique({ storeName: 'myApp', key: 'user', }), { validate: (v): v is User => UserSchema.safeParse(v).success, }), ); // If the stored value fails validation → entry is discarded, resource loads fresh // If it passes → restored normally ``` ## Parallel resources With `query(name, { identifier })`, each instance is cached individually under its identifier — no extra configuration: ```typescript const postsQuery = yield* query( 'postsQuery', { params: () => currentPostId(), identifier: (id) => id, loader: async ({ params }) => fetchPost(params), }, insertStoragePersister(craftUnique({ storeName: 'myApp', key: 'posts', }), { cacheTime: 15 * 60_000, staleTime: 2 * 60_000, }), ); ``` ## Pitfalls **`staleTime` must be smaller than `cacheTime`.** Otherwise the entry is deleted before it ever gets a chance to be revalidated. **A shipped model change invalidates nothing by itself.** Users carry the old shape in their configured storage backend. Use `validate` — that is what it is for. **Restoring a value is not the same as having loaded it.** Check `isPlaceHolderData` / the status before treating a restored value as fresh. ::: details Managing stored data globally Clearing, inspecting or migrating persisted entries across the whole app goes through [GlobalPersisterHandler](/guide/state/persistence-handler). It delegates to the active `StoragePersister`, so the built-in localStorage and sessionStorage backends clear their own persisted entries. ::: ## See Also * [GlobalPersisterHandler](/guide/state/persistence-handler) * [query](/guide/state/server-state) * [Insertions](/guide/concepts/insertions) — composing with other insertions * [Architecture rules](/guide/testing/architecture) — `assertCraftUnique` on storage identities, `assertPersistedPrimitiveHasUnique` when a persister has no identity --- --- url: https://ng-angular-stack.github.io/craft/guide/state/persistence-handler.md --- # GlobalPersisterHandler Clears everything `@craft-ng` has persisted through the active `StoragePersister`, in one call. **Use it when** cached data must not outlive a session boundary: logout, switching accounts, a "reset the app" action. **Not when** you want to invalidate one resource — reload that query, or give it a shorter `cacheTime` in [Persistence](/guide/state/persistence). ::: danger It clears everything There is no per-key variant. Every persisted query, mutation and async process goes. ::: ```typescript import { GlobalPersisterHandlerService, provideGlobalPersisterHandlerService, } from '@craft-ng/core'; providers: [provideGlobalPersisterHandlerService()]; ``` ## How it works The handler delegates to the active `StoragePersister`. The built-in localStorage and sessionStorage implementations remove every key that starts with the `ng-craft-` prefix from their respective backend. This ensures complete cleanup of all data cached by `@craft-ng`, including: * Persisted queries * Persisted mutations * Persisted async processes * Any other data cached by the `@craft-ng` persistence layer ## The common case — clearing on logout ```ts import { craftService, GlobalPersisterHandlerService } from '@craft-ng/core'; const { LogoutHandler } = craftService( { name: 'LogoutHandler', scope: 'toProvide' }, function* () { const persister = yield* GlobalPersisterHandlerService(); return { logout: () => persister.clearAllCache(), }; }, ); ``` ## Force refresh all data ```typescript const { CacheActions } = craftService( { name: 'CacheActions', scope: 'toProvide' }, function* () { const persister = yield* GlobalPersisterHandlerService(); return { clearCache: () => persister.clearAllCache() }; }, ); ``` ## Clear cache when switching accounts ```ts import { GlobalPersisterHandlerService, craftService } from '@craft-ng/core'; const { AccountSwitcher } = craftService( { name: 'AccountSwitcher', scope: 'toProvide' }, function* () { const persister = yield* GlobalPersisterHandlerService(); return { switchAccount: (accountId: string) => { persister.clearAllCache(); // Load the selected account... return accountId; }, }; }, ); ``` ::: details Other situations where this comes up ### 1. User Logout Remove all user-specific cached data when a user logs out to prevent data leakage to the next user. ```typescript logout() { this.persisterHandler.clearAllCache(); this.authService.logout(); } ``` ### 2. Privacy Compliance Ensure no sensitive data remains in the selected storage backend after a user session ends. ```typescript ngOnDestroy() { if (this.isPrivateMode) { this.persisterHandler.clearAllCache(); } } ``` ### 3. Development/Testing Quickly clear all cached data during development or testing. ```typescript resetCache() { if (environment.development) { this.persisterHandler.clearAllCache(); console.log('Cache cleared'); } } ``` ### 4. Data Corruption Recovery Clear potentially corrupted cached data and force fresh data loading. ```typescript handleDataError() { this.persisterHandler.clearAllCache(); this.showMessage('Cache cleared. Please refresh the page.'); } ``` ::: ## See Also * [Local Storage Persister](/guide/state/persistence) * [Query](/guide/state/server-state) * [Mutation](/guide/state/mutations) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/pagination-placeholder.md --- # Pagination placeholders `insertPaginationPlaceholderData` keeps the previous page on screen while the next one loads, so paging through a list never flashes an empty state. **Use it when** a query is paginated with an `identifier` per page. **Not when** you just want to avoid a flicker on a non-paginated query — a query already keeps its previous value while loading, with no configuration ([query](/guide/state/server-state)). ```typescript import { insertPaginationPlaceholderData } from '@craft-ng/core'; ``` ## The common case It is a **higher-order insertion**: call it with a config and pass the result to `query`. `config.initialValue` is both the default value and the page type — which is why `currentPageData` is a `Signal` that is **never `undefined`**. ```typescript const pagination = yield* state('pagination', 1); const { userQuery } = yield* query( 'userQuery', { params: pagination, identifier: (params) => '' + params, loader: function* ({ params }) { const response = yield* CraftHttpClient.get(({ response }) => ({ url: `/api/users?page=${params}`, success: response(), })); return response.json(); }, }, insertPaginationPlaceholderData({ initialValue: [] as User[] }), ); // Access the current page data (or placeholder data during loading) const data = userQuery.currentPageData(); // Check the loading status of the current page const status = userQuery.currentPageStatus(); // Determine if placeholder data is being shown const isPlaceholder = userQuery.isPlaceHolderData(); // Get the current page identifier const identifier = userQuery.currentIdentifier(); ``` ## Returned Properties | Property | Type | Description | | ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `currentPageData` | `Signal` | The data for the current page, or placeholder data from the previous page during loading. Falls back to `initialValue` (never `undefined`). | | `currentPageStatus` | `Signal` | The loading status of the current page (`'idle'`, `'loading'`, `'resolved'`, `'error'`) | | `isPlaceHolderData` | `Signal` | `true` when showing previous page data as a placeholder | | `currentIdentifier` | `Signal` | The identifier of the current page | ## Custom Outputs (`build` callback) Pass an optional second argument to attach your own computed values or methods next to the pagination outputs. Its helpers (`state`, `set`, `update`, `patch`) are scoped to the **current page** (the displayed data), so mutations only affect the page the user is looking at — other cached pages are left untouched. ```typescript const { usersQuery } = query( 'usersQuery', { params: pagination, identifier: (params) => `${params.page}-${params.pageSize}`, loader: function* ({ params }) { return yield* ApiService.getDataList(params); }, }, insertPaginationPlaceholderData( { initialValue: [] as Data[] }, ({ state, settledState, set }) => ({ // a computed derived from the current page totalOfUnCompletedData: craftComputed(function* () { return (yield* state()).filter((d) => !d.completed).length; }), settledCount: craftComputed(function* () { return (yield* settledState()).length; }), markAsCompleted: function* (id: string) { const current = yield* state(); return yield* set( current.map((d) => (d.id === id ? { ...d, completed: true } : d)), ); }, }), ), ); yield* usersQuery.totalOfUnCompletedData(); // number yield* usersQuery.markAsCompleted('42'); ``` The `build` context exposes: | Helper | Type | Description | | -------- | --------------------------------------- | ---------------------------------------------------- | | `state` | yieldable reader for `T` | The current page data (or `initialValue`) | | `settledState` | generator reader for `T` | The current page data only when loaded; suspends during the first load or a page transition | | `set` | yieldable write returning `T` | Replace the current page data (no-op if not loaded) | | `update` | yieldable write returning `T` | Update the current page data from its previous value | | `patch` | yieldable write returning `T` | Patch the current page data with a partial value | The pagination outputs (`currentPageData`, `currentPageStatus`, `isPlaceHolderData`, `currentIdentifier`) are also available in the `build` context. ::: details A full paginated component ```typescript import { button, craftComponent, div, each, ifBlock, span } from '@craft-ng/component'; import { craftComputed, query, state } from '@craft-ng/core'; export const UsersList = craftComponent( 'UsersList', {}, function* () { const page = yield* state('page', 1, ({ state, update, set }) => ({ next: () => update((value) => value + 1), previous: function* () { const current = yield* state(); return yield* set(Math.max(1, current - 1)); }, isFirst: craftComputed(function* () { return (yield* state()) === 1; }), label: craftComputed(function* () { return `Page ${yield* state()}`; }), })); const userQuery = yield* query( 'userQuery', { params: page, identifier: (page) => `page-${page}`, loader: async ({ params }) => (await fetch(`/api/users?page=${params}`)).json() as Promise, }, insertPaginationPlaceholderData({ initialValue: [] as User[] }), ); return { page, userQuery }; }, ({ page, userQuery }) => [ div( { class: function* () { return (yield* userQuery.isPlaceHolderData()) ? 'users-list loading' : 'users-list'; }, }, each( userQuery.currentPageData, { track: (user) => user.id }, (user) => UserCard({ user }), ), ), div({ class: 'pagination' }, [ button({ click: page.previous, disabled: page.isFirst }, 'Previous'), span(page.label), button({ click: page.next }, 'Next'), ]), ifBlock(userQuery.isPlaceHolderData, () => div({ class: 'loading-indicator' }, 'Loading new page…'), ), ], ); ``` ::: ## How it works 1. When the page parameters change, the insertion checks whether the new page's data is already cached. 2. If the new page is loading and has no data yet, it serves the previous page's data as a placeholder. 3. `isPlaceHolderData` tells you that is what is on screen — use it to dim the list or show a spinner. 4. Once the real data arrives, it switches over automatically. ## Pitfalls **It needs an `identifier`.** Without one page identity, there is no "previous page" to fall back to. **`initialValue` defines the page type.** Passing `[]` untyped collapses `currentPageData` to `never[]` — write `[] as User[]`. **Mutating through the `build` helpers only affects the current page.** Other cached pages are untouched, which is usually what you want, but means a global change needs a reload. ## See Also * [query](/guide/state/server-state) — the base primitive * [Reacting to mutations](/guide/state/react-on-mutation) * [Insertions](/guide/concepts/insertions) --- --- url: https://ng-angular-stack.github.io/craft/guide/state/schema-validation.md --- # Schema validation Primitives accept any schema implementing `StandardSchemaV1`, so Zod, Valibot, Effect or a hand-written schema all work — and none of them becomes a dependency of `@craft-ng`. **Use it when** data crosses a boundary you don't control: a method argument, a server response, a restored value. **Not when** the value never leaves your own typed code — TypeScript covers that already. ## Resource schemas Resource schemas correspond to different configurations. They are shown separately here so the documentation does not suggest that they can be combined in one declaration. ### Validating a method argument ```typescript const search = yield* query('search', { methodSchema: SearchInputSchema, method: (input) => ({ term: input.term }), loader: async ({ params }) => fetchResults(params), }); ``` `methodSchema` validates the argument received by `call`, `mutate` or `method`; the method then receives the schema output value. ### Validating reactive params ```typescript const products = yield* query('products', { paramsSchema: FiltersSchema, params: () => ({ page: 1, term: searchTerm() }), loader: async ({ params }) => fetchProducts(params), }); ``` `paramsSchema` validates the value produced by `params` or a reactive source. ### Validating the loader result This is the one that matters most: the loader is where **data you don't control** enters the app. ```typescript const products = yield* query('products', { loaderSchema: ProductsSchema, params: () => ({ page: 1 }), loader: async ({ params }) => fetchProducts(params), }); ``` `loaderSchema` covers more than the initial fetch — it validates loader results, **stream values**, and **local writes** through `set`, `update` and `patch`. So a value that enters the resource later, by any path, is checked the same way. If the schema transforms (a `.trim()`, a coercion, a rename), the resource publishes the **output** type — the rest of your code sees the transformed shape, not the raw one. ::: warning `response()` is a claim, not a check With `CraftHttpClient`, the type parameter only *asserts* what the endpoint returns. Nothing verifies it at runtime: ```typescript loader: function* () { return yield* CraftHttpClient.get(({ response }) => ({ url: '/api/products', success: response(), // trusted, never verified })); } ``` Two ways to make it real. Add `loaderSchema` to the query, which validates whatever the loader returns: ```typescript yield* query('products', { loaderSchema: ProductsSchema, loader: /* the CraftHttpClient call above */, }); ``` Or decode at the request itself — `response(...)` takes any `{ decode(input: unknown) }`, which every schema library provides: ```typescript success: response({ decode: (input) => ProductsSchema.parse(input) }), ``` Use `loaderSchema` when you want the failure to surface as a craft exception under `exceptions().parse.loader` and to obey the validation policy; use `decode` when the decoding belongs to the endpoint's own contract. ::: ## State State schemas are declared beside `$self` and validate initial values, writes, insertions and values produced by `craftComputed`: ```typescript const user = yield* state('user', { $self: { id: 123, name: 'Alice' }, schema: UserSchema, }); ``` The input type constrains `$self`; the exposed reader uses the schema output type. Invalid derived values keep the last valid value when the policy rejects them. ### Derived state A schema also validates every new value produced by a `craftComputed` while keeping the dependency reactive: ```typescript const price = yield* state('price', 10); const quantity = yield* state('quantity', 2, ({ set }) => ({ set })); const total = yield* state('total', { $self: craftComputed('totalSelf', function* () { return (yield* price()) * (yield* quantity()); }), schema: NonNegativeNumberSchema, }); console.log(yield* total()); // 20 yield* quantity.set(3); console.log(yield* total()); // 30 ``` When a derived value fails validation, the configured policy decides whether the last valid value is retained or the new value is accepted. ## Policy and exceptions The default policy rejects invalid values in development and accepts them in production. It can be replaced globally or locally: ```typescript provideCraftSchemaValidationPolicy(({ exception }) => { monitoring.captureException(exception); return { action: isDevMode() ? 'reject' : 'accept' }; }); ``` ```typescript query('products', { loaderSchema: ProductsSchema, schemaValidationPolicy: () => ({ action: 'reject' }), // ... }); ``` Rejected parses produce a `SCHEMA_VALIDATION_ERROR` with `scope: 'parse'`. Resource exceptions expose the stage through `exceptions().parse.method`, `exceptions().parse.params` and `exceptions().parse.loader`; states expose `exceptions().parse.state`. All four primitives expose `hasSchema()`, which is `true` when at least one schema is configured. ## See Also * [Anatomy of a primitive](/guide/concepts/primitive-anatomy) * [Persistence](/guide/state/persistence) — validating restored values * [Exceptions as values](/guide/concepts/exceptions) --- --- url: >- https://ng-angular-stack.github.io/craft/guide/patterns/inject-at-point-of-use.md --- # Inject at the point of use This page introduces the first **recommended approach** for structuring a Craft application. It is not a catalogue of "bad" Angular code: Angular and Craft make different trade-offs. The useful rule is simple: > **Get what you need where you need it.** Declare a dependency in the smallest factory that actually uses it. If a query needs an API method, the query yields that method. If a route guard needs the current user, the guard yields the user service. There is no need to add an intermediary method to a component just to forward the call. ## The usual Angular shape In a conventional Angular component, the API dependency is often injected into the class and then exposed through a method that performs the request: ```typescript export class TasksComponent { constructor(private readonly api: TaskApi) {} readonly tasks = signal([]); loadTasks() { this.api.list().subscribe((tasks) => this.tasks.set(tasks)); } } ``` This is perfectly valid Angular. But the component now owns several different responsibilities: it resolves the API, starts the request, stores the result, and usually has to reproduce loading and error handling as well. The actual dependency is also hidden from the outside. Looking at the public type of `TasksComponent` does not tell the compiler, a route, or a test that `TaskApi` is required. ## Craft puts the dependency next to the work With Craft, the component declares the query directly, and the query yields exactly the API operation it needs. In this example, `TaskApi` is a crafted service (or an existing Angular service adapted with [`toCraftService`](/guide/app/integrate-existing)): ```typescript import { craftComponent, each, ifBlock, li, p, ul } from '@craft-ng/component'; import { query } from '@craft-ng/core'; export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* query('tasks', { params: () => true, loader: function* () { return yield* TaskApi.list(); }, }); return { tasks }; }, ({ tasks }) => ifBlock( tasks.isLoading, () => p('Loading…'), () => ul( each( () => tasks.value() ?? [], { track: (task) => task.id }, (task) => li(task.title), ), ), ), ); ``` `TaskApi` is used directly from the `query` loader. The query owns the server state, while the template owns only the rendering of that state. There is no `loadTasks()` method, and no extra service whose only job is to forward this request. ## Why this is useful ### The dependency graph is explicit `yield* TaskApi.list()` is part of the factory's dependency type. Craft can use the same information for route DI checks, test registers, and dependency snapshots. A missing provider or mock is found at the boundary where it matters. ### Dependencies stay granular When a consumer needs one operation, yield that operation instead of the whole service: ```typescript const list = yield * TaskApi.list(); ``` The graph records the property that was used. Tests only need to provide `list`, and future changes to unrelated API methods do not expand this consumer's contract. ### Async behaviour has one owner `query` derives the loading, value, and exception state. The component does not need a second signal, subscription, or manual error flag that could drift away from the request. ## The rule of thumb * If a query or mutation needs an API operation, yield it in that query or mutation. * If a service needs another service, yield the dependency in that service's factory. * If a component needs a dependency directly, yield it in the component's factory. * Create a dedicated service when it owns reusable behaviour or a meaningful boundary — not merely to forward one method call. Direct does not mean unstructured. The dependency is still named, tracked, scoped, mockable, and exposed through a deliberate public API. It simply lives close to the code that uses it. ## See also * [The mental model](/guide/concepts/mental-model) — declare, yield, derive * [`craftService`](/guide/app/craft-service) — define and compose services * [Shaping a service's public API](/guide/app/expose-api) — expose only what a consumer needs * [Testing services](/guide/testing/services) — test the same dependency graph * [Architecture rules](/guide/testing/architecture) — constraints across that graph --- --- url: https://ng-angular-stack.github.io/craft/guide/app/craft-service.md --- # craftService A service is a factory with a **name** and a **scope** — not a class. It packages primitives and dependencies behind an explicit API, and keeps the whole dependency graph visible to the compiler. **Use it when** logic outgrows a single component field, or when two places need the same behaviour. **Not when** you are adapting an existing Angular service or token — that is [`toCraftService`](/guide/app/integrate-existing). The contrast with `inject(...)` scattered across classes is the point: dependencies here are explicit and **type-visible**, which is what the route DI check and the test registers read. ```typescript import { craftService } from '@craft-ng/core'; ``` Service inputs that can change should be consumed as yieldable readers (`CraftServiceInput`), the service counterpart of a component `Input`. Yield them so the input-to-service edge stays in the dependency graph: ```typescript import { craftService, query, type CraftServiceInput } from '@craft-ng/core'; const { UserQuery } = craftService( { name: 'UserQuery', scope: 'global' }, (inputs: { userId: CraftServiceInput }) => query('userQuery', { params: function* () { return yield* inputs.userId(); }, loader: ({ params }) => ApiService.getItemById(params), }), ); ``` The call site still accepts a resolved value, an Angular signal, or a Craft reader — the service boundary adapts it into that reader. Inside the factory, always `yield* inputs.x()`. ## What you get Declaring a service gives you a set of generated helpers. For one named `Counter`: * `Counter(...)` — consume or compose it inside a craft generator * `Counter.someProperty(...)` — derive one public property directly * `provideCounter(...)` — for provider-capable scopes * `COUNTER_META_DATA` — for metadata-driven tooling * `CounterRequirement` — for `abstract` services * `provideCounter(factory)` — on `abstract` services, to implement the contract inline Which of those exist depends on the scope. ::: warning Breaking change — no more `injectX` The generated helper is the service name itself: `X`. `craftService` no longer exports `injectX`, and the former `XToYield` helper is gone. Use `X()` in a craft generator and compose with `yield* X()`. ::: ## Supported scopes A service declares how many instances of it exist through `scope`: `function`, `toProvide`, `global`, `manuallyProvidedAtRoot` or `abstract`. Default to `function`. Each scope and when to pick it: **[Service scopes](/guide/app/service-scopes)**. ## The common case ```ts import { craftService, state } from '@craft-ng/core'; const { Counter } = craftService( { name: 'Counter', scope: 'global' }, function* () { const counter = yield* state('counter', 0, ({ update }) => ({ increment: () => update((value) => value + 1), decrement: () => update((value) => value - 1), })); return counter; }, ); const { CounterConsumer } = craftService( { name: 'CounterConsumer', scope: 'global' }, function* () { const counter = yield* Counter(); yield* counter.increment(); return counter; }, ); ``` ## Returning one primitive directly When a service exposes only one primitive, the factory can return its generator directly. `craftService` drives it and the generated service helper returns the primitive reference: ```typescript import { craftService, query, type CraftServiceInput, } from '@craft-ng/core'; const { UserQuery } = craftService( { name: 'UserQuery', scope: 'global' }, (inputs: { userId: CraftServiceInput }) => query('userQuery', { params: function* () { return yield* inputs.userId(); }, loader: ({ params }) => ApiService.getItemById(params), }), ); ``` For several primitives, use `craftYieldRecord`. It resolves every generator in the record and preserves the record keys: ```typescript import { craftService, craftYieldRecord, query, state, type CraftServiceInput, } from '@craft-ng/core'; const { UserQuery } = craftService( { name: 'UserQueryWithState', scope: 'global' }, (inputs: { userId: CraftServiceInput }) => craftYieldRecord({ userQuery: query('userQuery', { params: function* () { return yield* inputs.userId(); }, loader: ({ params }) => ApiService.getItemById(params), }), refresh: state('refresh', 0, ({ update }) => ({ increment: () => update((value) => value + 1), })), }), ); ``` Inside a generator factory, the equivalent explicit form remains available: `const userQuery = yield* query(...)`. ## Scoping providers to the service Use `providers` in the service config when the service factory itself needs locally-scoped dependencies: ```typescript const { UserFacade } = craftService( { name: 'UserFacade', scope: 'global', providers: [provideUserApi(), provideUserLogger()], }, function* () { const api = yield* UserApi(); const logger = yield* UserLogger(); return { rename: (user: { id: string; name: string }, name: string) => { logger.log(`rename:${user.id}`); return api.updateUser({ ...user, name }); }, }; }, ); ``` This is separate from `provideUserFacade()`, which is only generated for provider-capable scopes like `toProvide`. ## Composing services ```ts import { craftService, state } from '@craft-ng/core'; const { Counter } = craftService( { name: 'Counter', scope: 'global' }, function* () { const counter = yield* state('counter', 0, ({ update }) => ({ increment: () => update((value) => value + 1), })); return counter; }, ); const { CounterFacade } = craftService( { name: 'CounterFacade', scope: 'global' }, function* () { const counter = yield* Counter(); return { read: function* () { return yield* counter(); }, increment: function* () { return yield* counter.increment(); }, }; }, ); ``` ## Shaping the public API `yield* X()` can expose only part of a dependency, and `X.property()` derives a single one. See **[Shaping a service's public API](/guide/app/expose-api)**. ## Contracts without an implementation `scope: 'abstract'` declares a contract that a provider must satisfy later. See **[Abstract services](/guide/app/abstract-services)**. ## Startup work `craftService` also supports startup hooks through `appStart: true` and `yield* onAppStart(...)`. The callback can be a plain function or a generator function. Use the generator form when startup logic needs to `yield*` crafted dependencies: ```ts import { craftAppConfig } from '@craft-ng/core'; import { Console, craftService, onAppStart } from '@craft-ng/core'; const { AppStartLog } = craftService( { name: 'AppStartLog', scope: 'global', appStart: true, }, function* () { yield* onAppStart(function* () { yield* Console.log('startup log'); return Promise.resolve(); }); return true; }, ); // register the current service to the AppStartRegistry // it is auto-generated when used with the craft-ng ESLint plugin declare module '@craft-ng/core' { interface CraftAppStartRegistry { AppStartLog: typeof AppStartLog; } } // inside craftAppConfig export const appConfig = craftAppConfig({ appStart: { AppStartLog, }, }); ``` Dependencies used only inside that callback are still tracked on the parent service. ## Pitfalls **Reaching for `global` by default.** A global service is a singleton for the whole app, whether or not that was intended. Start at `function` — see [Service scopes](/guide/app/service-scopes). **`toProvide` without the provider.** Angular does not report a missing provider at compile time; the failure appears at runtime. The [route DI check](/guide/routing/setup) is what closes that hole. [Architecture tests](/guide/testing/architecture#assertroutediproofs) keep that check from quietly disappearing — a `CanRun` alias that nobody references still compiles. **Returning the whole world.** What a service returns is its API. Return the narrow thing; consumers that need more can yield more. **Calling `inject()` inside a craft factory.** It works and it is invisible to every check that makes this worthwhile. The `craft-ng/no-angular-inject` rule exists for exactly this. ## See Also * [Service scopes](/guide/app/service-scopes) — the one decision to make * [Shaping the public API](/guide/app/expose-api) * [Testing services](/guide/testing/services) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/service-scopes.md --- # Service scopes `scope` decides how many instances of a `craftService` exist and who has to provide it. It is the one decision to make when declaring a service. ::: tip Short version Default to `function`. Move to `toProvide` the day a child component needs the same instance. Use `global` only for genuinely app-wide state. ::: ## Supported Scopes ### `global` * singleton provided at root * ideal for app-wide services and shared state * no explicit `provideX()` helper ### `toProvide` * requires `provideX()` where the service is mounted * useful for feature-local service trees * works well with tests that need explicit providers ### `manuallyProvidedAtRoot` * explicit provider helper, but designed to be mounted at root * also exposes `XToProvide` for public provider composition * allows this scope to be yielded by global services, which is not possible with `toProvide` (it still requires explicit setup when testing with `setupCraftServiceTestingByRegister`). ### `function` * creates a fresh instance on each injection * useful for reusable factories with bindings and inputs ### `abstract` * declares a contract without implementation * exposes a requirement token to force a concrete implementation later ## Recommendations For Choosing a Scope * Prefer `function` for a service owned by a single component. It avoids an explicit provider and makes it clear the instance is not meant to be shared with other components or child components. * Move to `toProvide` when the same instance must be shared with child components, or across several components through a common parent or route. In that case, provide it at the component boundary, a parent component, or the route. * Be careful with `toProvide`: Angular does not report a compilation error when the provider is missing, so the failure usually appears at runtime instead. The [route DI check](/guide/routing/setup) closes that hole; [architecture tests](/guide/testing/architecture#assertroutediproofs) keep the check armed. * Use `global` when the instance is intentionally shared application-wide. * For startup-only logic that should run when the app boots but is not injected elsewhere, prefer `function` together with `provideAppInitializer(...)`. If the same instance also needs to be injected by other services, use `global` instead. ## See Also * [craftService](/guide/app/craft-service) * [Route providers](/guide/routing/route-providers) — providing a service from a route * [Testing services](/guide/testing/services) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/expose-api.md --- # Shaping a service's public API A service returns whatever should be public. These are the ways to consume less than everything a dependency exposes — which keeps the dependency graph precise, and therefore keeps inference and test registers small. ## Single Property Shortcut When only one public property is needed, `X.property()` is a shortcut for a one-property derivation. ```ts import { craftService } from '@craft-ng/core'; const { UsersApi } = craftService( { name: 'UsersApi', scope: 'global' }, () => ({ updateUser: (user: { id: string; name: string }) => Promise.resolve(user), getUsers: () => Promise.resolve([]), }), ); const { UserUpdater } = craftService( { name: 'UserUpdater', scope: 'global' }, function* () { const updateUser = yield* UsersApi.updateUser(); return { rename: (user: { id: string; name: string }, name: string) => updateUser({ ...user, name }), }; }, ); ``` For method properties on services without public inputs, the shortcut can call the method directly: ```typescript return yield * UsersApi.updateUser({ id: '1', name: 'Romain' }); ``` The shortcut accepts the same bindings as `X(...)`: ```typescript const increment = yield* Counter.increment({ initialValue: startAt }); ``` Use the full `X(bindings, expose)` form when deriving several properties, creating aliases, exposing `$self`, using symbol keys, or when a service property collides with a native function property such as `name`. ## Property Shortcut The same shortcut notation is available on the generated `X` helper. Use it inside a craft generator when only one property is needed: ```ts import { craftService, state } from '@craft-ng/core'; const { UsersApi } = craftService( { name: 'UsersApi', scope: 'global' }, function* () { const currentUser = yield* state('currentUser', { id: '1', name: 'Ada', }); return { updateUser: (user: { id: string; name: string }) => Promise.resolve(user), currentUser, }; }, ); const { CurrentUser } = craftService( { name: 'CurrentUser', scope: 'global' }, function* () { return yield* UsersApi.currentUser(); }, ); ``` The result carries the same dependency tracking as `yield* UsersApi()`, so testing utilities see exactly which property was accessed. For method properties on services without public inputs, the shortcut calls the method directly: ```typescript const update = yield * UsersApi.updateUser({ id: '1', name: 'New' }); ``` ## Nested Property Shortcuts When only a sub-property of a service output is needed, add a second `.property` before calling: ```ts import { craftService, state } from '@craft-ng/core'; const { SearchApi } = craftService( { name: 'SearchApi', scope: 'global' }, function* () { const isLoading = yield* state('isLoading', false); const data = yield* state('data', [] as string[]); return { usersQuery: { isLoading, data, }, }; }, ); const { SearchFacade } = craftService( { name: 'SearchFacade', scope: 'global' }, function* () { const isLoading = yield* SearchApi.usersQuery.isLoading(); return { isLoading }; }, ); ``` The dependency graph records only the accessed nested property (`derivedPropertiesUsed: { usersQuery: { isLoading: ... } }`), not the full `usersQuery` object. Testing utilities therefore only require the used sub-property in mock objects. The result of `yield* X.parent.child()` carries the same tracked dependency metadata, so `ExtractDeps` correctly surfaces the service dependency. ## OmitInputs When a service has public inputs, the no-arg form of a property shortcut is intentionally disabled at the type level, because calling without bindings would silently use default values and mask a missing dependency: ```ts import { craftService, type CraftServiceInput } from '@craft-ng/core'; const { Counter } = craftService( { name: 'Counter', scope: 'function' }, function* (inputs: { initialValue?: CraftServiceInput }) { const initialValue = inputs.initialValue ? yield* inputs.initialValue() : 0; return { count: initialValue }; }, ); ``` ```typescript // Fine — bindings are explicit const count = yield* Counter.count({ initialValue: startAt }); // Type error — no-arg call is forbidden when inputs exist // Counter.count(); ``` Use `X.OmitInputs.property()` to explicitly opt out of input bindings and use the defaults: ```typescript const count = yield* Counter.OmitInputs.count(); const count2 = yield* Counter.OmitInputs.count(); ``` `OmitInputs` is purely a type-level gate — at runtime it is transparent. `OmitInputs` composes with nested shortcuts: ```typescript const isLoading = yield* Counter.OmitInputs.userQuery.isLoading(); ``` ## Partial Exposure `yield* X()` can expose only the part of a dependency that should remain public. ```ts import { craftService, state } from '@craft-ng/core'; const { Counter } = craftService( { name: 'Counter', scope: 'toProvide' }, function* () { const counter = yield* state('counter', 0, ({ update }) => ({ increment: () => update((value) => value + 1), decrement: () => update((value) => value - 1), })); return counter; }, ); const { CounterExtended, provideCounterExtended } = craftService( { name: 'CounterExtended', scope: 'toProvide' }, function* () { return yield* Counter(undefined, ({ $self, increment }) => ({ $self, incrementCounter: increment, })); }, ); ``` This keeps the dependency graph precise, which is important for both type inference and testing. ## See Also * [craftService](/guide/app/craft-service) * [Testing services](/guide/testing/services) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/abstract-services.md --- # Abstract services An `abstract` service declares a **contract** with no implementation, and forces a concrete one to be supplied downstream. This is what makes a service's implementation a decision of the mounting site — a route, a feature config, a test — instead of a hard import. ## Abstract Requirements Use `scope: 'abstract'` to declare a contract that must be implemented elsewhere. ```ts import { abstract, craftService } from '@craft-ng/core'; type CounterContract = { (): number; increment(): void; }; const { CounterRequirement } = craftService( { name: 'Counter', scope: 'abstract' }, abstract(), ); ``` Concrete services can then depend on `CounterRequirement`. ## Abstract Providers An `abstract` service also exposes a `provideX(factory)` helper. It takes a **factory** — a plain function or a generator — produces a value matching the contract, and binds it to the requirement token. This lets you implement the contract **inline at the providing site** (a route, a component, a feature config) instead of declaring a separate concrete `craftService`. ```typescript import { abstract, craftService } from '@craft-ng/core'; type User = { name: string }; const { User, provideUser } = craftService( { name: 'User', scope: 'abstract' }, abstract(), ); // Implement the contract inline: const providers = [provideUser(() => ({ name: 'Ada' }))]; // Anywhere downstream, inside a craft generator: const user = yield * User(); ``` The factory can be a **generator** that yields other services. Everything it yields is tracked, so the resulting provider participates in the cascade DI check just like a regular service: ```typescript const { Greeting } = craftService( { name: 'Greeting', scope: 'global' }, () => ({ prefix: 'Hello' }), ); const providers = [ provideUser(function* () { const greeting = yield* Greeting(); return { name: `${greeting.prefix} Ada` }; }), ]; ``` This is the foundation of route-scoped providers: a route can implement an abstract contract from its own guarded data / params. See [Type-safe DI/Routes → Route Providers](/guide/routing/route-providers). ## See Also * [Service scopes](/guide/app/service-scopes) * [Route providers](/guide/routing/route-providers) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/integrate-existing.md --- # Integrating existing Angular code `toCraftService` adapts an existing Angular service, token or third-party dependency so it behaves like a craft service — yieldable, tracked, mockable in the same registers. **Use it when** you adopt craft progressively, or wrap a library you don't own. **Not when** you're writing the service yourself — declare it with [`craftService`](/guide/app/craft-service) directly. This is what makes adoption incremental: an app can be half Angular services and half craft services, with one dependency graph across both. ## Import ```typescript import { toCraftService } from '@craft-ng/core'; ``` ## Introduction `toCraftService` turns an existing Angular dependency into a composable service API that integrates with `craftService`. It is useful for dependencies like: * Angular services (`Router`, custom `@Injectable()` classes) * `InjectionToken` values * external callable objects returned by factories The generated API follows the same conventions as crafted services (`X`, optional `provideX`, optional `XToProvide`) and participates in typed dependency tracking. `toCraftService` does not export `injectX`, and it no longer exports the former `XToYield` helper. Consume the generated `X()` helper from a craft generator, typically with `yield* X()`. ## Supported Scopes `toCraftService` reuses the same scope semantics as `craftService`, with a subset of concrete scopes. ### `global` * singleton provided at root * supports both `token` and callback (`inject`) forms * callback form is only available on this scope ### `toProvide` * explicit provider helper required * requires `token` plus `provide` * exposes `provideX(...)` ### `manuallyProvidedAtRoot` * explicit provider helper plus public token * requires `token` plus `provide` * exposes both `provideX(...)` and `XToProvide` Scopes `function` and `abstract` are `craftService` scopes and are not available in `toCraftService`. ## Basic Global Example ```typescript import { Injectable, signal } from '@angular/core'; import { toCraftService } from '@craft-ng/core'; @Injectable({ providedIn: 'root' }) class RouterLike { readonly currentUrl = signal('/'); navigateByUrl(url: string) { this.currentUrl.set(url); return Promise.resolve(true); } } const { RouterLike } = toCraftService({ name: 'RouterLike', scope: 'global', token: RouterLike, }); const { Navigation } = craftService( { name: 'Navigation', scope: 'global' }, function* () { const router = yield* RouterLike(undefined, ({ navigateByUrl }) => ({ navigateByUrl, })); return { goToCheckout: () => router.navigateByUrl('/checkout'), }; }, ); ``` ## Global Callback Form ```typescript import { inject, InjectionToken } from '@angular/core'; import { toCraftService } from '@craft-ng/core'; const CURRENT_ROUTE = new InjectionToken<{ path: string }>('CurrentRoute'); const { CurrentRoute } = toCraftService({ name: 'CurrentRoute', scope: 'global', inject: () => inject(CURRENT_ROUTE), }); ``` ## Provider-Capable Example (`toProvide`) ```typescript import { Injectable, signal } from '@angular/core'; import { toCraftService } from '@craft-ng/core'; @Injectable() class CounterDriver { readonly total = signal(0); increment() { this.total.update((value) => value + 1); } } const { CounterDriver, provideCounterDriver } = toCraftService({ name: 'CounterDriver', scope: 'toProvide', token: CounterDriver, provide: () => [CounterDriver], }); // In tests or module providers: // providers: [provideCounterDriver()] ``` When an adaptation factory reads provider state, signal properties are exposed as yieldable readers and become dependency edges: ```typescript import { craftService, toCraftService } from '@craft-ng/core'; const { CounterValue } = toCraftService( { name: 'CounterValue', scope: 'toProvide', token: CounterDriver, provide: () => [CounterDriver], }, function* (counter) { return yield* counter.total(); }, ); ``` Consume `counter.total()` with `yield*` in the adaptation factory; ordinary methods and non-reactive provider properties keep their existing contract. ## `$provided` in Adaptation Inputs For `toProvide` and `manuallyProvidedAtRoot`, the adaptation factory can consume `$provided` internally while public bindings remain clean. ```typescript const { Catalog, provideCatalog } = toCraftService( { name: 'Catalog', scope: 'toProvide', token: CatalogDriver, provide: (provided: { apiBaseUrl: string }) => [ { provide: API_BASE_URL, useValue: provided.apiBaseUrl }, CatalogDriver, ], }, (catalog, inputs: { $provided: { apiBaseUrl: string }; prefix: string }) => ({ fetchPrefixedProducts: () => `${inputs.prefix}:${catalog.fetchProducts()}`, readProvidedBaseUrl: () => inputs.$provided.apiBaseUrl, }), ); ``` ## `HttpClient` Example `toCraftService` is also a good fit for Angular dependencies such as `HttpClient`. ```typescript import { HttpClient } from '@angular/common/http'; import { craftService, toCraftService } from '@craft-ng/core'; type User = { id: string; email: string }; const { HttpClient } = toCraftService({ name: 'HttpClient', scope: 'global', token: HttpClient, }); const { UsersApi } = craftService( { name: 'UsersApi', scope: 'global' }, function* () { const http = yield* HttpClient(undefined, ({ get, post }) => ({ get, post, })); return { listUsers: () => http.get('/api/users'), createUser: (payload: Pick) => http.post('/api/users', payload), }; }, ); ``` This keeps `HttpClient` explicit in the dependency graph while still letting you expose only the methods your service actually needs. ## Method Binding Behavior When adapting class instances, exposed methods stay bound to their original instance. This makes extracted methods like `navigateByUrl` safe to call after derivation. ## See Also * [craftService](/guide/app/craft-service) * [setupCraftServiceTestingByRegister](/guide/testing/services) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/app-start.md --- # App start `onAppStart` declares work that must run — and finish — before the application renders, owned by the service that needs it rather than by a global bootstrap file. **Use it when** something must be true before the first paint: a loaded config, a restored session, a feature-flag fetch. **Not when** the work can happen after render — that is just an effect, and blocking on it costs your users a blank screen. ## Import ```typescript import { onAppStart } from '@craft-ng/core'; ``` ## Overview `onAppStart(...)` is used inside a `craftService(..., function* () {})` generator to declare logic that should run when the application starts. Important constraints: * the owning service must be declared with `appStart: true` * a service can declare `yield* onAppStart(...)` only once * the callback can be a plain function or a generator function * nested `onAppStart(...)` calls inside the callback are not supported `craftAppConfig(...)` runs registered app-start services during Angular application initialization. ## Signature ```typescript function onAppStart( run: () => Observable | Promise | void, ): Generator; function onAppStart( run: () => Generator< Yielded, Observable | Promise | void, unknown >, ): Generator; ``` ## Plain Callback Use a plain callback when startup logic does not need to `yield*` crafted dependencies. ```ts import { craftService, onAppStart } from '@craft-ng/core'; export const { StartupFlag } = craftService( { name: 'StartupFlag', scope: 'global', appStart: true, }, function* () { yield* onAppStart(() => { console.log('app started'); return Promise.resolve(); }); return true; }, ); ``` ## Generator Callback Use a generator callback when startup logic needs to `yield*` crafted dependencies. ```typescript import { Console, craftService, onAppStart } from '@craft-ng/core'; export const { AppStartLog } = craftService( { name: 'AppStartLog', scope: 'toProvide', appStart: true, }, function* () { yield* onAppStart(function* () { yield* Console.log('This is a log from the appStart callback'); return new Promise((resolve) => setTimeout(resolve, 1000)); }); return 1; }, ); ``` The callback generator supports the same dependency-yield semantics as a normal crafted generator for: * `yield* X(...)` * `yield*` exposure tokens returned by derivation callbacks * browser boundaries such as `yield* Console.log(...)` Dependencies used only inside this callback are merged into the parent service dependency graph. ## Registering it with `craftAppConfig` Declaring `onAppStart` is only half of it — nothing runs until the service is **registered**. Two steps, and both are mechanical. Augment the app-start registry so the service is known by name: ```typescript declare module '@craft-ng/core' { interface CraftAppStartRegistry { AppStartLog: typeof AppStartLog; } } ``` Then list it in `craftAppConfig`: ```typescript export const appConfig = craftAppConfig({ appStart: { AppStartLog, }, providers: [ /* … */ ], }); ``` `craftAppConfig` runs every registered app-start service during Angular's application initialization, and the app renders once they have settled. Here it is end to end: ```ts import { Console, craftAppConfig, craftService, onAppStart } from '@craft-ng/core'; const { AppStartLog } = craftService( { name: 'AppStartLog', scope: 'global', appStart: true, }, function* () { yield* onAppStart(function* () { yield* Console.log('startup log'); return Promise.resolve(); }); return true; }, ); declare module '@craft-ng/core' { interface CraftAppStartRegistry { AppStartLog: typeof AppStartLog; } } export const appConfig = craftAppConfig({ appStart: { AppStartLog }, }); ``` ::: tip The registry augmentation is generated The `declare module` block is written for you by the craft-ng ESLint plugin — you rarely type it by hand. ::: ::: warning A declared hook that is never registered simply never runs It is not an error: `appStart: true` and `yield* onAppStart(...)` describe the service, the `appStart` map in `craftAppConfig` is what activates it. If startup logic silently doesn't happen, check the map first. ::: ## Dependency Tracking Generator callbacks are type-visible. If the callback only uses `Console`, the owning service dependency graph includes `ConsoleService` as a normal dependency node, with `browserBoundary: true`. This means startup-only dependencies are still visible to: * `GetServiceDependencies` * route/app DI checks built on top of service metadata * test helpers that inspect crafted dependency graphs ## Runtime Behavior `onAppStart(...)` does not run when the service instance is created. It registers a startup hook that is executed when the application initializer runs that service, typically through `craftAppConfig(...)`. If the callback returns: * `void`: startup continues immediately * `Promise`: startup waits for the promise to resolve * `Observable`: startup waits through Angular's initializer handling Generator callbacks preserve the same waiting behavior. The generator itself resolves first, then its returned `Promise` / `Observable` / `void` is used as the startup result. ## Common Errors ### Missing `appStart: true` ```typescript yield * onAppStart(() => undefined); ``` This throws at runtime if the owning service was not declared with `appStart: true`. ### Nested `onAppStart(...)` ```typescript yield * onAppStart(function* () { yield* onAppStart(() => undefined); // unsupported return undefined; }); ``` Nested declarations are rejected at runtime. ## See Also * [`craftService`](/guide/app/craft-service) * [`Browser Boundaries`](/guide/testing/browser-boundaries) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/lazy-services.md --- # Lazy services `craftLazy(load)` code-splits a **service**, the way `loadComponent` code-splits a component — and reuses the same retry and cache-busting engine. **Use it when** an expensive dependency is only needed on some paths: a PDF renderer, a chart library, an admin-only API client. **Not for** a service every route needs — the extra round-trip buys nothing. `craftLazy(load)` lazily imports a module **on demand** from inside an async craft driver — an [`asyncProcess`](/guide/state/async-process) loader, or a route guard/resolver — reusing the exact same retry + cache-busting engine as route lazy loading ([`loadComponent` / `loadChildren`](/guide/routing/route-load-errors)). Use it when you want to code-split a **service function** (an exported `craftGen`, an API helper, a heavy computation) and only fetch its chunk when it is actually needed, while keeping Craft's `status` / `exception` / `reload` semantics. ## Why not a manual dynamic import? The reflex is to reach for a manual `import()` and inject/call the result imperatively (an `injectAsync`-style helper): ```ts // ❌ manual: no status, no typed exceptions, no retry, not reactive async function runSearch(q: string) { const { search } = await import('./search'); // may throw on a stale chunk return search(q); // exceptions are untyped, failures are unhandled } ``` `craftLazy` replaces that with a first-class craft program: | Concern | Manual `import()` | `craftLazy` | | ------------------------------------ | ---------------------- | -------------------------------------------------------- | | Loading / resolved / exception state | you wire it by hand | inherited from the enclosing `asyncProcess` (`status()`) | | Stale-chunk retry after a redeploy | none | shared `withRetry` cache-busting engine | | Import failure | an unhandled rejection | a typed `CRAFT_LAZY_LOAD_ERROR` exception | | The module's own business exceptions | erased to `any` | preserved and propagated through the type system | | Recovery | manual `try/catch` | `.pipe(catchTag(...))` or route `handleExceptions` | ## Signature ```ts craftLazy(load: (helpers: CraftLazyLoadHelpers) => Promise): CraftGenInvocation; interface CraftLazyLoadHelpers { // Wrap the dynamic import so a chunk whose hashed URL went stale after a // redeploy is re-fetched with a cache-busting query param. withRetry(moduleImport: Promise): Promise; } ``` * `craftLazy(...)` is a [`craftGen`](/guide/concepts/generators) program: `yield*`-composable and [`.pipe(...)`](/guide/advanced/program-operators)-able. * Its resolved value is the module `T`, **untouched** — the module's exported `craftGen`s keep their own exception unions. * On a final import failure it returns a `CraftLazyLoadError` (`code: 'CRAFT_LAZY_LOAD_ERROR'`), which `craftGen` surfaces as a short-circuit → the enclosing resource's `status()` becomes `'exception'`. ::: warning It must run in an async driver `craftLazy` awaits its import through the async program pump, so it can only be `yield*`-ed from an **`asyncProcess` loader** or a **route guard/resolver**. It cannot be used inside a synchronous [`craftMethod`](/guide/reactivity/craft-method) (that driver throws on an await request). A `craftMethod` may only *trigger* the enclosing `asyncProcess`. ::: ## With `asyncProcess` The module to split — an exported `craftGen`: ```ts // search.ts (its own chunk) import { craftGen } from '@craft-ng/core'; import { SearchApi } from './search-api'; export const search = craftGen(function* (q: string) { const api = yield* SearchApi(); return yield* api.search(q); // may raise E1 | E2 }); ``` Load it from an `asyncProcess` loader. The simplest form triggers on demand with the generated `method`: ```ts import { asyncProcess, craftLazy } from '@craft-ng/core'; const searchModule = yield* asyncProcess('searchModule', { method: () => undefined, // call searchModule.method() to start loading loader: function* () { return yield* craftLazy(({ withRetry }) => withRetry(import('./search'))); }, }); ``` `searchModule.status()` walks `idle → loading → resolved` (or `exception`), exactly like any other `asyncProcess`, so the template can drive the UI: ```html @switch (searchModule.status()) { @case ('loading') { } @case ('exception') { } } ``` To **prefetch** as soon as some event fires (the reactive equivalent of an eager `injectAsync`), bind the process to a source instead of a `method`: ```ts import { asyncProcess, craftLazy, on$ } from '@craft-ng/core'; const searchModule = yield* asyncProcess('searchModule', { // load at the first emission of the source (e.g. on focus of the search box) method: on$(searchFocused$, () => undefined), loader: function* () { return yield* craftLazy(({ withRetry }) => withRetry(import('./search'))); }, }); ``` ### Load once, use many The canonical pattern: one `asyncProcess` owns the module, a second one awaits it with [`craftUntilSettled`](/guide/routing/guards) and calls the loaded function. Wrapping both in a [`craftService`](/guide/app/craft-service) exposes a clean API: ```typescript import { asyncProcess, craftLazy, craftService, craftUntilSettled, on$, } from '@craft-ng/core'; const { Search } = craftService({ name: 'Search', scope: 'component' }, () => { // prefetch the module at the first emission of the source const searchModule = yield* asyncProcess('searchModule', { method: on$(searchFocused$, () => undefined), loader: function* () { return yield* craftLazy(({ withRetry }) => withRetry(import('./search')), ); }, }); // run a search on a user action — triggerSearch(q) sets the params const searchResult = yield* asyncProcess('searchResult', { method: (q: string) => q, loader: function* ({ params: q }) { const { search } = yield* craftUntilSettled(searchModule); // wait for the chunk return yield* search(q); }, }); return { searchModule, searchResult }; }); ``` Exception propagation is fully typed, with **no** manual plumbing: * `craftLazy` may add `CRAFT_LAZY_LOAD_ERROR`; * `craftUntilSettled(searchModule)` relays it to `searchResult`; * `search(q)` relays its own `E1 | E2`. So `searchResult.exception()?.code` is exactly `'CRAFT_LAZY_LOAD_ERROR' | 'E1' | 'E2'`, and `searchResult.value()` keeps the return type of `search`. ## In routes Guards and resolvers are async drivers too, so you can `yield* craftLazy(...)` directly inside them. A failed import surfaces as `CRAFT_LAZY_LOAD_ERROR` and flows into the route's [exception handlers](/guide/concepts/exceptions), exactly like any other guard/resolver exception: ```ts craftRoute( 'search', { resolve: craftResolve(function* () { const { search } = yield* craftLazy(({ withRetry }) => withRetry(import('./search')), ); return yield* search('*'); }), }, { CRAFT_LAZY_LOAD_ERROR: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'offline' }); }), E1: craftExceptionHandler(function* () { return [] as Result[]; }), // E2 left unhandled → surfaces as a route exception }, ); ``` This is the code-splitting counterpart of a lazy `loadComponent`: instead of splitting the *component*, you split the *data-loading logic* it depends on, with the same retry + error screen guarantees as [Route Load Errors](/guide/routing/route-load-errors). ## Handling the load error `CRAFT_LAZY_LOAD_ERROR` is an ordinary craft exception, so all the usual tools apply. **Catch it at the source** (fall back to another module or a default), which removes it from the exception union: ```ts loader: function* () { return yield* craftLazy(({ withRetry }) => withRetry(import('./search'))).pipe( catchTag('CRAFT_LAZY_LOAD_ERROR', function* () { return yield* craftLazy(({ withRetry }) => withRetry(import('./search-fallback'))); }), ); } ``` **Catch a business exception of the loaded function** — `search(q)` is itself a pipeable `craftGen`: ```ts loader: function* ({ params: q }) { const { search } = yield* craftUntilSettled(searchModule); return yield* search(q).pipe( catchTag('E1', function* () { return [] as Result[]; }), // E2 stays in searchResult.exceptions() ); } ``` **Read it reactively** — anything left uncaught keeps `status()` at `'exception'` and shows up in `exceptions()` / `hasException()`, ready to render in the template. See [Program Operators](/guide/advanced/program-operators) for `catchTag` / `catchTag.exhaustive`. ## Retry & cache-busting `withRetry(import(...))` is what makes a stale chunk recover after a redeploy: on failure the chunk URL is re-fetched with a cache-busting query param. The attempt/back-off policy is injectable and defaults to the shared craft loader retry (one retry, 250 ms): ```ts import { provideCraftLazyLoadRetry } from '@craft-ng/core'; providers: [ provideCraftLazyLoadRetry({ attempts: 2, delayMs: (error, ctx) => 250 * ctx.attempt, shouldRetry: (error) => isRecoverable(error), }), ]; ``` The dynamic `import(url)` used for cache-busting is itself overridable through `CRAFT_DYNAMIC_IMPORT` (useful in tests). This is the very same engine as [route load retry](/guide/routing/route-load-errors), so a `craftLazy` import and a lazy route load behave identically under a bad deployment. ## API | Export | Purpose | | ------------------------------------------------------------- | ---------------------------------------------------------------- | | `craftLazy(load)` | Lazily import a module from an async craft driver. | | `CraftLazyLoadHelpers` | The `{ withRetry }` helpers passed to `load`. | | `CraftLazyLoadError` / `CRAFT_LAZY_LOAD_ERROR_CODE` | The exception (and its code) returned on a final import failure. | | `provideCraftLazyLoadRetry(config)` / `CRAFT_LAZY_LOAD_RETRY` | Configure the `craftLazy` retry policy. | | `CRAFT_DYNAMIC_IMPORT` | Override the dynamic `import(url)` (cache-busting / tests). | ## See Also * [craftService](/guide/app/craft-service) * [asyncProcess](/guide/state/async-process) — the usual driver for `craftLazy` * [Route load errors](/guide/routing/route-load-errors) — the same retry engine --- --- url: https://ng-angular-stack.github.io/craft/guide/app/register.md --- # craftRegisterFor `craftRegisterFor` exposes, within a Craft injection scope, the services, components and directives that are **currently alive** in it. **Use it when** a parent must drive several children without each child having to push a bespoke API upwards: counters, audio players, selected items, validation across a form section. **Not when** one known child is involved — pass it a service or an input instead. A registry trades explicitness for reach. ## Declaring a registry The registry is typed from the Craft targets it accepts: ```ts import { craftComputed, craftRegisterFor } from '@craft-ng/core'; const { RegisterForCounter, provideRegisterForCounter } = craftRegisterFor( 'Counter', [Counter, CounterChild], ); ``` The first argument is the registry's mandatory name. It generates the two public helpers `RegisterForCounter` and `provideRegisterForCounter`, a convention that lets several registries coexist in one scope without name collisions. With a single target, the array can be omitted: ```ts const { RegisterForCounter } = craftRegisterFor( 'Counter', Counter, ({ Counter }) => ({ total: craftComputed('total', function* () { return (yield* Counter())?.length ?? 0; }), }), ); const counters = yield* RegisterForCounter(); const total = craftComputed('total', function* () { return (yield* counters())?.length ?? 0; }); ``` If a projection uses several groups, every target must be declared: ```ts craftRegisterFor( 'Counter', [Counter, CounterChild], ({ Counter, CounterChild }) => ({ total: craftComputed('total', function* () { return (yield* Counter())?.length ?? 0; }), incrementAll: function* () { for (const { ref } of (yield* CounterChild()) ?? []) { yield* ref.increment(); } }, }), ); ``` Then add the providers returned by `provideRegisterForCounter()` to the scope that should observe the instances: ```ts export const RegisterForDemo = craftComponent({ name: 'RegisterForDemo', providers: [provideRegisterForCounter()], // ... }); ``` By default the registry also includes `global` services resolved under that scope. To restrict observation to services whose scope matches the parent: ```ts craftRegisterFor('Counter', [Counter], { includeGlobal: false }); ``` The first declared target is reachable through `RegisterForCounter()` directly; additional targets get their own property, e.g. `RegisterForCounter.CounterChild()`. ## The common case — driving child components Each child creates a `toProvide` service, and the parent providing the registry observes them: ```ts const { Counter, provideCounter } = craftService( { name: 'Counter', scope: 'toProvide' }, function* () { const counter = yield* state( 'counter', 0, ({ update }) => ({ increment: () => update((value) => value + 1), decrement: () => update((value) => value - 1), }), ); return counter; }, ); const CounterChild = craftComponent( 'CounterChild', { providers: [provideCounter()] }, function* () { return yield* Counter(); }, ({ counter }) => div(counter), ); const { RegisterForCounter, provideRegisterForCounter } = craftRegisterFor( 'Counter', [Counter, CounterChild], ); const CounterBoard = craftComponent( 'CounterBoard', { providers: [provideRegisterForCounter()] }, function* () { const counters = yield* RegisterForCounter(); const children = yield* RegisterForCounter.CounterChild(); return { incrementAll: function* () { for (const { ref } of (yield* counters()) ?? []) { yield* ref.increment(); } }, childCount: craftComputed('childCount', function* () { return (yield* children())?.length ?? 0; }), }; }, ({ incrementAll, childCount }) => section([ button({ click: incrementAll }, 'Increment every child'), p(function* () { return `Active children: ${yield* childCount()}`; }), each([1, 2, 3], () => CounterChild({})), ]), ); ``` When a child is added, its `Counter` appears in the group. When it leaves the DOM, the group updates on its own. ## Reading a group Groups are yieldable from a Craft factory. Their signal is `undefined` while no instance is registered, and returns to `undefined` when the last one is destroyed: ```ts const counters = yield* RegisterForCounter(); const incrementAll = function* () { for (const { ref } of (yield* counters()) ?? []) { yield* ref.increment(); } }; ``` Each entry carries: * `ref` — the value produced by the service, or the context returned by the component/directive factory; * `hostName` — the name of the host scope that created the entry. The signal is live: the parent never re-subscribes when a child appears or disappears. ## Partial exposure As with `craftService`, a group can expose only the façade the parent needs. The first argument stays `undefined` to keep the yieldable-helper syntax, and `$self` is the group's full signal: ```ts const childComponents = yield* RegisterForCounter.CounterChild( undefined, ({ $self }) => ({ total: craftComputed(function* () { return (yield* $self())?.length ?? 0; }), incrementAll: function* () { for (const { ref } of (yield* $self()) ?? []) { yield* ref.increment(); } }, decrementAll: function* () { for (const { ref } of (yield* $self()) ?? []) { yield* ref.decrement(); } }, }), ); ``` The parent then keeps only `total`, `incrementAll` and `decrementAll`. The dependency stays precise — the computed values read the group's signal, and instances are still added and removed automatically. ## Derived registry properties To share common projections, the second parameter of `craftRegisterFor` receives the groups' signals directly: ```ts const { RegisterForCounter, provideRegisterForCounter } = craftRegisterFor( 'Counter', [Counter, CounterChild], ({ Counter, CounterChild }) => ({ totalCounter: craftComputed('totalCounter', function* () { return (yield* Counter())?.length ?? 0; }), incrementAllCounterChild: function* () { for (const { ref } of (yield* CounterChild()) ?? []) { yield* ref.increment(); } }, decrementAllCounterChild: function* () { for (const { ref } of (yield* CounterChild()) ?? []) { yield* ref.decrement(); } }, }), ); ``` Each derived property becomes a yieldable helper: ```ts const totalCounter = yield* RegisterForCounter.totalCounter(); const incrementAll = yield* RegisterForCounter.incrementAllCounterChild(); console.log(yield* totalCounter()); yield* incrementAll(); ``` For a single-target registry, the main call also returns the signal enriched with those derived properties — so the value stays callable for the raw entries while exposing `total` and the added methods: ```ts const childComponents = yield* RegisterForCounterChild(); const entries = yield* childComponents(); const total = yield* childComponents.total(); yield* childComponents.incrementAllChildCounter(); yield* childComponents.decrementAllChildCounter(); ``` In a Craft template, pass a method straight to an event and call signals inside a reactive callback: ```ts button({ click: childComponents.incrementAllChildCounter }, 'Increment all'); span(function* () { return `Children: ${yield* childComponents.total()}`; }); ``` The main group, the additional groups and the derived properties can all be used together. Derived properties are computed once per registry injector and keep the reactive signals the groups provide. ## Registering a directive Craft directives can be targets too: ```ts const { RegisterForCounter } = craftRegisterFor('Counter', [ CounterChild, CounterDebugDirective, ]); const debugEntries = yield* RegisterForCounter.CounterDebugDirective(); debugEntries()?.forEach(({ hostName, ref }) => { console.debug('directive active', hostName, ref); }); ``` A functional directive has no class instance, so `ref` is the factory context of the decorated component. Its `hostName` remains specific to the directive and its instance, which is what lets you tell several identical directives apart on the same screen. ## Lifecycle and references Services are registered when their yield resolves. The runtime attaches their removal to the destruction of the injector that carries them. Craft components and directives are functional factories with no class instance, so `ref` is their factory context. For a directive used with `.pipe(...)`, the final component's context is exposed, because that is the execution scope the directive shares. Every Craft component automatically gets a host tag of the form `component:#`, so `provideHostName` is not needed in a component's providers — it stays useful only to override that automatic name. Directives applied to an element get their own `hostName`, generated from the directive name and an instance id. These names distinguish two identical instances and are usable for diagnostics and observability. Entries are removed automatically in every case: destruction of the component/directive, destruction of its DI scope, or replacement of a composition. ## Pitfalls ::: warning An empty registry is not an error Compilation checks that the target you pass to `craftRegisterFor` is a valid Craft service, component or directive — but it cannot check that an instance will ever be created. If no registered target exists in the executed code, there is no compile error and no runtime error: the signal is simply `undefined`. ::: ::: warning Craft targets only `craftRegisterFor` does not detect arbitrary Angular classes. It targets `craftService`, `craftComponent` and `craftDirective`, whose scope and lifecycle the runtime knows. ::: **Declaring the same target twice** in the list is not supported — each target appears once. **Treating the group signal as always populated.** It is `undefined` before the first instance and after the last one; the `?.` is not optional. ::: details Extending the mechanism — target and yield wrappers The registry rests on two separate pieces: 1. a **yield wrapper** observes services as they are actually resolved; 2. the component/directive **runtime** reports their creation and ties cleanup to their lifecycle. The first is `provideCraftTargetWrapper`, documented on [Target wrapper](/guide/app/target-wrapper). The second is `provideServiceYieldWrapper`, the low-level hook `craftRegisterFor` uses to wrap every Craft service resolution in the scope where the yield runs — deliberately close to `provideFnWrapper`, but limited to service yields: ```ts import { provideServiceYieldWrapper, type ServiceYieldContext, } from '@craft-ng/core'; function* reportServiceYield( context: ServiceYieldContext, next: () => Generator, ) { const startedAt = performance.now(); const value = yield* next(); console.debug('service resolved', { name: context.name, hostScope: context.hostScope, duration: performance.now() - startedAt, }); return value; } export const providers = [ provideServiceYieldWrapper( 'Warning: the wrapper runs in the current Craft injection context.', reportServiceYield, ), ]; ``` `context.resolve()` resolves the real service; `next()` keeps the wrapper chain intact. Wrappers compose in registration order — the first is the outermost. The context provides `name`, `scope`, `hostScope`, `injector` and `resolve`. Like `provideFnWrapper`, this hook suits cross-cutting concerns — registries, metrics, traces, diagnostics — not business logic. A new tool can reuse `provideServiceYieldWrapper` to observe services without `craftRegisterFor` at all. For functional Craft targets the runtime also exposes its internal registration primitives, so another specialised view can be built — but `craftRegisterFor` stays the recommended application-level API. ::: ## See Also * [Target wrapper](/guide/app/target-wrapper) — the extension point underneath * [craftService](/guide/app/craft-service) * [Customization](/guide/components/customization) --- --- url: https://ng-angular-stack.github.io/craft/guide/app/target-wrapper.md --- # Target wrapper `provideCraftTargetWrapper` wraps the registration of **every Craft component or directive created in the current injector**, giving you a hook at the moment each one comes to life. **Use it when** you need to observe or enrich target registration across a subtree: a specialised registry, observability, host names decorated with tags. **Not when** you just need a parent to drive its children — [`craftRegisterFor`](/guide/app/register) is built on this and already does it. ::: warning Dependency injection here is not type-checked The callback runs in a runtime chain, outside the usual DI inference. A service that is not provided in the current injector fails **at runtime**, and the wrapper's type cannot catch it. That is why the first argument is a mandatory warning string. ::: ## The common case ```ts import { provideCraftTargetWrapper } from '@craft-ng/core'; const provideTargetCustomization = provideCraftTargetWrapper( 'Warning: dependency injection here is not type-safe and may fail at runtime', function* (context, next) { return yield* next(); }, ); ``` The callback is a generator, so it can yield a Craft service: ```ts const provideTargetAudit = provideCraftTargetWrapper( 'Warning: dependency injection here is not type-safe and may fail at runtime', function* (context, next) { const audit = yield* TargetAuditService(); audit.recordCreatedTarget(context.kind, context.name); return yield* next(); }, ); ``` ## The context ```ts type CraftTargetContext = { target: unknown; kind: 'component' | 'directive'; name: string; ref: unknown; hostName: string; injector: Injector; }; ``` `target`, `kind`, `name` and `ref` describe the real instance and are immutable. **`hostName` is the only field you can change**, by passing it to `next(...)`. ## Tagging the host name ```ts import { HOST_TAG_LIST, provideCraftTargetWrapper } from '@craft-ng/core'; const provideTagBasedTargetRegistration = provideCraftTargetWrapper( 'Warning: dependency injection here is not type-safe and may fail at runtime', function* (context, next) { const tags = context.injector.get(HOST_TAG_LIST, []); const hostName = tags.length === 0 ? context.hostName : `${tags.join('/')}/${context.hostName}`; return yield* next({ hostName }); }, ); ``` Install it in the component's scope: ```ts const RegisterForDemo = craftComponent( 'RegisterForDemo', { providers: [provideTagBasedTargetRegistration], }, // ... ); ``` Order matters — a wrapper that modifies the `hostName` a registry consumes must be declared **before** that registry's wrapper: ```ts providers: [ provideTagBasedTargetRegistration, provideRegisterForCounter(), ], ``` Wrappers chain in declaration order; the first is the outermost, exactly like `provideFnWrapper`. ## `next()` and cleanup `next()` continues the chain and **returns a release function**, because the wrappers after yours may have added a registration or a resource of their own. A wrapper that only adapts the `hostName` just delegates: ```ts function* wrapper(context, next) { return yield* next({ hostName: `tag:${context.hostName}` }); } ``` A wrapper that creates its own resource must combine both cleanups: ```ts const provideObserver = provideCraftTargetWrapper( 'Warning: dependency injection here is not type-safe and may fail at runtime', function* (context, next) { const releaseNext = yield* next(); const releaseObserver = observeTarget(context); return () => { releaseObserver(); releaseNext(); }; }, ); ``` The runtime calls the cleanup automatically when the component's injector is destroyed. For a directive, it runs when the rendered node is removed. ## Pitfalls **Dropping the release function from `next()`.** Everything registered further down the chain then leaks. Always return it, alone or combined with your own. **Declaring the wrapper after the registry it should influence.** The registry will have already consumed the unmodified `hostName`. **Assuming a yielded service exists.** Nothing checks it here — a missing provider is a runtime failure. ::: details Building a specialised registry A registry can use the wrapper directly, without depending on `craftRegisterFor`: ```ts const provideSpecializedRegistry = provideCraftTargetWrapper( 'Warning: dependency injection here is not type-safe and may fail at runtime', function* (context, next) { const registry = yield* SpecializedRegistry(); const releaseNext = yield* next(); const releaseRegistry = registry.add({ kind: context.kind, name: context.name, ref: context.ref, hostName: context.hostName, }); return () => { releaseRegistry(); releaseNext(); }; }, ); ``` This is how you build registries by tag, by component kind, by scope or by business need, while reusing the same lifecycle as `craftRegisterFor`. ::: ## See Also * [craftRegisterFor](/guide/app/register) — the built-in registry on top of this * [Observability](/guide/advanced/observability) --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/setup.md --- # Routing setup Six steps turn Angular's routes into routes the compiler checks: a missing provider, a misspelled input or a route pointing at nothing becomes a build error instead of a blank screen. Architecture tests then keep those proofs from quietly disappearing. **Do this once per app**, then let [the CLI](/guide/routing/automation) write new routes for you. This guide assumes you are integrating type-safe DI/routes into an Angular app that consumes `@craft-ng/core`. ::: tip Prefer the guided version [Learn step 9](/learn/09-routing) walks through the same setup on a single route, with the reasoning attached. ::: ## Prerequisites Install the runtime package and the dev tooling in your app: ```bash npm install @craft-ng/core npm install -D @craft-ng/dev-tools ``` ## 1. Add a cascade DI check to every routes file DI is checked next to the routes it covers. Every file containing `craftRoutes(...)` must pair its collection with `ValidateCascadeRoutesFile` and `CanRun`; a parent check deliberately does not descend through `loadChildren`. ```ts import { craftRoutes, type CanRun, type ValidateCascadeRoutesFile, } from '@craft-ng/core'; import type { Router } from '@angular/router'; export const { appRoutes } = craftRoutes('app', [ /* routes */ ]); type _CheckAppDI = ValidateCascadeRoutesFile; type _CanRunApp = CanRun<_CheckAppDI>; ``` `ValidateCascadeRoutesFile` compares: * the generated dependencies declared on every route * the providers available from the app, parent mount, route and component If a route depends on a service that is not provided, or if a routed component expects an input that the route does not supply, `_CanRunApp` turns that mismatch into a TypeScript error in the routes file. Typical errors look like: * `The Counter service is not provided in path: "some-path"` * `Input "userId" is not provided in path: "some-path"` ## 2. Define routes with `craftRoute` and collect them with `craftRoutes` Do not export a plain Angular `Routes` array directly. Define each typed route with `craftRoute(...)`, collect them with `craftRoutes(...)`, and declare `componentDeps` on each route component. ::: warning Breaking rename The former `route(...)` helper has been renamed to `craftRoute(...)`. There is no compatibility alias: update both the import and every call site. ::: ```ts import { craftRoute, craftRoutes } from '@craft-ng/core'; export const { appRoutes } = craftRoutes('app', [ craftRoute('', { loadComponent: ({ withRetry }) => withRetry(import('./test')), componentDeps: {} as import('./test').GenDeps_TestComponent, }), ]); ``` The important part is: ```ts componentDeps: {} as import('./test').GenDeps_TestComponent, ``` That line connects the generated `GenDeps_*` type of the component to the same-file cascade check. ### Prefer the route CLI for day-to-day authoring The CLI is the primary writing façade while the generated result remains ordinary editable TypeScript: ```bash npx craft route add npx craft route add /users/:userId --component src/app/users/user-detail.ts#UserDetailComponent npx craft route add /users/:userId --create-component users/user-detail ``` By default it detects the Angular project and `craftRoutes` collections, creates one lazy routes file per feature, adds `componentDeps`, `withRetry`, `.withParent`, the parent mount assertion and the same-file DI check, then runs ESLint and TypeScript diagnostics. Use `--dry-run` to inspect the plan, `--yes` for non-interactive scripts and `--json` for machine-readable output. Static redirects stay in the selected collection: ```bash npx craft route add /old-users --redirect-to /users --parent src/app/app.routes.ts#appRoutes ``` Existing flat groups can be split explicitly: ```bash npx craft route split \ --parent src/app/app.routes.ts#appRoutes \ --prefix users \ --target src/app/users/users.routes.ts ``` The split command only moves statically analyzable routes. It reports local declarations or dynamic paths without mutating files, so business logic is never guessed. Then wire the crafted routes into your application config: ```ts import { craftAppConfig } from '@craft-ng/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { appRoutes } from './app.routes'; export const appConfig = craftAppConfig({ routingDeps: appRoutes.META_DATA, providers: [provideRouter(appRoutes.toRoutes(), withComponentInputBinding())], }); ``` Notes: * `appRoutes.toRoutes()` gives Angular the real runtime routes. * `appRoutes.META_DATA` gives `craftAppConfig(...)` the compile-time route dependency graph. * For **non-blocking navigation** (immediate URL commit, pending UI, centralised exception handling), render `CraftRouterOutlet()` from `@craft-ng/component` inside a Craft component tree instead of ``, and use `provideCraftRouter(...)` instead of `provideRouter(...)` — it accepts Angular router features **and** craft loading features (`withErrorComponent`, `withRouteLoadError`, `withTransitionTimings`, …) in one call, e.g. `provideCraftRouter(appRoutes.toRoutes(), withComponentInputBinding(), withErrorComponent({ component: MyGlobalErrorScreen, componentDeps }))`. (The features also work standalone via `provideCraftLoading(...)`.) `withRouteLoadError(...)` must stay in `provideCraftRouter(...)` because it also registers an Angular navigation error handler and an internal recovery route. See [Non-blocking navigation & pending UI](/guide/routing/pending-ui) and [Route Load Errors](/guide/routing/route-load-errors). * For lazy routes, `loadChildren` should return the named route tree exported by the child collection, for example `childRoutes.childRoutes`. ### When a routes file gets big The cascade check has a per-file budget, and past it TypeScript reports `TS2589` and silently degrades inference in the whole file. The fix is to split into lazy child collections, each with its own check — see **[Scaling routes](/guide/routing/scaling)**. ## 3. Run the Angular brand codemod through the published script Add a script in your app: ```json { "scripts": { "craft:brand": "craft-brand --root src/app" } } ``` Then run: ```bash npm run craft:brand ``` This is the step that creates the initial `GenDeps_*` aliases in your component files, for example: ```ts export type GenDeps_TestComponent = GetDeps<{ deps: { CommonModule: CommonModule; Counter: GetServiceDependencies; }; provided: {}; publicProperties: GetPublicComponentProperties; }>; ``` Adjust `--root` to your real source root: * `src/app` for a standard Angular app * `projects/my-app/src/app` for a workspace app * `libs/my-feature/src` for a library If you use a project-level `craft-brand.config.ts`, you can extend the script: ```json { "scripts": { "craft:brand": "craft-brand --root src/app --config ./craft-brand.config.ts" } } ``` ## 4. Install the ESLint rules Several checks in this guide rely on code a rule generates or keeps in sync — `GenDeps_*` aliases, the same-file DI proof, the exhaustiveness assert. Others enforce the architecture itself. Installing the plugin and the rule list is its own page: **[ESLint rules](/guide/routing/eslint-rules)**. ## 5. When a component changes, regenerate `GenDeps` with the Quick Fix After changing a component's DI-related shape, refresh its generated alias. Typical triggers: * adding or removing `inject(...)` * changing constructor injection * changing component `imports` * changing `providers` * changing `viewProviders` Recommended workflow: * first generation or bulk refactor: `npm run craft:brand` * one file without `GenDeps_*`: trigger the VS Code ESLint Quick Fix on `craft-ng/brand-angular-gen-deps-required` * one file with `GenDeps_*`: trigger the VS Code ESLint Quick Fix on `craft-ng/brand-angular-deps-match` * CLI alternative for one file: `eslint --fix src/app/feature/my-component.ts` Important limits: * the Quick Fix only handles the current file * if you rename the component class, rerun the generator so the `GenDeps_*` alias name stays aligned :::warning An Eslint error does not trigger a compilation error, so make sure to run the Quick Fix or `eslint --fix` after changing a component's DI shape. Otherwise, `main.ts` will not see the updated `GenDeps_*` and may miss real DI errors. ::: ## 6. Make the DI contract enforceable The proofs in this guide are unused type aliases unless they stay in the file: comment out a `CanRun` and the project still compiles. That is the one fragile step in an otherwise compile-time guarantee. Architecture tests close it. `assertRouteDiProofs` walks the static graph and fails unless every routed component — including lazy `loadChildren` collections — every pending or error screen, and every `craftAppConfig` error surface is hooked to an armed mapper. TypeScript still judges whether a dependency is provided; the architecture suite judges whether that judgement was invoked. Copy the demo layout (`apps/demo/architecture/`) and add: ```typescript it('requires a DI proof on every routed component and app-config error screen', () => { assertRouteDiProofs(graph.graph); }); ``` Full setup — analysis tsconfig, catalog, Nx target — is on [Architecture rules](/guide/testing/architecture). ## See Also * [CLI automation](/guide/routing/automation) — let the CLI write routes for you * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` keeps the proofs armed * [Route guards](/guide/routing/guards) — the next thing you'll add * [Scaling routes](/guide/routing/scaling) — when one routes file gets too big --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/automation.md --- # CLI automation Writing a typed route by hand means four pieces that must agree: the route, its `componentDeps`, the `withRetry` wrapper and the DI check. The CLI writes all four, and the output stays ordinary editable TypeScript. **Use it for** day-to-day route authoring and for migrating an existing app. **Then edit the result** — nothing here is generated code you must not touch. `@craft-ng/dev-tools` provides codemods to migrate an Angular application to Craft primitives, services, type-safe routes, and selectorless Craft Components. ## Install the migration tool ```shell npm install @craft-ng/core npm install --save-dev @craft-ng/dev-tools@beta ``` The migration binaries are available starting with `0.5.1-beta.0` and are currently published on the `beta` tag. The `latest` version and older beta versions do not include `craft-migrate`. If the package was installed before that release, update it and verify the resolved version: ```shell npm install --save-dev @craft-ng/dev-tools@beta npm ls @craft-ng/dev-tools ``` Commit or stash the current application changes before running a migration in write mode. ## Run the complete migration Preview all migrations first: ```shell npx craft-migrate \ --project tsconfig.app.json \ --root src \ --dry-run ``` Then apply them: ```shell npx craft-migrate \ --project tsconfig.app.json \ --root src \ --write ``` `craft-migrate` runs the migrations in the required order: 1. `craft-migrate-primitives` 2. `craft-migrate-services` 3. `craft-migrate-routes` 4. `craft-migrate-components` 5. `craft-migrate-architecture` The `--write` command also runs ESLint fixes on the touched files. Use `--no-eslint` only when your project runs this step separately. ## Run a targeted migration Use an individual codemod when the earlier stages have already been migrated: ```shell npx craft-migrate-routes \ --project tsconfig.app.json \ --root src \ --dry-run npx craft-migrate-routes \ --project tsconfig.app.json \ --root src \ --write npx craft-migrate-components \ --project tsconfig.app.json \ --root src \ --write npx craft-migrate-architecture \ --project tsconfig.app.json \ --root src \ --write ``` The route migration converts supported Angular route collections to `craftRoutes(...)`, adds type-safe route metadata, and reports transformations that require a manual decision. For a nested route collection, provide its mount context when it cannot be inferred safely: ```shell npx craft-migrate-routes src/app/admin/admin.routes.ts \ --project tsconfig.app.json \ --parent-mount admin \ --parent-names CurrentUser,Permissions \ --write ``` ## Review diagnostics Write the complete report to a JSON file: ```shell npx craft-migrate \ --project tsconfig.app.json \ --root src \ --dry-run \ --json migration-report.json ``` Resolve every manual diagnostic before considering the migration complete. In particular, verify generated `componentDeps`, inherited route providers, lazy child collections, and the file-level DI checks. ## Add a CI check After applying and reviewing the migration, prevent supported legacy patterns and unresolved manual diagnostics from returning: ```shell npx craft-migrate \ --project tsconfig.app.json \ --root src \ --check \ --fail-on-manual ``` Finish with the application's normal lint, type-check, test, and build commands. See the [complete migration guide](/resources/migration) for the post-codemod checklist. ## Make the DI contract enforceable The CLI writes the route, `componentDeps`, `withRetry` and the DI proof. Those proofs are unused type aliases: omit one and the project still compiles. That is the one fragile step in an otherwise compile-time guarantee. [Architecture tests](/guide/testing/architecture#assertroutediproofs) close it. `assertRouteDiProofs` walks the static graph and fails unless every routed component — including lazy `loadChildren` collections — every pending or error screen, and every `craftAppConfig` error surface is hooked to an armed mapper. TypeScript still judges whether a dependency is provided; the architecture suite judges whether that judgement was invoked. Add `assertRouteDiProofs` to the app's architecture suite and run it in CI. That is the application-facing check for the routing contract. ## Compiler fixture suite (optional) `craft route verify` is a separate, heavier check: it type-checks the project, then writes temporary valid and invalid fixtures covering route DI, `toProvide` providers, lazy child checks, route params and inputs, Angular and Craft templates, pending/error components, lazy loading, guard/resolve/component exceptions, local recovery and exhaustive handlers. Invalid fixtures are expected to fail, and their diagnostics are matched with the expected `path`, `pending component` or `exception component` context. Use it when you need to regression-test the type machinery itself — not as the app's proof that *your* routes still carry `CanRun`. Architecture tests cover that. ```json { "scripts": { "craft:verify-routes": "craft route verify --project tsconfig.app.json" } } ``` ```shell npm run craft:verify-routes ``` Fixtures are removed in a `finally` block. Use `--json` for a machine-readable report, `--root` when the application source root is not detected automatically, and `--keep-fixtures` only while diagnosing a failed verification. `--project` and `--tsconfig` are aliases for selecting the app tsconfig. This validates compile-time and ESLint bookkeeping guarantees. Runtime chunk-loading scenarios remain covered by the browser tests. ## See Also * [Routing setup](/guide/routing/setup) — what the CLI generates for you * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` is the app-facing routing check * [Angular brand config](/guide/routing/angular-brand-config) * [Scaling routes](/guide/routing/scaling) — `craft route split` --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/eslint-rules.md --- # ESLint rules The rule set is not decoration: several checks in this documentation only work because a rule generated or maintained the code they read. Others enforce the architecture — no raw `inject`, no Angular `HttpClient` — and most of them **autofix**. **Install them once** when you set up routing and type-safe DI. **Then lean on the quick fixes** rather than writing the boilerplate by hand. ::: warning An ESLint error is not a compile error A missing autofix does not break the build. If you skip the quick fix after changing a component's DI shape, `main.ts` keeps reading a stale `GenDeps_*` and can miss a real DI error. Run `eslint --fix` in CI. ::: The plugin is exposed from `@craft-ng/dev-tools/eslint-rules`. Add it to your ESLint flat config: ```ts import craftRules from '@craft-ng/dev-tools/eslint-rules'; export default [ // keep your existing ESLint config entries { files: ['**/*.ts'], plugins: { 'craft-ng': craftRules, }, rules: { 'craft-ng/brand-angular-gen-deps-required': 'error', 'craft-ng/brand-angular-deps-match': 'error', 'craft-ng/component-test-gen-deps-match': 'error', 'craft-ng/no-angular-inject': 'error', 'craft-ng/prefer-craft-template-blocks': 'error', 'craft-ng/no-render-writes': 'error', 'craft-ng/require-reactive-template-bindings': 'error', 'craft-ng/no-craft-use-in-template': 'error', 'craft-ng/no-ephemeral-template-form-state': 'error', 'craft-ng/no-craft-computed-side-effects': 'error', 'craft-ng/require-craft-method-for-yieldable-callback': 'error', 'craft-ng/prefer-direct-yieldable-callback': 'error', 'craft-ng/require-yieldable-reactive-read': 'error', 'craft-ng/require-yieldable-template-method': 'error', 'craft-ng/require-yieldable-insertion-write': 'error', 'craft-ng/prefer-craft-reactivity': 'error', 'craft-ng/prefer-craft-service': 'error', 'craft-ng/prefer-craft-http-client': 'error', 'craft-ng/prefer-craft-http-transport': 'error', 'craft-ng/prefer-craft-input-output': 'error', 'craft-ng/require-primitive-derived-property': 'error', 'craft-ng/no-async-await': 'error', 'craft-ng/no-throw': 'error', 'craft-ng/no-imperative-craft-resource-trigger': 'error', 'craft-ng/require-craft-resource-trigger-yield': 'error', 'craft-ng/require-assert-exhaustive-route-exceptions': 'error', 'craft-ng/require-craft-exception-handler': 'error', 'craft-ng/require-exception-component-di-check': 'error', 'craft-ng/require-pending-component-di-check': 'error', 'craft-ng/require-child-route-mount-check': 'error', 'craft-ng/require-lazy-load-with-retry': 'error', 'craft-ng/require-cascade-route-di-check': 'error', 'craft-ng/global-exception-registry-match': 'error', }, }, ]; ``` What each rule does: * `craft-ng/brand-angular-gen-deps-required`: generates a missing `GenDeps_*` alias for Angular components, directives, and pipes through the ESLint Quick Fix * `craft-ng/brand-angular-deps-match`: keeps existing `GenDeps_*` aliases in sync through the same ESLint Quick Fix flow * `craft-ng/component-test-gen-deps-match`: checks `setupCraftComponentTestingByRegister(Component, {} as GenDeps_Component, ...)` pairs in tests * `craft-ng/no-angular-inject`: forbids raw Angular `inject()` usage so dependencies go through `craftService(...)` or `toCraftService(...)` * `craft-ng/prefer-craft-template-blocks`: keeps `craftComponent(...)` templates declarative by rejecting ternaries, logical expressions, and imperative control flow; use `ifBlock(...)`, `matchBlock.exhaustive(...)`, `each(...)`, or `defer(...)` * `craft-ng/no-render-writes`: rejects detectable `set()`, `update()`, and `mutate()` calls in component templates and render bindings while allowing DOM event and `onXxx` output callbacks * `craft-ng/require-reactive-template-bindings`: requires Angular Signals, named Craft values, and component inputs to be read inside granular binding callbacks instead of during VNode construction; static values remain valid * `craft-ng/no-craft-use-in-template`: forbids the synchronous `craftUse(...)` escape hatch in Craft templates; pass the reactive reader directly, such as `status: usersQuery.currentPageStatus` * `craft-ng/no-ephemeral-template-form-state`: forbids `let` / `const` / `var` in the fourth argument of `craftComponent(...)` and `craftDirective(...)` (inline or a same-file identifier). Declare that state in the logic factory with `state()` or `craftComputed()` instead * `craft-ng/no-craft-computed-side-effects`: forbids writes and asynchronous work inside `craftComputed`; only reactive reads and `settled(...)` are allowed. The graph-wide counterpart is [`assertCraftComputedPure`](/guide/testing/architecture#assertcraftcomputedpure). * `craft-ng/prefer-craft-reactivity`: rejects authored Angular signal/computed/effect/resource APIs, explicit `.subscribe()` calls, and RxJS `Subject`/`BehaviorSubject`/`ReplaySubject`; use `state`, `craftComputed`, `craftEffect`, `query`, and named `source$`/`on$` flows * `craft-ng/prefer-craft-service`: forbids authored Angular `@Injectable()` / `@Service()` services in favor of `craftService(...)` and `toCraftService(...)` * `craft-ng/prefer-craft-http-client`: forbids Angular `HttpClient` usage in favor of `CraftHttpClient` * `craft-ng/prefer-craft-http-transport`: forbids direct `fetch()` and `XMLHttpRequest`; use `query()` for reads or `mutation()` for writes with `CraftHttpClient` * `craft-ng/prefer-craft-input-output`: forbids Angular `input()`/`output()` and `@Input`/`@Output`; use `Input`/`Output` from `@craft-ng/component` in `craftComponent(...)` * `craft-ng/require-primitive-derived-property`: requires a `computed` or `craftComputed` that only depends on one primitive in the same component/service to be exposed by that primitive's insertion; simple cases are autofixed * `craft-ng/no-async-await`: forbids `async` functions, `await`, and `for await...of`; use generator-based Craft primitives, `craftSleep`, and `CraftHttpClient` instead * `craft-ng/no-throw`: forbids `throw` in Craft code and offers a Quick Fix that returns `craftException({ code: 'UNEXPECTED_ERROR' }, { error: ... })`; keep technical boundaries and tests outside this rule when their contracts require thrown errors * `craft-ng/no-imperative-craft-resource-trigger`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` in a `craftEffect` dependency graph, including through `craftGen(...)`. The graph-wide counterpart, including `state` / `source$` writes, is [`assertCraftEffectNoImperativeSync`](/guide/testing/architecture#assertcrafteffectnoimperativesync). * `craft-ng/require-craft-resource-trigger-yield`: requires those triggers to use `yield*` inside generator functions, while ordinary UI callbacks may keep imperative calls * `craft-ng/require-craft-method-for-yieldable-callback`: requires callbacks returned by a `craftComponent` factory to wrap yieldable Craft method calls in `craftMethod(...)` * `craft-ng/prefer-direct-yieldable-callback`: replaces a template generator that only returns `yield* callback()` with the callback reference itself * `craft-ng/require-yieldable-reactive-read`: requires Craft reactive readers to be delegated with `yield*` inside generator functions; a function that reads a Craft reader must itself be a generator (`craftUse` remains the synchronous boundary) * `craft-ng/require-yieldable-template-method`: requires yieldable Craft method calls in a `craftComponent` template to be delegated with `yield*`, or passed as a reference (`click: counter.increment`) * `craft-ng/require-yieldable-insertion-write`: requires `set(...)`, `patch(...)`, and `update(...)` to be delegated with `yield*` when they are used inside a generator method * `craft-ng/require-assert-exhaustive-route-exceptions`: adds the collection-level `assertExhaustiveRouteExceptions(...)` safety net * `craft-ng/require-craft-exception-handler`: enforces `craftExceptionHandler(function* (...) {})`; simple handlers are autofixed and ambiguous raw redirects are reported for manual migration * `craft-ng/require-exception-component-di-check`: generates O(1) `RouteExceptionComponentCheckedDI` checks for `renderComponent`, route-level `errorComponent`, `withErrorComponent`, `withRouteLoadError`, and route-local `provideRouteLoadErrorComponent` * `craft-ng/require-pending-component-di-check`: generates the independent `RouteCheckedDI` check for each `pendingComponent` * `craft-ng/require-child-route-mount-check`: adds the missing `assertChildRouteMounts(...)` call + import (Quick Fix) for any `craftRoutes(...)` collection that mounts lazy `loadChildren`, so a `.withParent`-pinned child mounted under the wrong path is a compile error * `craft-ng/require-lazy-load-with-retry`: wraps route `loadComponent` and `loadChildren` imports with the generated `withRetry(...)` loader helper while preserving a statically analyzable import specifier * `craft-ng/require-cascade-route-di-check`: rejects any `craftRoutes(...)` collection without a same-file `ValidateCascadeRoutesFile + CanRun` proof; its autofix adds the conservative `` context, which should be adjusted when the mount inherits providers * `craft-ng/global-exception-registry-match`: keeps `CraftGlobalExceptionRegistry` synchronized with handlers delegating to `globalError()` ### Accessibilité (`craft-ng/a11y`) Spread `craftRules.configs.a11y.rules` to enable the WCAG 2.2 AA preset as `error`. The rules walk **all** hyperscript in the file (`craftTemplate`, factories extraites, `h('tag')`), not only `craftComponent` argument 3. * `prefer-named-html-helpers`: forbids `h('img')` / `h('button')` when a named helper exists * `img-has-alt`, `iframe-has-title`, `button-has-type`, `anchor-has-href` * `control-has-accessible-name`, `label-has-associated-control`, `heading-has-content` * `no-noninteractive-element-interactions`, `no-positive-tabindex` * `valid-aria`, `role-has-required-aria`, `target-blank-noopener` * `prefer-relative-heading`, `require-route-heading-outline`, `require-outlet-heading-section`, `no-heading-level-skip` * `require-focus-visible`, `require-reduced-motion` (CSS of `craftComponent`) See [Accessibilité](/guide/components/accessibility). The two migration rules also expose a VS Code ESLint Quick Fix suggestion that inserts a temporary local disable comment with the intended migration note when you need to unblock a file before doing the full refactor. The template and reactivity rules are intentionally diagnostic-only: replacing a resource or subscription can change lifecycle and error semantics, so the rule points at the Craft primitive without applying a potentially unsafe rewrite. ### Why templates use blocks Craft template blocks preserve the branch structure in the type-level render contract. A ternary or `condition && node` produces only a computed value, so the type checker cannot assert which branch renders which content. Keep derived values and business decisions in the component's state/query layer, then make the template express visibility explicitly: ```ts ifBlock( isReady, () => p('Ready'), () => p('Loading…'), ); matchBlock.exhaustive(query.exceptions, 'code', { NOT_FOUND: () => p('Not found'), FORBIDDEN: () => p('Forbidden'), }); ``` This rule is for Craft's TypeScript templates. Angular HTML templates are not rewritten by it. ### Reactive values belong in binding callbacks `require-reactive-template-bindings` uses TypeScript type information to find reactive reads. Reading a signal while constructing a VNode would make it a dependency of the structural component render, so the rule rejects this form: ```ts // Incorrect: count is read by the component template. p(`Count: ${count()}`); button({ disabled: isDisabled() }, 'Save'); div({ class: { active: isActive() } }); ``` Keep each read inside the callback owned by its DOM binding. Pass a yieldable reader, or use a generator when the binding must format: ```ts p(count); p(function* () { return `Count: ${yield* count()}`; }); button({ disabled: isDisabled }, 'Save'); div({ class: isActiveClass }); ``` Literal and otherwise static values are still allowed, as are reads performed from DOM events and `onXxx` output callbacks. Because the rule is type-aware, the ESLint parser must use `projectService: true` or a TypeScript `project`. If your project is adopting this progressively, enable both `craft-ng/brand-angular-gen-deps-required` and `craft-ng/brand-angular-deps-match` so the same Quick Fix can generate missing aliases and refresh existing ones. `craft-ng/no-angular-inject` is an architecture-enforcement rule and may require a broader migration. ### Yield insertion writes from generator methods `require-yieldable-insertion-write` requires `set(...)`, `patch(...)`, and `update(...)` calls to be delegated with `yield*` when they are used inside a generator method: ```ts nextPage: function* () { const current = yield* state(); return yield* patch({ page: current.page + 1 }); }, ``` Insertion callbacks that are not generators may return a write directly; the insertion wrapper consumes that result for them. ## What generates what Three rules do more than complain — they write code you would otherwise maintain by hand: | Rule | Generates | | -------------------------------------------- | ---------------------------------------------------------- | | `brand-angular-gen-deps-required` | the missing `GenDeps_*` alias for an Angular component | | `brand-angular-deps-match` | keeps an existing `GenDeps_*` in sync | | `require-cascade-route-di-check` | the same-file DI proof for a `craftRoutes(...)` collection | | `require-assert-exhaustive-route-exceptions` | the collection-level exhaustiveness assert | | `require-child-route-mount-check` | the `assertChildRouteMounts(...)` call and its import | | `require-lazy-load-with-retry` | the `withRetry(...)` wrapper on lazy route imports | | `prefer-direct-yieldable-callback` | removes redundant template generators | ## Adopting them progressively On an existing codebase, enable them in waves rather than all at once: 1. **The generators first** — `brand-angular-gen-deps-required` and `brand-angular-deps-match`. They only add code. 2. **The route safety nets** — the `require-*` rules. Mostly autofixable. They generate the proofs; [architecture tests](/guide/testing/architecture#assertroutediproofs) (`assertRouteDiProofs`) fail CI if a proof is later removed or left unarmed. 3. **The architecture rules last** — `no-angular-inject`, `prefer-craft-service`, `prefer-craft-http-client`, `require-yieldable-reactive-read`, `require-yieldable-template-method`, `require-yieldable-insertion-write`. These ask for real refactors. The two migration rules also expose a VS Code quick fix that inserts a temporary local disable comment with the intended migration note, so you can unblock a file before doing the full refactor. ## See Also * [Routing setup](/guide/routing/setup) — where these rules are installed * [CLI automation](/guide/routing/automation) — the codemods they complement * [Angular brand config](/guide/routing/angular-brand-config) * [Architecture rules](/guide/testing/architecture) — graph-wide constraints ESLint cannot see --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/route-providers.md --- # Route providers A route can provide services built from **its own URL** — the `:userId` in the path, its `data`, its query params, the value its guard resolved — with full type-safe dependency tracking. **Use it when** a subtree's services depend on which route rendered them: a "current project" service, a tenant-scoped API client. **Not when** the dependency is global — provide it at the app level instead. Build route-level Angular providers from a route's **own auto-provisioned tokens** — path params, `data`, `queryParams`, and `canActivate` guarded data — with full, type-safe dependency tracking. ## The problem A `craftRoutes` route auto-provisions route-scoped services. For a route `query/:userId` in the `demo` collection, `craftRoutes` generates helpers such as `injectDemoUserIdParams` and the yieldable `DemoQueryUserIdGuardedData`. The params helper is useful **inside a component**. Guarded data is consumed from a generator with `yield* DemoQueryUserIdGuardedData()`. Route `data` is intentionally not exported as a collection-level `inject…Data` helper; inside `withProviders`, consume it through the local `Data` generator. This also lets you take the value resolved by `canActivate` and feed it into a provider that the routed component injects. ## The solution: `craftRoute(...).withProviders(...)` `craftRoute(path, definition)` authors a single route and returns a builder with a `.withProviders(...)` method. The callback receives **route-scoped service generators**, one per auto-provisioned token that exists on the route, and returns a normal Angular providers array. ```ts import { abstract, craftRoutes, craftService, query, craftRoute, } from '@craft-ng/core'; type User = { name: string }; // 1. An abstract contract — implemented per route. const { UserRequirement, provideUser } = craftService( { name: 'User', scope: 'abstract' }, abstract(), ); // 2. A guard that resolves the user. const { Auth } = craftService({ name: 'Auth', scope: 'global' }, function* () { const auth = yield* query('auth', { params: () => true, loader: async () => ({}) as User, }); return auth; }); export const { demoRoutes } = craftRoutes('demo', [ craftRoute('query/:userId', { componentDeps: {} as import('./query').GenDeps_GlobalQuery, loadComponent: ({ withRetry }) => withRetry(import('./query')), canActivate: function* () { const user = yield* Auth(); const userValue = user.value(); if (!userValue) { return false; } return safeUser; // becomes the route's guarded data }, }).withProviders(({ GuardedData }) => [ provideUser(function* () { const guarded = yield* GuardedData(); // Signal return guarded(); }), ]), ]); ``` The routed component can now yield `User()` from its Craft component factory and receive the value that the guard resolved — without ever touching the fully-qualified route helper. ## The helpers object The `.withProviders(...)` callback receives an object with **route-local short names** for every auto-provisioned token present on the route: | Helper | Present when… | Yields | | --------------- | --------------------------- | -------------------------------------- | | `GuardedData` | the route has `canActivate` | `Signal` | | `Params` | per path param | `Signal` (e.g. `UserIdParams`) | | `QueryParams` | the route has `queryParams` | the query-params state | | `Data` | the route has `data` | `Signal` | Names are **scoped to the single route**, so the collection prefix and route path are dropped: `GuardedData`, not `DemoQueryUserIdGuardedData`. The path-param name is kept to keep multiple params distinct (`UserIdParams`, `TeamIdParams`, …). Each helper is a generator you consume with `yield*`, exactly like a service's `X()`: ```ts .withProviders(({ UserIdParams, QueryParams }) => [ provideSomething(function* () { const userId = yield* UserIdParams(); // Signal const qp = yield* QueryParams(); // query-params state return { userId, qp }; }), ]) ``` ## Pairing with an abstract service `craftRoute(...).withProviders(...)` shines with `scope: 'abstract'` services. The abstract service declares a contract; each route provides a concrete implementation derived from that route's data. Abstract services now expose a `provideX(factory)` helper that takes a **generator factory**, tracks everything it yields, and binds the result to the requirement token. See [craftService → Abstract Providers](/guide/app/craft-service#abstract-providers). ```ts const { User, provideUser } = craftService( { name: 'User', scope: 'abstract' }, abstract(), ); // In a route: .withProviders(({ GuardedData }) => [ provideUser(function* () { return (yield* GuardedData())(); }), ]) // In the routed component factory: const user = yield* User(); // User ``` ## Dependency tracking & cascade DI Everything yielded inside a `withProviders` factory is tracked at the type level and folded into the route's dependency graph used by [`ValidateCascadeRoutesFile`](/guide/routing/setup): * The route's **auto-provisioned** tokens (guarded data, params, query params, data) are recognized as provided by the route itself — yielding them is always valid. * Any **other** service yielded inside the factory that is not provided by the route or the app surfaces as a missing-provider error, e.g.: ``` The SomeService service is not provided in path: "query/:userId" ``` * The provider's own name (`User` above) is registered as **self-provided**, so a component on that route can depend on it without a separate provider declaration. This means the pattern is safe by construction: you cannot wire a route provider against data the route does not actually expose. ## Plain providers still work `.withProviders(...)` is additive. A route can still declare a plain Angular `providers` array, and both are merged (auto-provisioned services first, then `providers`, then the `withProviders` factory output): ```ts craftRoute('admin', { componentDeps: {} as import('./admin').GenDeps_Admin, loadComponent: ({ withRetry }) => withRetry(import('./admin')), providers: [SomeAngularProvider], // plain array, untyped helpers }).withProviders(({ Data }) => [ /* factory-built providers with tracking */ ]); ``` Under the hood the builder stores the factory on a dedicated `providersFn` field, kept separate from Angular's `providers` array. ## See Also * [Setup](/guide/routing/setup) — the app-wide cascade DI check * [craftService](/guide/app/craft-service) — `abstract` scope, `provideX`, requirements --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/guards.md --- # Route guards A guard is a bare `function*` on `canActivate` / `canMatch`. It yields what it needs, and returns either a value or a `craftException` describing why the route must not render. **Use one when** access to a route depends on state: authentication, a role, a feature flag, an onboarding step. **Not when** the answer is a redirect with no condition — that is a static route. Guards here are **reusable and parameterised**, they **compose** inside a single `canActivate` / `canMatch`, and their failure cases are resolved **exhaustively** — an unhandled case is a **type error**. > **A guard is just a generator function.** `canActivate` / `canMatch` take a bare > `function* () { … }` directly — there is no `craftCanActivate` / `craftCanMatch` wrapper and no > inline `resolvers` argument. Every reachable `craftException` is resolved by a single, exhaustive > **[`handleExceptions`](/guide/concepts/exceptions)** map on the route, applied **after the URL commits** > by the non-blocking [`CraftRouterOutlet`](/guide/routing/pending-ui). ## The problem A `craftRoutes` `canActivate` accepts a single function (or generator function). To apply several authorization rules — role, account state, feature flag… — you have to inline everything into one generator and hand-roll each rejection by returning a `createUrlTree(...)`: ```ts canActivate: function* () { const { user } = yield* CraftAuth(undefined, ({ user }) => ({ user })); if (!user()) { return createUrlTree(['/auth/login']); // not authenticated } if (user()!.role !== 'admin') { return createUrlTree(['/unauthorized']); // wrong role } const { pizzeria } = yield* CraftAuth(undefined, ({ pizzeria }) => ({ pizzeria })); if (pizzeria()) { return createUrlTree(['/dashboard']); // already onboarded } return true; } ``` The rules are not reusable, the redirect logic is tangled with the checks, and nothing forces you to handle every rejection — forget a branch and it silently falls through. ## The solution: `craftGen` + a composing generator guard Split the two concerns: * **`craftGen`** authors a reusable, parameterised guard. It either returns a success value or a typed [`craftException`](#exceptions). * The route's **`canActivate` generator** composes guards with `yield*`; the route's exhaustive [`handleExceptions`](/guide/concepts/exceptions) map must cover **exactly** the reachable exception codes. For a focused overview of `craftGen` itself and why it is useful, see [`craftGen`](/guide/concepts/generators). ```ts import { craftException, craftGen, craftResolve, CraftHttpClient, query, craftRoute, craftUntilSettled, } from '@craft-ng/core'; // Reusable guards — each returns a success value | craftException(...) const roleGuard = craftGen( (...roles: Role[]) => function* () { const { user } = yield* CraftAuth(undefined, ({ user }) => ({ user, })); if (!user()) return craftException({ code: 'NOT_AUTHENTICATED' }); return roles.includes(user()!.role) ? true : craftException({ code: 'FORBIDDEN_ROLE' }); }, ); const noPizzeriaGuard = craftGen( () => function* () { const { pizzeria } = yield* CraftAuth(undefined, ({ pizzeria }) => ({ pizzeria, })); return pizzeria() ? craftException({ code: 'HAS_PIZZERIA' }) : true; }, ); const { pizzeriaDraftQuery } = query('pizzeriaDraftQuery', { params: () => true, loader: function* () { return yield* CraftHttpClient.get(({ response }) => ({ url: '/api/pizzerias/draft', success: response(), exceptions: [ function* ({ status }) { if (!(yield* status(404))) return; return craftException({ code: 'PIZZERIA_DRAFT_UNAVAILABLE' }); }, ], })); }, }); craftRoute( 'new', { title: 'Create Pizzeria', canActivate: function* () { yield* roleGuard(ROLES.PIZZERIA_ADMIN); // short-circuits on exception yield* noPizzeriaGuard(); return true; }, resolve: craftResolve(function* () { return yield* craftUntilSettled(pizzeriaDraftQuery); }), loadComponent: ({ withRetry }) => withRetry( import('./pages/admin-pizzeria-form-page/admin-pizzeria-form-page'), ).then((m) => m.AdminPizzeriaFormPage), componentDeps: {} as import('./pages/admin-pizzeria-form-page/admin-pizzeria-form-page').GenDeps_AdminPizzeriaFormPage, }, { // Resolved centrally — exhaustive over canActivate ∪ canMatch ∪ resolve. NOT_AUTHENTICATED: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'auth/login' }); }), FORBIDDEN_ROLE: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'unauthorized' }); }), HAS_PIZZERIA: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'pizzerias/admin' }); }), PIZZERIA_DRAFT_UNAVAILABLE: craftExceptionHandler(function* ({ globalError, }) { return globalError(); }), HttpError: craftExceptionHandler(function* ({ globalError }) { return globalError(); }), }, ); // After the collection is defined, assert every route handles exactly its codes: // assertExhaustiveRouteExceptions(adminRoutes); ``` ## Reactive guards While a route is active, its `canActivate` invariant stays **under observation** (live guards, on by default). If a signal the guard reads changes — e.g. the user logs out and `Auth` becomes `null` — the guard re-evaluates synchronously and applies [`handleExceptions`](/guide/concepts/exceptions) with `phase: 'active'`, so the target is never left rendered in an incoherent state. The reactive phase never re-runs `resolve` (no new pending). Opt out per route with `reactiveGuards: false`. ## How composition works `craftGen(factory)` returns a factory you invoke and delegate to with `yield*`: * The guard's **dependency yields** (`CraftAuth`, `CraftRouter`, …) flow up to the route exactly as in a plain generator guard, so [cascade DI tracking](/guide/routing/setup) still sees them. * As soon as a composed guard produces a `craftException`, the enclosing generator **short-circuits**: `yield* roleGuard(...)` interrupts the whole `function*`, and the exception is propagated to the route's guard boundary — no `if`/`return` plumbing in the composing guard. * The set of exceptions each guard can produce is tracked **at the type level**, so the route's `handleExceptions` map knows precisely which codes it must handle. Order matters: guards run top-to-bottom and the first exception wins (fail-fast). ## The handler context Each route exception handler receives the typed exception and payload, the navigation phase, the native Angular `Router` helpers, and the five outcome constructors. See [Centralised Exception Handling](/guide/concepts/exceptions#handler-context) for the exhaustive list and examples. Use `redirectTo(...)` for typed internal routes: ```ts { RATE_LIMITED: craftExceptionHandler(function* ({ payload, redirectTo }) { return yield* redirectTo({ to: 'cooldown', queryParams: { retryAfter: String(payload.retryAfter) }, }); }), } ``` A handler returns a `CraftExceptionOutcome` via `redirectTo`, `redirectUrl`, `renderComponent`, `globalError`, `stay`, or `noop`. The `payload` is taken from `craftException({ code }, payload)`'s second argument and typed per code. ## Handlers can yield services A handler may be a **generator** that `yield*`s craft services before building the redirect — for example to read the login URL from a config service. Those yields are tracked exactly like the guards' own dependencies, so a service used only at redirect-time still flows into the route's [cascade DI](/guide/routing/setup) (yield an unprovided service and it surfaces as a missing-provider error on the route): ```ts craftRoute( 'admin', { canActivate: function* () { yield* roleGuard(ROLES.ADMIN); return true; }, }, { // Generator handler — `RedirectConfig` becomes a tracked route dependency. FORBIDDEN_ROLE: craftExceptionHandler(function* ({ redirectUrl }) { const { unauthorizedUrl } = yield* RedirectConfig(); return redirectUrl(unauthorizedUrl); }), }, ); ``` Every handler uses the generator wrapper, including handlers that do not yield a service. ## Exhaustiveness The handler map is typed over the reachable codes, so **every** reachable code must be handled — a missing one is a type error: ```ts craftRoute( 'admin', { canActivate: guard }, { FORBIDDEN_ROLE: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'unauthorized' }); }), // Type error: Property 'HAS_PIZZERIA' is missing. }, ); ``` Add a guard that can raise a new code, and every route using it stops compiling until its handler is added. A typo'd code is caught the same way because the correctly-spelled key is then missing. ## Guarded data still flows through A `canActivate` guard's **success value** (anything other than `true`/`UrlTree`/…) becomes the route's [guarded data](/guide/routing/route-providers) — `craftException` returns are never treated as data: ```ts const authGuard = craftGen( () => function* () { const user = yield* Auth(); const userValue = user.value(); return userValue ? userValue : craftException({ code: 'NOT_AUTHENTICATED' }); }, ); craftRoute( 'query/:userId', { componentDeps: {} as import('./query').GenDeps_GlobalQuery, loadComponent: ({ withRetry }) => withRetry(import('./query')), canActivate: function* () { return yield* authGuard(); // success value = the user }, }, { NOT_AUTHENTICATED: craftExceptionHandler(function* ({ redirectUrl }) { return redirectUrl('/login-form'); }), }, ).withProviders(({ GuardedData }) => [ provideUser(function* () { return (yield* GuardedData())(); // Signal → User }), ]); ``` ## `canMatch` `canMatch` is the sibling of `canActivate` — same composition and exhaustive resolution through `handleExceptions`. Unlike `canActivate`, a `canMatch` guard produces no guarded data. ```ts const featureFlagGuard = craftGen( (flag: string) => function* () { const { flags } = yield* CraftConfig(); return flags[flag] ? true : craftException({ code: 'FLAG_DISABLED' }); }, ); craftRoute( 'beta', { componentDeps: {} as import('./beta').GenDeps_Beta, loadComponent: ({ withRetry }) => withRetry(import('./beta')), canMatch: function* () { yield* featureFlagGuard('beta'); return true; }, }, { FLAG_DISABLED: craftExceptionHandler(function* ({ redirectUrl }) { return redirectUrl('/home'); }), }, ); ``` ## Async guards {#async-guards} The guards above are **synchronous** — every `craftGen` resolves in one pass. To decide based on data that has to be *fetched first*, suspend the composing guard with `craftUntilSettled` (or `craftUntilDefined`). The guard stays a normal generator: `yield* a(); const x = yield* craftUntilSettled(...); yield* b()` composes across the await, and the awaited operation's `craftException`s flow into the same exhaustive `handleExceptions` map — the compiler still forces you to handle every reachable code. ### `craftUntilSettled` — await a resource or an HTTP call `craftUntilSettled` takes either a craft **resource** (`query` / `mutation` / `asyncProcess`) or a `CraftHttpClient.*` **call** and suspends until it settles, then returns its success value. ```ts craftRoute( 'users/:userId', { componentDeps: {} as import('./user').GenDeps_User, loadComponent: ({ withRetry }) => withRetry(import('./user')), canActivate: function* (route) { const userId = route.params['userId']; // (a) Await an HTTP call directly — no named resource needed. Its declared // `exceptions` flow into the route's handleExceptions below. const user = yield* craftUntilSettled( CraftHttpClient.get(({ response }) => ({ url: `/api/users/${userId}`, success: response(), exceptions: [ function* ({ status, code }) { if (!(yield* status(400))) return; if (!(yield* code('PASSWORD_REQUIRED'))) return; return craftException({ code: 'PASSWORD_REQUIRED', scope: 'UsersFeature', }); }, ], })), ); return user.active ? true : craftException({ code: 'INACTIVE_USER' }); }, }, { // Both the guard's own exception AND the HTTP call's exception are required. INACTIVE_USER: craftExceptionHandler(function* ({ redirectUrl }) { return redirectUrl('/inactive'); }), PASSWORD_REQUIRED: craftExceptionHandler(function* ({ redirectUrl }) { return redirectUrl('/password'); }), }, ); ``` The **resource** form is identical — pass the ref (an inline `query(name, ...)` works, though it is reactive; prefer the HTTP form for one-shots): ```ts const user = yield * craftUntilSettled( query('user', { params: () => userId, loader: ({ params }) => fetchUser(params), }).user, ); ``` **Settle semantics & exception routing:** * A resource settles when its `status` reaches `'resolved'` or `'error'`. A loader `craftException` **short-circuits** to `handleExceptions`; a thrown loader error is **rethrown**; otherwise the resolved value is returned. * An HTTP call's declared business `exceptions` short-circuit to `handleExceptions`. The generic transport-level `HttpError` (`scope: 'HttpClient'`) is **rethrown** — a network failure is not a resolvable business case. (An opt-in `HttpError` handler may come later.) * The awaited HTTP endpoint is tracked as a route dependency automatically, exactly like one used in a component or loader. ### `craftUntilDefined` — await a readiness signal `craftUntilDefined(signal)` suspends until `signal()` is no longer `undefined`, then returns its non-nullable value. There is no exception channel — use it to wait on a plain readiness signal. ```ts const session = yield * craftUntilDefined(sessionService.current); ``` ### Notes * A guard that never reaches an `craftUntilSettled` / `craftUntilDefined` await still resolves **synchronously** (no forced microtask) — existing synchronous guards are unchanged. * This works for both `canActivate` and `canMatch`; the outlet drives the guard to settlement after the URL commits. ## Exceptions {#exceptions} Guards fail with `craftException({ code }, payload?)` — the same typed-exception primitive used by `query` / `mutation`: ```ts craftException({ code: 'FORBIDDEN_ROLE' }); craftException({ code: 'RATE_LIMITED' }, { retryAfter: 30 }); // payload reaches the handler ``` The `code` drives both the exhaustiveness check and the handler lookup; the optional payload is typed and forwarded to the handler. ## When to reach for it `craftGen` + a `canActivate` / `canMatch` generator fit **sequential, fail-fast gates resolved at a single boundary**: authorization, account-state checks, feature flags, action preconditions. It is **not** the right tool when you want to **collect and surface multiple failures** reactively — that is what `query` / `mutation` `hasException` and the form-submit exception model are for. Guards stop at the first failure and hand off to a handler. ## See Also * [Route Providers](/guide/routing/route-providers) — consume guarded data in route providers * [Setup](/guide/routing/setup) — the app-wide cascade DI check * [craftService](/guide/app/craft-service) — services yielded inside guards --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/exception-handling.md --- # Route exception handling When a guard, a matcher or a resolver raises a declared exception, this page is where you say what happens next: redirect, render a dedicated component, stay put, or carry on. One map per route resolves the **union** of every code those three steps can produce — and the compiler checks that the map is exactly complete, no more and no less. **Use it when** a route's guards, matchers or resolvers can fail in ways the user should see. **Not when** the failure is local to one primitive — read it off `exceptions()` instead, see [Exceptions as values](/guide/concepts/exceptions). ::: warning Breaking change Every handler must use `craftExceptionHandler(function* (...) {})`. Internal redirects use `yield* redirectTo({ to, params, queryParams, viewTransition })`; opaque URLs or prebuilt `UrlTree` values use `redirectUrl(...)`. `renderComponent`, route-level `errorComponent` and `withErrorComponent` accept only `{ component | loadComponent, componentDeps }` descriptors. Bare handler functions, `redirect(...)` and bare error components are rejected. ::: ## The common case ```ts USER_DISABLED: craftExceptionHandler(function* ({ renderComponent }) { return renderComponent({ loadComponent: () => import('./user-disabled-error-page').then( (m) => m.UserDisabledErrorPage, ), componentDeps: {} as import('./user-disabled-error-page').GenDeps_UserDisabledErrorPage, }); }), NOT_AUTHENTICATED: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'auth/login', queryParams: { reason: 'session-expired' }, }); }), ``` `canActivate` / `canMatch` / `resolve` stay your **writing** API — each may raise a typed [`craftException`](/guide/routing/guards#exceptions). Instead of an inline resolver map per guard, a single **`handleExceptions`** map on the route resolves the union of every code reachable from those three steps. The non-blocking [`CraftRouterOutlet`](/guide/routing/pending-ui) applies the chosen outcome **after the URL has committed**, so a slow guard never freezes navigation. The route result also exposes a route-scoped signal helper per code, such as `injectDemoUserIdUserDisabledException()`. It returns the exact exception and payload for the locally rendered branch, and is cleared on the next navigation. ## A full route, end to end ```ts const { profileQuery } = query('profileQuery', { params: () => true, loader: function* () { return yield* CraftHttpClient.get(({ response }) => ({ url: '/api/profile', success: response(), exceptions: [ function* ({ status, code }) { if (!(yield* status(403))) return; if (!(yield* code('USER_DISABLED'))) return; return craftException({ code: 'USER_DISABLED' }); }, ], })); }, }); craftRoute( 'user/:userId', { loadComponent: ({ withRetry }) => withRetry(import('./user-detail')), componentDeps: {} as import('./user-detail').GenDeps_UserDetail, canMatch: function* () { const ff = yield* FeatureFlags(); return ff.userPageEnabled ? true : craftException({ code: 'FEATURE_OFF' }); }, canActivate: function* () { const user = yield* Auth(); return user.value() ?? craftException({ code: 'NOT_AUTHENTICATED' }); }, resolve: craftResolve(function* () { return yield* craftUntilSettled(profileQuery); }), }, { FEATURE_OFF: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'home' }); }), NOT_AUTHENTICATED: craftExceptionHandler(function* ({ redirectTo, phase }) { return yield* redirectTo({ to: 'login', queryParams: phase === 'active' ? { reason: 'session-expired' } : {}, }); }), USER_DISABLED: craftExceptionHandler(function* ({ globalError }) { return globalError(); }), HttpError: craftExceptionHandler(function* ({ globalError }) { return globalError(); }), }, ), ``` `canActivate` / `canMatch` are bare generator functions — there is no guard wrapper and no inline `resolvers` argument. Every reachable code flows to the third `craftRoute(...)` argument. ## Handler context Every handler receives a `CraftExceptionHandlerContext` typed for its exception code: | Field | Type / purpose | | ----------------- | -------------------------------------------------------------------------------------------- | | `exception` | The complete typed `craftException`, including `code`, `scope`, and `payload`. | | `payload` | The typed payload passed as the second argument of `craftException(...)`. | | `phase` | `'enter'` during initial activation, `'active'` during a live guard re-check. | | `router` | The native Angular `Router` instance. | | `createUrlTree` | Bound `Router.createUrlTree`, useful for building a redirect with query params or fragments. | | `navigate` | Bound `Router.navigate`. Imperative; prefer returning `yield* redirectTo(...)`. | | `navigateByUrl` | Bound `Router.navigateByUrl`. Imperative; prefer a redirect outcome. | | `redirectTo` | Typed internal redirect checked against `META_PATHS`; yields `CraftRouter`. | | `redirectUrl` | Explicit escape hatch for an opaque string URL or `UrlTree`. | | `renderComponent` | Builds an outcome that renders a dedicated component. | | `globalError` | Delegates rendering to the application-wide error component. | | `stay` | Restores the previous URL and keeps the triggering page. | | `noop` | Continues to the target despite the exception. Resolve data remains `undefined`. | A handler is always a synchronous generator wrapped with `craftExceptionHandler`. It may resolve services but cannot suspend with `craftUntilSettled` / `craftUntilDefined`. ## Outcomes Each handler receives a context and returns an outcome constructor: | Outcome | Effect | | ----------------------------- | -------------------------------------------------------------------------------------------------------- | | `yield* redirectTo(input)` | Navigate to a registered internal route with typed params/query params/view transition. | | `redirectUrl(target)` | Navigate to an opaque string URL or `UrlTree`. | | `renderComponent(descriptor)` | Render a DI-checked `{ component \| loadComponent, componentDeps }` descriptor. | | `globalError()` | Render the application-wide error component (see [global error component](./global-error-component.md)). | | `stay()` | Cancel the navigation; restore the previous URL (stay on the triggering page). | | `noop()` | Render the target anyway, with `resolve` data left `undefined`. | The context also carries the typed `exception`, its `payload`, the Angular-native `redirect` helpers (`createUrlTree` / `navigate` / `navigateByUrl`), and the navigation `phase` (see below). A handler may be a **generator** that `yield*`s craft services before its outcome. ## Examples ### Typed payload and `UrlTree` Use `redirectTo(...)` for registered application routes and `redirectUrl(...)` for a prebuilt `UrlTree`: ```ts { NOT_AUTHENTICATED: craftExceptionHandler(function* ({ redirectTo }) { return yield* redirectTo({ to: 'auth/login' }); }), RATE_LIMITED: craftExceptionHandler(function* ({ payload, redirectTo }) { return yield* redirectTo({ to: 'cooldown', queryParams: { retryAfter: String(payload.retryAfter) }, }); }), } ``` Here `payload` is inferred from `craftException({ code: 'RATE_LIMITED' }, { retryAfter: 30 })`. ### Initial entry versus live guard ```ts { NOT_AUTHENTICATED: craftExceptionHandler(function* ({ phase, redirectTo }) { return yield* redirectTo({ to: 'login', queryParams: phase === 'active' ? { reason: 'session-expired' } : {}, }); }), } ``` ### Local, global, stay, and noop outcomes ```ts { ACCOUNT_LOCKED: craftExceptionHandler(function* ({ renderComponent }) { return renderComponent({ component: AccountLockedPage, componentDeps: {} as import('./account-locked-page').GenDeps_AccountLockedPage, }); }), MAINTENANCE: craftExceptionHandler(function* ({ renderComponent }) { return renderComponent({ loadComponent: () => import('./maintenance-page').then((m) => m.MaintenancePage), componentDeps: {} as import('./maintenance-page').GenDeps_MaintenancePage, }); }), HttpError: craftExceptionHandler(function* ({ globalError }) { return globalError(); }), UNSAVED_CHANGES: craftExceptionHandler(function* ({ stay }) { return stay(); }), OPTIONAL_PROFILE_UNAVAILABLE: craftExceptionHandler(function* ({ noop }) { return noop(); }), } ``` The descriptor is checked independently with the O(1) `RouteExceptionComponentCheckedDI`; it is not added to `ValidateCascadeRoutesFile`. [Architecture tests](/guide/testing/architecture#assertroutediproofs) fail if that proof is missing or not armed with `CanRun`. ### Handler using a craft service ```ts { FORBIDDEN_ROLE: craftExceptionHandler(function* ({ redirectUrl }) { const config = yield* RedirectConfig(); return redirectUrl(config.unauthorizedUrl); }), } ``` Dependencies yielded by handlers participate in route DI checking, like dependencies yielded by guards and resolvers. ## Exhaustiveness The union is only resolvable once the whole collection is inferred, so exhaustiveness is asserted **after** `craftRoutes` (mirroring the cascade DI check) rather than inline on each route: ```ts export const { demoRoutes } = craftRoutes('demo', [ /* … */ ]); // Compile error if any route's handleExceptions misses — or over-covers — a reachable code. assertExhaustiveRouteExceptions(demoRoutes); ``` [Architecture tests](/guide/testing/architecture#assertroutediproofs) fail if a `craftRoutes(...)` collection has no `assertExhaustiveRouteExceptions`. A missing code (e.g. `resolve` can throw `USER_DISABLED` but no handler) **and** an extra code (a handler for a code nothing can produce) are both type errors, naming the offending route + codes. ## Pitfalls **`HttpError` appears or disappears depending on the `craftUntilSettled` form.** This is the most common surprise: * `craftUntilSettled(CraftHttpClient.get(...))` **excludes** `HttpError` from the routable union and rethrows it. The outlet sends that navigation error to the global error component. * `craftUntilSettled(queryRef)` routes every exception the query exposes. When its loader returns a `CraftHttpClient` request, that **includes** `HttpError`, so the route must declare an explicit handler such as `HttpError: craftExceptionHandler(function* ({ globalError }) { return globalError(); })`. Declared business exceptions remain routable in both forms. **A handler cannot suspend.** It may `yield*` services, but not `craftUntilSettled` / `craftUntilDefined`. **Over-covering is an error too.** A handler for a code nothing can produce fails the exhaustiveness assert, same as a missing one. ::: details The `phase` field `phase` distinguishes the initial activation (`'enter'`) from a reactive re-evaluation (`'active'`) of a live `canActivate` guard (see [live guards](/guide/routing/guards#reactive-guards)). Use it to soften a reaction mid-session — a different redirect reason on session expiry, say — or ignore the reactive phase entirely with `noop()`. ::: ## See Also * [Exceptions as values](/guide/concepts/exceptions) — the concept * [Route guards](/guide/routing/guards) — where exceptions are raised * [Global error component](/guide/routing/global-error-component) * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` keeps the exhaustiveness assert in place --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/pending-ui.md --- # Non-blocking navigation By default, a slow guard or resolver freezes the app on the previous page with no feedback. `CraftRouterOutlet()` inverts that: the URL commits immediately and a pending component appears only if the wait is actually noticeable. **Use it when** guards or resolvers do real work — an HTTP call, a permission check. **Not when** everything resolves synchronously; `` is fine then. `CraftRouterOutlet()` replaces `` with **non-blocking** navigation : the URL commits immediately, a pending component appears only if the guard/resolve chain is slow, and the target component is mounted **only on success** — never while an exception is being resolved. ## Setup Call the outlet inside a Craft component tree: ```ts import { CraftRouterOutlet, craftComponent, main } from '@craft-ng/component'; export const App = craftComponent( 'App', {}, () => ({}), () => main({ class: 'content' }, CraftRouterOutlet()), ); ``` Routes with no craft guard/resolve render immediately, exactly like ``. ## Lifecycle For a route with a craft chain, on navigation the outlet lets the URL commit immediately (no blocking guard), then runs **three phases** while the chain is in flight — so a fast navigation never flashes a blank screen or a loader: 1. **stay** — for `stayMs` (default `300`) the **previous page is kept on screen**. The chain runs in the background; if it settles within this window, the outlet transitions **straight to the target** (no blank, no loader); 2. **blank** — for the next `blankMs` (default `300`), a **blank** surface, signalling the page is changing; 3. **pending** — the **pending component** (loader) is shown until the chain settles. On success the outlet writes the resolved data and mounts the **target**; on exception it applies the route's [`handleExceptions`](/guide/concepts/exceptions) outcome. Lazy JavaScript load failures (`loadComponent` / `loadChildren`) happen before the outlet can mount the target route. Configure [`withRouteLoadError`](/guide/routing/route-load-errors) to retry those failures and render a recovery screen while keeping the browser URL on the intended route. A slow JavaScript download or retry does not currently activate this pending timeline; dedicated loading UI for that earlier phase is a planned evolution. ``` clic → URL committée ├─ 0 → stayMs ........ page PRÉCÉDENTE conservée ─(résolu)─▶ cible ├─ stayMs → +blankMs . page BLANCHE ─(résolu)─▶ cible └─ au-delà ........... LOADER (min pendingMinMs) ─(résolu / redirect)─▶ cible / redirect ``` `pendingMinMs` adds anti-flicker: once the loader is shown, it stays visible for at least that long, so a chain that settles right after it appears does not blink it in and out. The previous page is kept **alive** (not re-created) during `stay`: the outlet renders through a single component slot it leaves untouched until the phase changes, so the old component instance keeps its state for the duration of the window. ## Configuration The loading/error features are plain feature objects (like Angular's `withComponentInputBinding()`). The recommended place for them is **directly in `provideCraftRouter(...)`**, mixed with Angular's own router features — they are split apart internally and routed to `provideRouter` / `provideCraftLoading`: ```ts provideCraftRouter( appRoutes.toRoutes(), withComponentInputBinding(), // Angular router feature withCraftViewTransitions(), // craft loading feature (see below) withErrorComponent({ component: MyGlobalErrorScreen, componentDeps: {} as import('./global-error').GenDeps_MyGlobalErrorScreen, }), withRouteLoadError({ component: MyRouteLoadErrorScreen, componentDeps: {} as import('./route-load-error').GenDeps_MyRouteLoadErrorScreen, retry: { attempts: 1, delayMs: 250 }, }), withTransitionTimings({ stayMs: 300, blankMs: 300, pendingMinMs: 500 }), withLoadingText(() => computed(() => translate('common.loading'))), withPendingComponent(MyBrandedSpinner), ), ``` Most loading features still work standalone via `provideCraftLoading(...)` if you prefer to keep them in a separate provider. Keep `withRouteLoadError(...)` in `provideCraftRouter(...)`: it also registers an Angular navigation error handler and an internal recovery route. ```ts provideCraftLoading( withTransitionTimings({ stayMs: 300, blankMs: 300, pendingMinMs: 500 }), withLoadingText(() => computed(() => translate('common.loading'))), withPendingComponent(MyBrandedSpinner), withErrorComponent({ component: MyGlobalErrorScreen, componentDeps: {} as import('./global-error').GenDeps_MyGlobalErrorScreen, }), ), ``` | Feature | Token | Default | | -------------------------- | --------------------------------------------------------------------- | --------------------------------- | | `withPendingComponent` | `CRAFT_PENDING_COMPONENT` | `DefaultCraftPendingComponent` | | `withLoadingText` | `CRAFT_LOADING_TEXT` | locale-aware (en/fr, fallback en) | | `withTransitionTimings` | `CRAFT_STAY_MS` / `CRAFT_BLANK_MS` / `CRAFT_PENDING_MIN_MS` | `300` / `300` / `0` | | `withErrorComponent` | `CRAFT_ERROR_COMPONENT` | `null` | | `withRouteLoadError` | `CRAFT_ROUTE_LOAD_ERROR_COMPONENT` / `CRAFT_ROUTE_LOAD_RETRY` | `null` / one retry after 250 ms | | `withCraftViewTransitions` | `CRAFT_VIEW_TRANSITIONS_ENABLED` / `CRAFT_VIEW_TRANSITION_SKIP_BLANK` | `false` / `false` | | `withA11yNavigationFocus` | `CRAFT_A11Y_NAVIGATION_FOCUS` | `false` | The default pending component renders `CRAFT_LOADING_TEXT`, which reads `LOCALE_ID` and picks a built-in translation (`Loading…` / `Chargement…`). ## Per-route overrides Any route may override the defaults via craft-only fields (stripped from the emitted Angular `Route`): ```ts craftRoute('user/:userId', { // … stayMs: 150, // shorten the "keep previous page" window blankMs: 0, // skip the blank phase → straight to loader pendingComponent: () => import('./user-skeleton'), // reactiveGuards: false, // opt out of live guards (on by default) }), ``` ## View Transitions Angular's `withViewTransitions()` brackets **only the synchronous URL commit** in `document.startViewTransition()`. With the non-blocking outlet that is the wrong instant: the target component mounts **after** the guard/resolve chain settles, so a shared-element morph captures `previous page → (stay/loader)` and the real `previous → target` morph is lost — worse, a full-screen loader becomes the captured "old" frame. `withCraftViewTransitions()` hands the morph to the **outlet** instead: it drives `document.startViewTransition()` around its **own** swaps (`previous page → skeleton → target`), so the morph survives even a slow chain. It guards `prefers-reduced-motion`, falls back to a plain swap when the API is missing, and is overridable in tests via the `CRAFT_START_VIEW_TRANSITION` seam. ```ts provideCraftRouter( appRoutes.toRoutes(), withCraftViewTransitions(), // replaces Angular's withViewTransitions() ), ``` ### Shared element across a slow chain For the morph to bridge a slow navigation, **something** carrying the shared element's `view-transition-name` must stay on screen while the chain runs — the **pending skeleton**. A route opts in by **declaring the shared-element payload shape** with `viewTransitionPayload()` — the view-transition analogue of how `queryParams` declares a route's query-params shape. This: * makes a typed `viewTransition: T | null` payload **required** on every `craftRouterLink` / `navigate` targeting it (`null` is an explicit opt-out); * exposes a route-generated, fully-typed `injectXxxViewTransition(): Signal` helper; * tells the outlet to **skip the blank phase** (a blank would break the morph): `stay → pending → loaded`. ```ts export const { photosRoutes, injectPhotosPhotoIdViewTransition } = craftRoutes( 'photos', [ craftRoute( ':photoId', { componentDeps: {} as import('./photo-detail').GenDeps_PhotoDetailComponent, loadComponent: ({ withRetry }) => withRetry(import('./photo-detail')), withLoaderViewTransitionImage: viewTransitionPayload<{ name: string; image: string | null; }>(), pendingComponent: () => import('./photo-skeleton'), // The skeleton's DI is verified separately (see "Verifying the skeleton's DI"). canActivate: function* () { /* slow guard */ }, }, { /* … */ }, ), ], ).withParent>(); ``` This collection is a lazy child mounted via `loadChildren` (kept out of the parent's cascade DI budget). Because its components depend on the `:photoId` param **and** the declared view-transition payload, it is only correct under the `photos` route — so it is **pinned** to that mount with `.withParent>()`, and the parent enforces it with `assertChildRouteMounts(...)`. See [Pinning a lazy child to its mount path](/guide/routing/setup#pinning-a-lazy-child-to-its-mount-path-withparent-assertchildroutemounts). The link passes a payload of the **declared type** (required, and shape-checked): ```ts [craftRouterLink]="{ to: 'photos/:photoId', params: { photoId: photo.id }, viewTransition: { name: 'photo-' + photo.id, image: photo.preview }, }" ``` The skeleton (and/or the target) reads it through the **route-generated typed helper** and wears the matching `view-transition-name`: ```ts export default class PhotoSkeleton { protected readonly photoId = injectPhotosPhotoIdParams(); // Signal<{ name: string; image: string | null } | null> — typed by the route. private readonly viewTransition = injectPhotosPhotoIdViewTransition(); protected readonly image = computed( () => this.viewTransition()?.image ?? null, ); // template: } ``` > The global, untyped `injectCraftViewTransition(): Signal` still exists for ad-hoc reads, but > prefer the route-generated helper when you have a declared payload. The payload travels in Angular's navigation `state`, so it is **lost on reload or direct URL access** — there is no previous page to morph from in that case anyway; the app stays functional (skeleton without the preview image, then the target). Pass `withCraftViewTransitions({ skipBlank: true })` to skip the blank phase for **every** route, not just opted-in ones. ### Verifying the skeleton's DI The pending skeleton is a real component that injects dependencies (route params, the typed payload, monitoring, …), but the aggregated cascade (`ValidateCascadeRoutesFile`) only sees the **target** component — it never descends into `pendingComponent`. So the skeleton is verified **directly**, with the per-component, O(1) [`RouteCheckedDI`](/guide/routing/setup#escape-hatch-the-o-1-per-route-check) escape hatch (not a second aggregated pass — that would add to the instantiation-count budget the cascade is already spending): ```ts type _CheckTargetDI = ValidateCascadeRoutesFile< AppNames, AppValues, typeof photosRoutes >; type _CanRunTarget = CanRun<_CheckTargetDI>; // The skeleton injects the `:photoId` param and the typed payload — both // auto-provided by the route, so list those service names as available; the // parent context (`AppValues` here) is the same one the cascade check uses. type _CheckPendingDI = RouteCheckedDI< import('./photo-skeleton').GenDeps_PhotoSkeletonComponent, 'PhotosPhotoIdParams' | 'PhotosPhotoIdViewTransition', AppValues, 'pending component: photos/:photoId' >; type _CanRunPending = CanRun<_CheckPendingDI>; ``` A service the skeleton injects but nothing provides becomes a TypeScript error on `_CanRunPending` (`The X service is not provided in pending component: photos/:photoId`). The `craft-ng/require-pending-component-di-check` ESLint rule **generates and refreshes this whole block** from `pendingComponent` on `--fix` — resolving the skeleton's `GenDeps_*`, deriving the auto-provided service names from the route's path params + payload, and borrowing the parent context from the collection's own `ValidateCascadeRoutesFile` — so you never hand-write or stale it. [Architecture tests](/guide/testing/architecture#assertroutediproofs) (`assertRouteDiProofs`) fail if that pending proof is missing or not armed with `CanRun`. ## See Also * [Route exception handling](/guide/routing/exception-handling) * [Route guards](/guide/routing/guards) — what the outlet is waiting on * [Global error component](/guide/routing/global-error-component) * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` keeps the pending-component proof armed --- --- url: >- https://ng-angular-stack.github.io/craft/guide/routing/global-error-component.md --- # Global error component One component, declared once, for every failure a route decided not to handle locally. **Use it when** several exception codes deserve the same screen, or as the backstop for unexpected errors. **Not when** a specific failure needs its own UI — `renderComponent(...)` in the route's handler is more precise. See [Route exception handling](/guide/routing/exception-handling). When a route exception handler delegates to `globalError()`, the outlet renders one application-wide error component and feeds it the exception. That component can read **all** of its possible exceptions — typed and exhaustive — because the codes routed to it are mirrored in a global registry maintained automatically by ESLint. ## Register the component Pass `withErrorComponent(...)` directly to `provideCraftRouter(...)` (mixed with your router features): ```ts provideCraftRouter( appRoutes.toRoutes(), withComponentInputBinding(), withErrorComponent({ component: MyGlobalErrorScreen, componentDeps: {} as import('./my-global-error-screen').GenDeps_MyGlobalErrorScreen, }), ), ``` It also works standalone via `provideCraftLoading(withErrorComponent({ component, componentDeps }))`. ## Consume the exception ```ts export const MyGlobalErrorScreen = craftComponent( 'MyGlobalErrorScreen', {}, function* () { // Signal const error = yield* CraftGlobalError(); return { message: computed(() => { switch (error()?.code) { case 'USER_DISABLED': return 'This account is disabled.'; default: return 'Something went wrong.'; } }), }; }, ({ message }) => div(h1(() => message())), ); ``` `CraftGlobalError()` is typed as the **union of every exception** any route delegates to the global component, so `switch (error().code)` is exhaustively typed. The outlet writes the active exception into `CRAFT_GLOBAL_ERROR` just before rendering the component. ## The registry (auto-maintained) The union comes from `CraftGlobalExceptionRegistry`, keyed by route path and code: ```ts declare module '@craft-ng/core' { interface CraftGlobalExceptionRegistry { 'user/:userId': { USER_DISABLED: CraftRouteExceptionType< typeof demoRoutes, 'user/:userId', 'USER_DISABLED' >; HttpError: CraftRouteExceptionType< typeof demoRoutes, 'user/:userId', 'HttpError' >; }; } } ``` **Do not edit this block by hand.** The `craft-ng/global-exception-registry-match` ESLint rule detects every `handleExceptions` handler that calls `globalError()` and keeps the registry in sync: ```bash npx nx lint demo --fix ``` A missing entry is reported as an error; `--fix` inserts the `[path][code]` entry. `CraftRouteExceptionType` resolves the typed exception object for a code on a route from the collection's route definitions (no type checker required — the rule builds the reference from the collection variable and the path/code literals). The screen itself still needs an armed `RouteExceptionComponentCheckedDI` in `app.config.ts`. [Architecture tests](/guide/testing/architecture#assertroutediproofs) fail if that proof is missing. ## Default behaviour If no `withErrorComponent` is configured, `globalError()` and unhandled thrown errors leave the outlet in its `error` state without a component. Provide a global error component to render a fallback UI. ## See Also * [Route exception handling](/guide/routing/exception-handling) — where `globalError()` is returned * [Route load errors](/guide/routing/route-load-errors) * [Non-blocking navigation](/guide/routing/pending-ui) * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` keeps the error-screen proof armed --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/route-load-errors.md --- # Route load errors This is the failure mode nothing else covers: the route is valid, the guards passed, and the **JavaScript chunk itself** never arrives — a stale hash after a deploy, a flaky network, an offline user. **Use it when** your app is lazy-loaded and deployed more than once. Which is to say: use it. `withRouteLoadError(...)` handles failures that happen before Angular can mount the target route: lazy `loadComponent` / `loadChildren` chunks that fail to load, rejected dynamic imports, stale deployments, CDN errors, or offline transitions. This is different from [`handleExceptions`](/guide/concepts/exceptions): route exceptions are business exceptions raised by guards, resolvers, or route code. Route load errors happen while Angular is trying to fetch the JavaScript needed to activate the route. ## Register the route-load error screen Pass `withRouteLoadError(...)` to `provideCraftRouter(...)`, next to Angular router features and other craft loading features: ```ts import { provideCraftRouter, withRouteLoadError, withErrorComponent, } from '@craft-ng/core'; provideCraftRouter( appRoutes.toRoutes(), withErrorComponent({ component: MyGlobalErrorScreen, componentDeps: {} as import('./my-global-error-screen').GenDeps_MyGlobalErrorScreen, }), withRouteLoadError({ component: MyRouteLoadErrorScreen, componentDeps: {} as import('./my-route-load-error-screen').GenDeps_MyRouteLoadErrorScreen, retry: { attempts: 1, delayMs: 250, }, }), ); ``` The component must be eager. Do not configure the route-load error screen with `loadComponent`: the failure case is precisely that lazy JavaScript may be unavailable. ## Runtime behaviour When a lazy route load fails, Craft: 1. runs the configured retry strategy; 2. converts the final failure to a `craftException` with code `CRAFT_ROUTE_LOAD_ERROR`; 3. renders the configured route-load error component; 4. keeps the browser URL on the original target URL. The last point matters. Internally, Angular activates a technical recovery route so there is something safe to render, but `browserUrl` keeps the visible URL as the intended route: ```text /mutation/123 → lazy chunk fails → retry fails → route-load error screen is shown → browser URL stays /mutation/123 → F5 reloads /mutation/123 and retries the real route ``` ::: info No dedicated loading UI during JavaScript fetches yet While Angular is fetching a lazy `loadComponent` / `loadChildren` chunk, including time spent in the configured retry strategy, Craft does not currently display the route's `pendingComponent` or another dedicated loading component. The pending component starts only after the JavaScript has loaded and the route has been activated, while the Craft `canMatch` / `canActivate` / `resolve` chain is running. Extending the pending timeline to cover slow chunk downloads and retries is planned as a future evolution. Until then, the previous route may remain visible while the JavaScript request is pending; the route-load error component appears only after all configured retries fail. ::: ::: warning Browser-cached module failures Browsers can remember a failed dynamic `import()` for the exact same module specifier. Wrap each Craft lazy route import with the loader's `withRetry` helper: ```ts loadComponent: ({ withRetry }) => withRetry(import('./detail')), loadChildren: ({ withRetry }) => withRetry(import('./admin.routes')).then((m) => m.adminRoutes), ``` The initial import remains statically analyzable, so Angular and Vite still rewrite it to the hashed production chunk. On a configured retry, Craft extracts the emitted chunk URL from the browser error and adds `__craft_route_retry` only to the failed request. A successful retry module is kept for the lifetime of the application and reused by later route activations. This recovery depends on the browser including the failed module URL in the dynamic-import error. When it does not, `reload()` remains the reliable recovery path. Do not write `import(withRetryPrefix('./detail'))`: a runtime import specifier prevents the production chunk from being statically discovered. ::: ## Build the error component The component can inject both the active technical exception and the recovery API: ```ts import { button, craftComponent, div, h2, p } from '@craft-ng/component'; import { CraftRouteLoadError, CraftRouteLoadRecovery, provideHostName, } from '@craft-ng/core'; export const MyRouteLoadErrorScreen = craftComponent( 'MyRouteLoadErrorScreen', { providers: [provideHostName('component:MyRouteLoadErrorScreen')], styles: ` :scope { padding: 2rem; border: 1px solid #f97316; border-radius: 8px } .actions { display: flex; gap: .75rem; margin-top: 1rem } `, }, function* () { return { error: yield* CraftRouteLoadError(), recovery: yield* CraftRouteLoadRecovery(), }; }, ({ error, recovery }) => { const current = error(); return div([ h2('Route could not be loaded'), p( current ? `Failed to load ${current.payload.phase} for route "${current.payload.routePath}" after ${current.payload.attempt} attempts.` : 'The requested route chunk could not be loaded.', ), div({ class: 'actions' }, [ button({ click: () => void recovery.retry() }, 'Retry route load'), button({ click: () => recovery.reload() }, 'Reload app'), ]), ]); }, ); ``` `CraftRouteLoadError()` yields a signal of the reserved `craftException`. Its payload includes: * `phase`: `'component'` or `'children'`; * `routePath`: the route definition path that failed; * `targetUrl`: the URL the user tried to reach; * `cause`: the final error thrown by the loader/retry strategy; * `attempt`: the number of load attempts made. `injectCraftRouteLoadRecovery().retry()` navigates back to `targetUrl`; `reload()` refreshes the browser. ## Configure retry globally The default retry is one retry after 250 ms. You can make it explicit in `withRouteLoadError(...)`: ```ts withRouteLoadError({ component: MyRouteLoadErrorScreen, componentDeps: {} as import('./my-route-load-error-screen').GenDeps_MyRouteLoadErrorScreen, retry: { attempts: 2, delayMs: 500, }, }); ``` `attempts` is the number of retry attempts after the initial failure. So `attempts: 2` means at most three loader calls total: the initial call plus two retries. Use callbacks when retry behaviour depends on the error: ```ts withRouteLoadError({ component: MyRouteLoadErrorScreen, componentDeps: {} as import('./my-route-load-error-screen').GenDeps_MyRouteLoadErrorScreen, retry: { attempts: 3, shouldRetry: (error, context) => { // Only retry dynamic import / chunk loading failures. if (!(error instanceof TypeError)) return false; // Stop earlier for a route where retrying is known to be useless. return context.routePath !== 'admin'; }, delayMs: (_error, context) => { // Simple backoff: retry attempt 2 waits 250 ms, attempt 3 waits 500 ms, … return 250 * (context.attempt - 1); }, }, }); ``` The retry context passed to callbacks contains `phase`, `routePath`, `targetUrl`, `attempt`, and `error`. The `attempt` value is the load attempt about to run. After the first failed load, the first retry callback receives `attempt: 2` and `error` set to the initial failure. For custom logic, pass a retry strategy: ```ts withRouteLoadError({ component: MyRouteLoadErrorScreen, componentDeps: {} as import('./my-route-load-error-screen').GenDeps_MyRouteLoadErrorScreen, retry: { async execute(loader, context) { console.warn('route load failed, retrying', context); return loader(); }, }, }); ``` The strategy can also be an injectable class implementing `CraftRouteLoadRetry`. ## Override per route Both the retry strategy and the rendered component are regular DI providers. Override them on a specific route when the failure should have local behaviour: ```ts import { provideRouteLoadErrorComponent, provideRouteLoadRetry, } from '@craft-ng/core'; craftRoute('admin', { providers: [ provideRouteLoadRetry({ attempts: 3, delayMs: 1_000, }), provideRouteLoadErrorComponent({ component: AdminRouteLoadErrorScreen, componentDeps: {} as import('./admin-route-load-error-screen').GenDeps_AdminRouteLoadErrorScreen, }), ], loadChildren: ({ withRetry }) => withRetry(import('./admin.routes')).then((m) => m.adminRoutes), }); ``` The local component receives the same `injectCraftRouteLoadError()` and `injectCraftRouteLoadRecovery()` values, resolved through the failing route's injector. ## DI checks Route-load error components participate in the same generated DI checks as other error surfaces. The ESLint rule `craft-ng/require-exception-component-di-check` generates `RouteExceptionComponentCheckedDI` checks for: * global `withRouteLoadError(...)` components; * route-local `provideRouteLoadErrorComponent(...)` components. Run ESLint with `--fix` after adding or changing a route-load error component: ```bash npx nx lint your-app --fix ``` Do not hand-maintain the generated `_Check*DI` blocks. [Architecture tests](/guide/testing/architecture#assertroutediproofs) (`assertRouteDiProofs`) fail if a registered route-load error screen has no armed `RouteExceptionComponentCheckedDI`. ## See Also * [Routing setup](/guide/routing/setup) — `withRetry` on lazy imports * [Global error component](/guide/routing/global-error-component) * [Non-blocking navigation](/guide/routing/pending-ui) * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` keeps the error-screen proof armed --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/scaling.md --- # Scaling routes `ValidateCascadeRoutesFile` walks every route in a collection **at the type level**, so a single routes file has a finite budget before TypeScript's instantiation ceiling. This page is about what happens at that ceiling, and how to organise routes so you never reach it. ::: tip You don't need this yet If your app has one routes file with a handful of routes, [Setup](/guide/routing/setup) is enough. Come back when a file grows past a few dozen routes, or when you see `TS2589`. ::: ## Large route files — the cascade DI depth limit `ValidateCascadeRoutesFile<…, typeof appRoutes>` walks **every** route in the collection at the type level. TypeScript caps how deeply it will instantiate a recursive type, so a single collection has a **finite route budget**. Past it (in practice a few dozen routes, sooner if routes carry guards / `resolve` / `handleExceptions`), the check overflows: ``` TS2589: Type instantiation is excessively deep and possibly infinite. app.routes.ts → ValidateCascadeRoutesFile ``` ::: warning Watch out for the knock-on collapse A `TS2589` makes TypeScript abandon that type and fall back to `any`, which **poisons inference of neighbouring `const`s in the same file**. The visible symptoms are misleading: `craftRoute(...)` calls collapse to `RouteWithProvidersBuilder<{ path }>`, the `craftRoutes(...)` helpers go missing (`Property 'injectXxx' does not exist`), and `craftRouterLink` targets type as `never`. The root cause is the overflowing check, not those routes. ::: **Solution — split into a lazy child collection, and keep its own DI check.** The cascade check reads only the *current* collection's metadata; it does **not** descend into `loadChildren`. So move the extra routes into their own `craftRoutes(...)` file and reference it via `loadChildren`. That keeps the parent file under budget — **but a child collection ships with *no* DI checking unless you add one**, so re-declare the check in the child file to keep DI sound. [Architecture tests](/guide/testing/architecture#assertroutediproofs) fail if that child proof is missing. ```ts // feature.routes.ts — its own lazy collection import { craftRoutes, craftRoute, type CanRun, type ValidateCascadeRoutesFile, } from '@craft-ng/core'; import type { Router } from '@angular/router'; export const { featureRoutes } = craftRoutes('feature', [ craftRoute('', { componentDeps: {} as import('./feature').GenDeps_Feature, loadComponent: ({ withRetry }) => withRetry(import('./feature')), // guards / resolve / handleExceptions … }), ]); // DI safety for THIS collection — `app.routes.ts` does NOT cover loadChildren. // Same parent context the parent route runs under: app-level `Router` by value, // no extra named providers. type _CheckFeatureDI = ValidateCascadeRoutesFile< never, Router, typeof featureRoutes >; type _CanRunFeature = CanRun<_CheckFeatureDI>; ``` ```ts // app.routes.ts — a cheap loadChildren entry, outside the parent's budget { path: 'feature', loadChildren: ({ withRetry }) => withRetry(import('./feature.routes')).then((m) => m.featureRoutes), }, ``` A missing provider in the child collection now surfaces as a TypeScript error **in the child file**, exactly like the main one: ``` The SomeService service is not provided in path: "" ``` If a single feature is itself large, repeat the split, or break one big collection into several `craftRoutes(...)` collections each with its own check — every check then validates a smaller slice and stays under the depth limit. The takeaway: **DI is always verified — never drop the check; move it next to the routes it covers.** ### Why the budget exists (the mechanism) `ValidateCascadeRoutesFile` recurses over the route tuple **4 routes per step**, so a file of `N` routes recurses to depth `N / 4`. Two distinct TypeScript ceilings are in play: * **Instantiation *depth*** (the `TS2589` "excessively deep" error). The 4-at-a-time unrolling is what fights this: it quarters the recursion depth, so the wall moves from ~50 routes to a few hundred — but it is still a per-file ceiling. * **Total instantiation *count***. Each route pays one full `RouteCheckedDI` instantiation (walking its `GenDeps`, the `missingProvider` map, the parent context). The total cost is therefore roughly **`N × cost-per-route`**, and a route carrying guards / `resolve` / `handleExceptions` costs several times more than a trivial one. This is why "a few dozen" is only a rough figure — the real budget is in route-*cost*, not route-*count*. ## Scaling to hundreds of routes The split above is not a one-off patch — it is the architecture. Organise routes as a **tree of feature files joined by `loadChildren`** (which you want anyway for code-splitting): ``` app.routes.ts # "manifest": ~N cheap { path, loadChildren } entries ├── billing.routes.ts # ~15–20 leaf routes + its own check ├── admin.routes.ts # ~15–20 leaf routes + its own check └── reporting.routes.ts # if itself large → re-split into sub-loadChildren (level 3+) ``` * A `{ path, loadChildren }` entry has no `componentDeps`, so it is **nearly free** in the parent's cascade check — the manifest can list dozens of them. * Each feature file pays the budget for **its own leaves only**. ~500 routes ÷ ~17 per file ≈ ~30 files; two levels are plenty, and you can nest further without limit. * **Every `craftRoutes(...)` file re-declares its own check** (see the iron rule above). With many files this is easy to forget and fails silently, so enable `craft-ng/require-cascade-route-di-check`. ::: tip Threading the parent DI context The child check's parent context (`ParentNames`, `ParentValues`) is everything provided **at its mount point** — app providers **plus** every ancestor route's providers. When no ancestor adds `providers`, this is just the app context (``, as in the examples above), identical in every file. When an ancestor route *does* add providers, re-export its cumulative context and union your own onto it: ```ts // billing.routes.ts (mounted under a route with providers: [provideBilling()]) export type BillingChildNames = AppProvidedNames | 'BillingService'; export type BillingChildValues = AppProvidedValues; // sub-billing.routes.ts type _Check = ValidateCascadeRoutesFile< BillingChildNames, BillingChildValues, typeof subRoutes >; ``` Forgetting to fold in an ancestor's provider makes the child check wrong (a real missing-provider bug slips through, or a provided service is flagged as missing), so keep the re-export next to the route that adds the providers. ::: ### Escape hatch — the `O(1)`-per-route check If a single file genuinely must hold a large flat list (no natural `loadChildren` boundary), switch it from the aggregated `ValidateCascadeRoutesFile` to the **per-route** `RouteCheckedDI`. It validates one component at a time with **no recursion between routes**, so it never hits the depth ceiling and scales to thousands of routes in one file — at the cost of one check block per component instead of one per file: ```ts import { type CanRun, type RouteCheckedDI } from '@craft-ng/core'; type _CheckItem0 = RouteCheckedDI< import('./item-0').GenDeps_Item0Component, AppProvidedNames, AppProvidedValues, 'Item0Component' >; type _CanRunItem0 = CanRun<_CheckItem0>; // …one pair per route component ``` Prefer the tree-of-`loadChildren` approach (it also lazy-loads); reach for `RouteCheckedDI` only when a single big file is unavoidable. ## Pinning a lazy child to its mount path (`.withParent` + `assertChildRouteMounts`) Splitting into `loadChildren` keeps each file under budget, but nothing yet guarantees a child is wired under the **right** parent route. A child whose components rely on a specific mount — its `:photoId` param, a declared view-transition payload, an ancestor's `providers` — is only correct under that path. Mount it elsewhere and its DI assumptions break silently. Pin a collection to its mount path with `.withParent>()`, then enforce it once in the parent with `assertChildRouteMounts(parentRoutes)`: ```ts // view-transitions.routes.ts — the child declares where it belongs import { craftRoutes, craftRoute, type ParentRoutes } from '@craft-ng/core'; export const { viewTransitionsRoutes } = craftRoutes('viewTransitions', [ craftRoute(':photoId', { componentDeps: {} as import('./photo-detail').GenDeps_PhotoDetailComponent, loadComponent: ({ withRetry }) => withRetry(import('./photo-detail')), // … }), ]).withParent>(); ``` ```typescript // app.routes.ts — the parent enforces placement (scoped to this file) import { assertChildRouteMounts, craftRoutes } from '@craft-ng/core'; export const { demoRoutes } = craftRoutes('demo', [ { path: 'view-transitions', loadChildren: ({ withRetry }) => withRetry(import('./view-transitions.routes')).then( (m) => m.viewTransitionsRoutes, ), }, ]); assertChildRouteMounts(demoRoutes); ``` Mount the pinned collection under any other path and the **parent file** fails to compile: ``` craftRoutes(...).withParent>() must be loadChildren-mounted under the route with path 'view-transitions', not 'admin' ``` Notes: * **Opt-in.** A collection without `.withParent` is *unpinned* and mountable anywhere — fully backward compatible. Pin only the children whose placement actually matters. * **Scoped to the parent.** `assertChildRouteMounts` reads the parent's **own** routes (`_routes`) — it does **not** descend into / re-validate the child (already checked in its own file), so it adds nothing to the child's instantiation budget. * **Type-only.** `.withParent<…>()` returns the same object at runtime; `ParentRoutes<'path'>` carries no value, only the path string — so importing it creates no runtime coupling between the files. * **Enforced by ESLint.** `craft-ng/require-child-route-mount-check` adds the missing `assertChildRouteMounts(...)` call + import on `--fix`. (Whether a child opts in with `.withParent` stays your decision — it expresses the "this belongs here" intent the rule can't guess.) ::: details Design notes — two approaches we rejected Reaching the standalone-assert design above took two dead ends, both defeated by TypeScript's instantiation ceiling. They're recorded here because the failure modes are instructive. **1. Enforcing placement inside `craftRoutes(...)` itself.** The first attempt wove the mount check into the `routes` argument type of **every** `craftRoutes(...)` call, so a wrong mount would error right at the route literal. It type-checked — but the extra per-collection instantiation tipped an already-at-ceiling file into `TS2589`, and even a 2-route child with **no** `loadChildren` paid the cost (every collection runs the same inference). The lesson: the check must be **scoped to the parent that actually mounts children** — a standalone `assertChildRouteMounts(...)` reading the raw `_routes` — not folded into the hot `craftRoutes` inference that every file pays on every build. **2. A `loadChildrenType` carrier to speed up the check.** To avoid inferring the child's type through the dynamic `import('./x').then((m) => m.xRoutes)`, we tried an explicit `loadChildrenType: {} as typeof import('./x').xRoutes` field on the lazy route. In isolation it built fine — but applied across the board it **materialises the child's full type (components included)**, which creates a **circular reference** for any child whose components inject the *parent's* route data (`TS2615` "circularly references itself" + `TS2589`): `parent → typeof childRoutes → child components → inject parent data → parent`. Since the dynamic-import resolution it replaced was both cycle-safe and — once measured against build-time noise — no slower, the carrier was dropped. `assertChildRouteMounts` resolves the child's pin through the existing `loadChildren` instead. ::: ## See Also * [Setup](/guide/routing/setup) * [Architecture rules](/guide/testing/architecture) — `assertRouteDiProofs` catches a split file with no check * [Route providers](/guide/routing/route-providers) --- --- url: https://ng-angular-stack.github.io/craft/guide/routing/angular-brand-config.md --- # Angular brand config The `craft-brand` codemod reads a component's Angular metadata to generate its `GenDeps_*`. When a symbol's real dependency isn't visible in that metadata — a pipe that needs a service, a library that provides implicitly — a project-level `craft-brand.config.ts` tells the codemod about it. **Use it when** a generated `GenDeps_*` is missing a dependency you know exists. **Not as a first step** — the default rules cover most of Angular, including `@angular/router`. `@craft-ng/dev-tools` can augment generated `GenDeps` from Angular metadata with a project-level `craft-brand.config.ts`. This is useful when a standalone import implies an extra DI dependency that does not belong to your component code directly. ## What It Solves Some third-party Angular APIs are visible in `imports`, but the actual runtime dependency sits elsewhere. Typical example: * `TranslatePipe` is imported in the component * `TranslateService` is the provider you want to track explicitly Instead of repeating that rule in every component, you can register it once in your project config. ## Config File Create a `craft-brand.config.ts` file at the root of your app or library: ```ts import { defineAngularBrandConfig } from '@craft-ng/dev-tools'; export default defineAngularBrandConfig({ importAugmentations: [ { match: { module: '@ngx-translate/core', symbols: ['TranslatePipe'], metadata: ['imports'], }, deps: [{ key: 'TranslateService', symbol: 'TranslateService' }], missingProvider: [ { key: 'TranslateService', symbol: 'TranslateService' }, ], }, ], }); ``` The codemod auto-discovers `craft-brand.config.ts`, and both `brand-angular-gen-deps-required` and `brand-angular-deps-match` use the same config so lint and generated types stay aligned. ## Matching Rules Each `importAugmentations` entry is declarative: * `match.module`: package name to match * `match.symbols`: optional exported symbols that trigger the rule * `match.metadata`: where the symbol must be used, currently `imports` and `hostDirectives` * `deps`: extra entries added to generated `GenDeps` * `missingProvider`: extra entries added to generated `missingProvider` Only actual Angular metadata usage is matched. A plain TypeScript import in the file is ignored if it is not used in `imports` or `hostDirectives`. ## Generated Result For a standalone component like this: ```ts import { Component } from '@angular/core'; import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'app-home', standalone: true, imports: [TranslatePipe], template: `home.title`, }) export class HomeComponent {} ``` The generated dependency shape can include: ```ts type GenDeps_HomeComponent = GetDeps<{ deps: { TranslatePipe: TranslatePipe; TranslateService: TranslateService; }; provided: {}; missingProvider: { TranslateService: TranslateService; }; publicProperties: GetPublicComponentProperties; }>; ``` That keeps the template import and the implicit provider explicit in the same generated type. ## Built-in Router Behavior `@angular/router` already has a built-in augmentation rule in the codemod. If a symbol from `@angular/router` is used in Angular metadata, the codemod can automatically associate it with `Router` in generated dependencies. Project config uses the same augmentation mechanism for your own libraries. ## Notes * The config file is `TypeScript` only in this iteration. * Rules are declarative only. * `symbol` must be a normal importable type name. * This feature augments generated types; it does not change Angular runtime metadata. ## See Also * [`toCraftService`](/guide/app/integrate-existing) * [`Browser Boundaries`](/guide/testing/browser-boundaries) --- --- url: https://ng-angular-stack.github.io/craft/guide/components.md --- # Components A Craft component is a **function**, not a class. No decorator, no separate template file, no host element wrapped around your markup. **Use it for** anything you would have written as an Angular component. **Keep Angular components** where you have them — the two coexist, and [`loadCraftComponent`](/guide/routing/setup) mounts a Craft one on a route. ## Install The component renderer is published as a separate package and is currently on the `beta` channel: ```shell npm i @craft-ng/core@beta @craft-ng/component@beta ``` See [`@craft-ng/component` on npm](https://www.npmjs.com/package/@craft-ng/component). ## The shape ```typescript craftComponent(name, meta, factory, template); ``` | Argument | What it is | | ---------- | ----------------------------------------------------------------- | | `name` | the component's name — used for host tags, snapshots, diagnostics | | `meta` | `providers`, `styles`, `host`, `contentStyles` | | `factory` | the **logic**: builds and returns the context | | `template` | receives that context, returns nodes | ```ts import { craftComponent, each, h1, li, ul } from '@craft-ng/component'; import { state } from '@craft-ng/core'; type Task = { id: string; title: string; done: boolean }; export const Tasks = craftComponent( 'Tasks', {}, function* () { const tasks = yield* state('tasks', [] as Task[]); return { tasks }; }, ({ tasks }) => [ h1('Tasks'), ul(each(tasks, { track: (task) => task.id }, (task) => li(task.title))), ], ); ``` The split matters: the factory produces a context **without touching the DOM**, and the template renders a context **without running the factory**. That is what makes the two [testable independently](/guide/testing/components). ## The logic factory A `function*` when it needs dependencies — every `yield*` is tracked and folds into the component's dependency type: ```typescript function* () { const tasks = yield* TaskList(); return { tasks }; } ``` A plain arrow when it needs none: ```typescript () => ({}); ``` Whatever it returns is the context the template receives. Nothing else is exposed. ## Inputs and outputs They are **parameters of the factory**, typed with `Input` and `Output`: ```ts import { Input, Output, button, craftComponent, div, span, } from '@craft-ng/component'; import { deepYieldable } from '@craft-ng/core'; type User = { name: string }; const UserCard = craftComponent( 'UserCard', {}, (user: Input, onRemove: Output<(user: User) => void>) => ({ user: deepYieldable(user), onRemove, }), ({ user, onRemove }) => div([ span(user.name), button({ type: 'button', *click() { yield* onRemove(yield* user()); }, }, 'Remove'), ]), ); ``` An `Input` **is a yieldable reader** — `yield* user()` reads the current value. An `Output` is a yieldable callback; delegate to it with `yield*`. Rendering a child is a function call, so there is no binding layer to get wrong: ```typescript UserCard({ user: currentUser, onRemove: removeUser }); ``` | Angular | Craft | | ------------------------------------------- | ------------------------------------------- | | `@Input()` / `input()` / `input.required()` | an `Input` factory parameter | | `@Output()` / `output()` + `.emit(...)` | an `Output` parameter, called directly | | `[user]="u"` / `(remove)="fn($event)"` | `UserCard({ user: u, onRemove: fn })` | | Missing required input → runtime | missing parameter → **compile error** | ## The template Nodes are built with hyperscript helpers — `div`, `ul`, `button`, and `h(tag, …)` for anything without one. Pass a yieldable reader to a binding. Use a generator when the binding must format or call a method: ```typescript ({ tasks }) => [ h1(function* () { return `Tasks — ${yield* tasks.remaining()} left`; }), h1(`Tasks — static`); // static text needs no reader ]; ``` The same binding boundary applies to attributes, DOM properties, classes, styles, and host props. Prefer exposing a derived reader on the primitive (`tasks.isEmpty`) and passing it (`disabled: tasks.isEmpty`) over wrapping a synchronous call. See [Fine-grained reactivity](/guide/components/fine-grained-reactivity) for the complete rendering model, structural scopes, observability expectations, and migration checklist. Keep render callbacks pure. They may read signals and calculate values, but must not call `set`, `update`, or `mutate`. Perform writes from DOM events, outputs, mutations, or explicit business effects. Enable `craft-ng/no-render-writes` to diagnose common violations. Control flow is made of functions rather than syntax — `each`, `ifBlock`, `matchBlock`, `defer`. The correspondence with Angular's blocks, and why a raw ternary is the wrong tool for **structure**, is in [Learn step 2](/learn/02-derive#control-flow-the-angular-equivalents). ## The meta ```typescript craftComponent( 'Card', { providers: [provideCardStore()], styles: ':scope { padding: 1rem } .title { font-weight: 700 }', host: { class: 'card-host' }, }, /* … */ ); ``` * **`providers`** — the component's own DI scope, evaluated before the template. * **`styles`** — scoped with CSS `@scope`; `:scope` is this component's root. See [Encapsulated styles](/guide/components/styles). * **`host`** — default properties for the root element. * **`contentStyles`** — styles offered to projected content, per slot. See [Content projection](/guide/components/content-projection). ## Composing behaviour `.pipe(...)` attaches directives, which decorate **both** the logic factory and the template, left to right: ```typescript const EditablePanel = Panel.pipe(WithPermission); ``` The same mechanism carries `withProviders(...)` and the exception handlers below. See [Directives and `.pipe(...)`](/guide/components/directives). ## Mounting the root The app root is a Craft component too: ```typescript // app.config.ts export const appConfig = craftAppConfig({ providers: [provideCraftRootComponent(App)], }); ``` ```typescript // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { CraftRootComponentHost } from '@craft-ng/component'; import { toApplicationConfig } from '@craft-ng/core'; bootstrapApplication(CraftRootComponentHost, toApplicationConfig(appConfig)); ``` `toApplicationConfig` produces the `ApplicationConfig` Angular expects, so the rest of your bootstrap is unchanged. ## Pitfalls **Reading a reader outside a binding.** `h1(tasks().length)` evaluates once at build time. Pass the reader (`p(tasks.remaining)`) or use a generator: `h1(function* () { return yield* tasks.remaining(); })`. **Forgetting `track` in `each`.** Without a stable identity the renderer cannot reuse, move or remove the right node. **Exceptions from the factory or providers don't vanish.** They become the component's initialization exceptions and flow up to the route unless handled with `.pipe(catchBlock.exhaustive(...))` — see [Exceptions as values](/guide/concepts/exceptions). **Naming mismatch.** The first argument must match the exported binding; the `craft-component-name-match` rule enforces it. ## See Also * [Learn: your first state](/learn/01-first-state) — the guided version * [Directives and `.pipe(...)`](/guide/components/directives) * [Accessibilité](/guide/components/accessibility) * [Testing components](/guide/testing/components) --- --- url: >- https://ng-angular-stack.github.io/craft/guide/components/fine-grained-reactivity.md --- # Fine-grained reactivity Craft templates are reactive at the **binding** level. When a signal changes, Craft updates the text node, DOM property, class, style, or host binding that read it. It does not need to execute the surrounding component template again. ```ts ({ counter }) => div([ h2('Counter'), p({ class: 'value' }, counter), button({ click: counter.increment }, '+'), ]); ``` Here, `counter` is passed to `p` as a yieldable reader. The renderer drives the read for that text binding. Incrementing the counter evaluates that binding and patches its text node; the `div`, heading, button, and component template remain untouched. ## The binding is the reactive boundary A function in a rendered position declares a binding. The callback only reads values already derived by the primitive layer; comparisons, formatting, and UI decisions stay out of the template: ```ts p(items.totalLabel); button( { disabled: items.isEmpty, title: items.clearTitle, }, 'Clear', ); div({ class: items.emptyClass, style: items.emptyStyle, }); ``` `totalLabel`, `isEmpty`, `clearTitle`, `emptyClass`, and `emptyStyle` are named derived values exposed by the state, query, insertion, or component context. Pass the reader. If only an item-related dependency changes, Craft evaluates only the affected bindings. A sibling binding depending on another reader does not run. Static values do not need callbacks: ```ts h2('Shopping cart'); button({ type: 'button' }, 'Clear'); ``` ## Do not read reactive values while building the template A direct read happens while the component constructs its VNodes. It cannot be assigned to one precise DOM binding and becomes a structural template dependency instead: ```ts // Avoid: these reads happen in the component template. p(items.totalLabel()); button({ disabled: items.isEmpty() }, 'Clear'); div({ class: items.emptyClass() }); ``` Move each read into the binding that consumes it: ```ts p(items.totalLabel); button({ disabled: items.isEmpty }, 'Clear'); div({ class: items.emptyClass }); ``` This is also the rule for component inputs. Pass a yieldable reader directly when the child must observe a changing value. When the child needs fields of an object, explicitly adapt the input with `deepYieldable`: ```ts UserCard({ user: selectedUser }); ``` The reader is lazy: constructing the parent template does not read `selectedUser`. Craft installs it as the source of the child's `user` input. The child then decides which granular binding observes it: ```ts const UserCard = craftComponent( (user: Input) => ({ user: deepYieldable(user) }), ({ user }) => h2(user.displayName), ); ``` When the `h2` binding first evaluates, `yield* user()` invokes the reader, which reads `selectedUser`. That text binding becomes the signal consumer. When the selected user changes, only the binding evaluates again and patches the existing `h2`; neither the parent template nor the child component template runs again. Reading the input eagerly in the child would move the dependency back to the component boundary and is rejected by `require-reactive-template-bindings`: ```ts // Avoid: resolving the input while the child template is built. h2(craftUse(user()).displayName); ``` ## Structure has its own reactive scopes Bindings update an existing node. Blocks own changes to the shape of the tree: ```ts ifBlock( hasItems, () => CartItems({ items: () => items() }), () => p('Your cart is empty.'), ); each(items, { track: (item) => item.id }, (item) => p(item.name)); ``` `ifBlock`, `each`, `matchBlock.exhaustive`, and `defer` isolate their own structural work. A branch or list can change without making the parent component rebuild unrelated siblings. Use these helpers for structure and binding callbacks for values on existing nodes. ## Keep bindings pure and free of logic A binding reads a value already derived by the primitive layer. It does not format data, make business decisions, or write state: ```ts // Correct: the primitive exposes the render-ready reader. p(cart.formattedTotal); // Incorrect: rendering changes application state. p(function* () { yield* counter.update((value) => value + 1); return yield* counter(); }); ``` Perform writes from DOM events, outputs, mutations, or explicit business effects. Purity makes a binding safe to evaluate whenever one of its dependencies changes. ## Enforce the model with ESLint Enable both renderer rules with type-aware ESLint configuration: ```js export default [ { files: ['**/*.ts'], languageOptions: { parserOptions: { projectService: true }, }, rules: { 'craft-ng/require-reactive-template-bindings': 'error', 'craft-ng/no-render-writes': 'error', }, }, ]; ``` * `require-reactive-template-bindings` rejects direct reads of Angular Signals, Craft values, and component inputs during VNode construction. * `no-render-writes` rejects detectable `set`, `update`, and `mutate` calls from templates and binding callbacks while allowing event and output handlers. See the [ESLint rules reference](/guide/routing/eslint-rules) for the complete configuration. ## What you should observe After a binding dependency changes: * the affected DOM value changes; * the node keeps its identity; * unrelated bindings do not evaluate; * the component template does not emit a new `component / update` trace. The current template trace reports component and structural renders, not each individual text or property effect. The absence of a component update therefore confirms that the change stayed below the component boundary; a DOM assertion confirms that the expected binding was patched. Effects are owned by their rendered nodes. Removing a branch, list item, or component destroys its binding effects, so their dependencies are released with the DOM they served. ## Migration checklist 1. Move comparisons, formatting, and display decisions into named derived primitive values such as `items.isEmpty`. 2. Pass yieldable readers to text bindings (`p(counter)`), or use a generator when the binding must format: `p(function* () { return \`Count: ${yield\* counter()}\`; })\`. 3. Pass yieldable readers to DOM properties such as `value`, `disabled`, and `title`. 4. Return complete reactive class and style readers from the primitive. 5. Pass changing component inputs as yieldable readers. 6. Express structural changes with `ifBlock`, `each`, `matchBlock.exhaustive`, or `defer`. 7. Enable the two ESLint rules and remove every direct reactive template read. Continue with [Components](/guide/components/) for the complete `craftComponent` model or [Observability](/guide/advanced/observability) to inspect rendering and correlated interactions. --- --- url: https://ng-angular-stack.github.io/craft/guide/components/directives.md --- # Directives and `.pipe(...)` A Craft directive decorates **both** a component's logic factory and its template — so behaviour and markup travel together, and compose. **Use one when** the same behaviour must be added to several components: a tooltip, a highlight, focus management, analytics on interaction. **Not when** the behaviour belongs to one component — put it in that component's factory. Directives are applied from left to right. ```ts import { button, craftComponent, craftDirective, div, p, type HostRequiredLogic, type HostTemplate, type Input, } from '@craft-ng/component'; ``` ## `InteractivePermissions` The examples below use a directive that adds a `permissions` object to the component context. Its configuration is internal to the directive; the component caller only provides the original `user` input. ```ts import { HostRequiredLogic, HostTemplate, Input, craftDirective, } from '@craft-ng/component'; type User = { id?: string; name: string }; type RequiresUser = { user: Input; }; type ProvidesPermissions = RequiresUser & { permissions: { canEdit: () => boolean; }; }; const InteractivePermissions = craftDirective( 'InteractivePermissions', {}, (baseLogic: HostRequiredLogic) => (user: Input) => { const context = baseLogic(user); return { ...context, permissions: { canEdit: () => user().permissions.includes('edit'), }, }; }, (baseTemplate: HostTemplate) => (context) => baseTemplate(context), ); ``` ## Basic composition A directive transforms the existing logic and template: ```ts const Card = craftComponent( 'Card', {}, (user: Input) => ({ user }), ({ user }) => div(user().name), ).pipe(InteractivePermissions); ``` The result of `InteractivePermissions` becomes the logic actually executed by `Card`: ```text component inputs ↓ original logic ↓ logic added by the directive ↓ final context ↓ final template ``` ## Directive configuration input A fixed configuration can be supplied when the directive is created: ```ts const hasPermission = (permission: Permission) => craftDirective( 'hasPermission', {}, (baseLogic: HostRequiredLogic) => (user: Input) => { const context = baseLogic(user); return { ...context, permissions: { canAccess: () => user().permissions.includes(permission), }, }; }, (baseTemplate: HostTemplate) => (context) => context.permissions.canAccess() ? baseTemplate(context) : [], ); const Card = craftComponent( 'Card', {}, (user: Input) => ({ user }), ({ user }) => div(user().name), ).pipe(hasPermission('edit')); ``` `edit` is internal configuration. The caller of `Card` does not provide it. ## Input supplied by the component caller A directive can also add a public input to the component: ```ts const hasPermissionInput = craftDirective( 'hasPermissionInput', {}, (baseLogic: HostRequiredLogic) => (user: Input, permission: Input) => { const context = baseLogic(user); return { ...context, permission, permissions: { canAccess: () => user().permissions.includes(permission()), }, }; }, ( baseTemplate: HostTemplate<{ user: Input; permission: Input; permissions: { canAccess: () => boolean; }; }>, ) => (context) => (context.permissions.canAccess() ? baseTemplate(context) : []), ); const Card = craftComponent( 'Card', {}, (user: Input) => ({ user }), ({ user }) => div(user().name), ).pipe(hasPermissionInput); Card({ user: () => currentUser, permission: () => 'edit', }); ``` The directive adds `permission` to the final logic and to `Card`'s public props. The renderer passes factory arguments in prop order, following the existing convention for functional component factories. ## Structural directive A structural directive decides whether the template produces nodes: ```ts import { HostRequiredLogic, HostTemplate, Input, craftComponent, craftDirective, div, p, } from '@craft-ng/component'; const whenDirective = craftDirective( 'whenDirective', {}, ( baseLogic: HostRequiredLogic<{ when: Input; }>, ) => baseLogic, ( baseTemplate: HostTemplate<{ when: Input; }>, ) => (context) => (context.when() ? baseTemplate(context) : []), ); const Panel = craftComponent( 'Panel', {}, (when: Input) => ({ when }), () => div(p('Conditional content')), ).pipe(whenDirective); Panel({ when: () => isVisible(), }); ``` When `when()` becomes false, the renderer removes the template output. When it becomes true again, the template is rendered again. A structural directive can consume context added by a previous directive: ```ts const onlyEditable = craftDirective( 'onlyEditable', {}, ( baseLogic: HostRequiredLogic<{ permissions: { canEdit: () => boolean; }; }>, ) => baseLogic, ( baseTemplate: HostTemplate<{ permissions: { canEdit: () => boolean; }; }>, ) => (context) => (context.permissions.canEdit() ? baseTemplate(context) : []), ); const EditableCard = craftComponent( 'EditableCard', {}, (user: Input) => ({ user }), ({ user }) => div(user().name), ).pipe(InteractivePermissions, onlyEditable); ``` The context flows from left to right: ```text original logic → InteractivePermissions → { user, permissions } → onlyEditable → template or [] ``` ## Directives on elements A component template can also apply a structural directive to a hyperscript node: ```ts const message = p('Message').pipe(whenDirective); ``` The component context is passed to the decorated template. Craft structural directives can therefore transform Craft output without introducing an intermediate component. Functional DOM directives can also be applied with `.pipe(...)`. Their declared inputs are consumed by the directive instead of becoming DOM attributes: ```ts button({ craftRouterLink: link }).pipe(CraftRouterLink); ``` A field configured with `insertSelectFormTree` must be selected before it is bound, so its lazy insertions (including validators) are registered: ```ts input({ type: 'email' }).pipe( CraftFieldDirective(loginForm.form.selectEmail()), ); ``` ## Composition rules * Create a configurable directive with `craftDirective(...)`, then pass it to `.pipe(...)`. * A directive can add public inputs; they appear in the final component props. * A directive placed after another receives the already decorated logic and template, so it can consume context added by the previous directive. * Generator factories continue to be executed by the Craft runtime. Dependencies from both the original and decorated factories remain part of the component dependency contract. ## See Also * [Customization](/guide/components/customization) * [Encapsulated styles](/guide/components/styles) * [Testing components](/guide/testing/components) --- --- url: https://ng-angular-stack.github.io/craft/guide/components/pending-block.md --- # settledValue & pendingBlock Reading an async value in a template without ever handling `undefined` — and being told at **compile time** when the loading state has nowhere to go. **Use it when** a template renders data that comes from a `query`. **Not when** you want to drive the loading state yourself: `query.value()` (`T | undefined`) and `query.status()` stay exactly as they were. ## Import ```typescript import { settled } from '@craft-ng/core'; import { pendingBlock } from '@craft-ng/component'; ``` ## Overview A resource-like `query`, `mutation` or `asyncProcess` exposes a second read next to `value`: ```typescript users.value(); // User[] | undefined — you handle the wait users.settledValue(); // User[] — the wait is handled for you ``` `settledValue` never returns `undefined` and never returns a value while the source carries an exception. When there is nothing to show it **suspends**: it throws a `CraftNotSettled` that the nearest `pendingBlock` turns into a fallback. A business exception throws through the existing channel instead, and lands in the nearest `catchBlock`. Because the dependency is visible in the types, a template that renders a suspending value with no `pendingBlock` around it does not compile. ## Reading a settled value in a computed Inside a `craftComputed` generator, `yield* settled(ref)` hands back the resource's settled read: ```typescript const teams = craftComputed('teams', function* () { const list = yield* settled(users); // `list()` is `User[]` here — never undefined, never in exception return () => [...new Set(list().map((user) => user.team))].sort(); }); ``` Nothing is awaited and nothing is yielded at runtime: the markers are type-only. What they do is tag `teams` as *depending on the async source `users`*, which is what the template checker reads. ## The boundary The boundary is piped onto any node above the reads: ```typescript div([span(teams), span(total)]).pipe( pendingBlock({ fallback: () => p('Chargement…') }), ); ``` One boundary covers every async source in its subtree — the same shape as `Suspense`. When each zone deserves its own skeleton, name the sources instead; the list is checked exhaustively, so a source with no fallback (and a fallback for a source that never suspends here) is a compile error: ```typescript div([...]).pipe( pendingBlock.exhaustive({ users: () => SkeletonList(), orders: () => SkeletonRows(), }), ); ``` The handler keys are the **query names**, even when the template only ever sees a computed derived from them. ## What the compiler enforces ```typescript craftComponent( 'teamList', {}, function* () { const users = yield* query('users', { ... }); const teams = craftComputed('teams', function* () { const list = yield* settled(users); return () => list().length; }); return { teams }; }, // ERROR_async_source_rendered_outside_a_pendingBlock: "users" ({ teams }) => div([span(teams)]), ); ``` The sources bubble up through the node tree exactly like unhandled exception codes do, and the check fires on the `craftComponent` template argument, naming the sources that have nowhere to show their loading state. Several suspending computeds in one template are all covered by the same rule: every one of them needs a boundary above it. The obligation travels through `each`, `ifBlock`, `defer`, projected content and nested elements — anywhere a node can carry children. ## Stale-while-revalidate A reload that keeps its previous value does **not** suspend: the stale value is served while the new one is in flight, so a refetch never blanks a screen that already has data. Only a source with nothing to show suspends. To make a reload suspend again, clear the value with `preservePreviousValue: () => false`. A refetch throws nothing, so the boundary cannot learn about it from the suspension channel — it watches the source's own status instead. Give a handler its `reloading` slot to report it, rendered **next to the still-visible subtree**: ```typescript pendingBlock.exhaustive({ issue: { pending: () => p('Waiting for an invoice…'), reloading: () => p('Re-issuing…'), }, }); // or, for the catch-all form pendingBlock({ fallback: () => Skeleton(), reloading: () => Spinner() }); ``` ## Runtime behaviour While a source is pending, the boundary renders its fallback and detaches the suspended subtree's DOM — **detaches, not destroys**. Keeping it alive is what makes resumption work: the suspended bindings stay subscribed to their source's status, so they re-run and release the boundary the moment the data arrives. Two escapes are reported rather than silently swallowed: * a settled read that suspends with no boundary above it throws `CraftUnhandledPendingError`; * a settled read whose source carries an exception with no `catchBlock` above it throws `CraftUnhandledExceptionError`. The first is the runtime backstop for what the types cannot see — typically a settled read hidden inside a lambda (`() => users.settledValue().name`), where the brand that carries the obligation is lost. Bind the value **by reference** (`span(users.settledValue)`, `span(teams)`) to keep the compile-time guarantee. ## Two boundaries, two obligations A settled read has two exits and each one has its own boundary: | Exit | Thrown | Boundary | Checked at | | ---- | ------ | -------- | ---------- | | nothing to show yet | `CraftNotSettled` | `pendingBlock` | `craftComponent(...)` | | the source carries an exception | `CraftGenShortCircuit` | `catchBlock` | `craftComponent(...)` | Both bubble up the node tree until a boundary clears them, and both fail the `craftComponent` template argument when uncovered. A `pendingBlock` is not an exception boundary — settled exceptions pass straight through it, and vice versa. These two throws are intentional CraftNG control flow. The shared `isCraftControlFlow(error)` predicate identifies them so observability and error-conversion wrappers can rethrow them without logging or taking an app snapshot. If a pending read escapes its boundary, it becomes `CraftUnhandledPendingError`; that is a real template error and remains observable. ```typescript div([span(summary)]) .pipe(pendingBlock.exhaustive({ issue: () => Skeleton() })) .pipe(catchBlock.exhaustive({ INVOICE_REJECTED: () => Rejected() })); ``` A `catchBlock` handler receives the exception as `AnyCraftException`: its `code` is known, its payload is not. Reach for `matchBlock` when the fallback needs the payload itself. ## Current limits * The by-id forms (`select(...)` / `selectOrCreate(...)`) have no settled read yet: a by-id ref holds one status per group member. * A component cannot yet delegate its boundaries to its caller: both checks are enforced on each `craftComponent` template. * A settled read hidden inside a lambda loses its brand, and with it both compile-time obligations — the runtime backstops still fire. The pending fallback is announced to assistive tech (`aria-live`, `aria-busy`). See [Accessibilité](/guide/components/accessibility). --- --- url: https://ng-angular-stack.github.io/craft/guide/components/accessibility.md --- # Accessibilité Craft force déjà les exceptions exhaustives, `pendingBlock`, et les templates réactifs. L’accessibilité suit le même ADN : **un état illégal ne compile pas, un oubli est une erreur ESLint, le runtime des blocs n’attend pas que l’auteur s’en souvienne.** Cible : **WCAG 2.2 niveau AA**. ## Les cinq couches 1. **Types** — `img` et `area` exigent `alt` (y compris `''` décoratif). Les helpers sémantiques (`dialog`, `fieldset`, `table`, `iframe`, `h4`–`h6`, `svg`…) existent pour que le lint s’applique sans passer par `h()`. 2. **ESLint `craft-ng/a11y`** — nom accessible, labels, ARIA, pas de click sur un `div`, `button` avec `type`, `h()` interdit quand un helper nommé existe. 3. **Runtime des blocs** — `pendingBlock` annonce le fallback (`aria-live`, `aria-busy`), `catchBlock` pose `role="alert"`, `defer` rend le placeholder clavier, `CraftRouterLink` pose `aria-current="page"`. 4. **Primitives** — `heading` / `headingSection` (outline relatif), `dialog` (modale native + focus), `liveRegion` (toasts). Pas de bouton **stylé** : `buttonControl` / `fieldControl` / `disclosureControl` injectent les props d’accessibilité dans vos éléments natifs. 5. **Tests** — `toBeAccessible()` sur le helper de template. ```ts import craftRules from '@craft-ng/dev-tools/eslint-rules'; export default [ { files: ['**/*.ts'], plugins: { 'craft-ng': craftRules }, rules: { ...craftRules.configs.a11y.rules, }, }, ]; ``` Les règles sont en `error` dans le preset. Un disable est un écart documenté, pas le chemin par défaut. ## Templates hyperscript `@angular-eslint/template-accessibility` ne voit que `**/*.html`. Les templates Craft sont du TypeScript. C’est le plugin `craft-ng` qui marche `button(...)`, `img(...)` **et** `h('img', …)`. ```ts img({ src: photo.url, alt: photo.title }); // décoratif : alt: '' button({ type: 'button' }, 'Enregistrer'); a({ href: '/tasks' }, 'Tâches'); label({ htmlFor: 'email' }, 'Email'); input({ id: 'email', type: 'email' }); ``` `h('button')` alors qu’un helper nommé existe est une erreur (`prefer-named-html-helpers`) : c’est le bypass des types. ## Outline de titres Un `h3` dans une Card est un faux positif classique : parfois sous un `h1`, parfois sous un `h2`. Le titre ne choisit pas son rang. **Le parent le fournit.** ```ts heading('Liste des tâches'); headingSection([ heading('Détail'), TaskCard(), // le heading() interne devient hN+1 ]); ``` Le snippet ci-dessus est le cœur de l’API. Le skip-link et `main` appartiennent au shell applicatif : * `heading()` lit le niveau courant (1–6) et rend `h1`…`h6`. * `headingSection(...)` incrémente d’un cran pour le sous-arbre — fragments commentaires, pas de wrapper DOM, comme `ifBlock`. * `headingRoot(...)` repart à `h1` (dialog, reset explicite). Un `dialog` pose aussi sa propre racine d’outline (titre du dialogue = niveau 1 **dans** le dialog). Les SFC `loadComponent` restent sur `heading()`. * `h1()`…`h6()` restent pour le HTML brut. La règle `prefer-relative-heading` les interdit dans un `craftComponent` (hors specs). Un composant réutilisable expose `heading()` sans `headingSection` local : le besoin d’outline **remonte** au parent. Appeler ce composant hors d’un `headingSection` **ne compile pas** (même ADN que `pendingBlock`). Toute SFC montée via `loadComponent` / `loadCraftComponent` appelle `heading()` — pas `headingRoot()`. Le rang (h1 vs h2+) vient du parent : * **Page** (sœur sous le shell) : `heading()` est le h1. * **Layout** (SFC avec `CraftRouterOutlet`) : `heading()` + `headingSection([…, CraftRouterOutlet()])` pour que l’enfant hérite h2+. * **Shell** (`App`) : `skipLink` + `main` + `CraftRouterOutlet`, **sans** `heading()` au-dessus de l’outlet. Sinon deux h1, ou des enfants coincés au même niveau que le titre du chrome. `require-route-heading-outline` lit la cible lazy. `require-outlet-heading-section` distingue layout et shell. Les types ne relient pas l’outlet à l’enfant routé. ```ts // Shell — pas de heading() au-dessus de l’outlet skipLink('main', 'Aller au contenu'); main({ id: 'main', tabIndex: -1 }, CraftRouterOutlet()); // Layout — titre + outlet dans headingSection heading('Équipe'); headingSection([CraftRouterOutlet()]); // Page (loadComponent) — heading() seulement ; h1 ou h2+ selon le parent heading('Liste des tâches'); headingSection([ heading('Détail'), TaskCard(), ]); ``` ## Blocs `pendingBlock` détache la source du document pendant le chargement (les nœuds restent montés, ils ne sont pas `hidden` en CSS). Le fallback est enveloppé dans `aria-live="polite"` `aria-atomic="true"` `aria-busy="true"`. Au reload, la source reste visible ; `aria-busy` signale le rafraîchissement. Le focus dans la source est restauré à la reprise. `catchBlock` enveloppe le message d’erreur dans `role="alert"` si le fallback n’est pas déjà une live region. `defer` pose `aria-busy` pendant le chargement. Un trigger `interaction` sur un placeholder qui n’est pas un contrôle reçoit `role="button"` et `tabIndex="0"`, et ne se déclenche au clavier que sur Entrée / Espace. ## Dialog et live region ```ts dialog( { labelledBy: 'title', open: true, onClose }, [heading({ id: 'title' }, 'Confirmer'), button({ type: 'button', click: onClose }, 'Fermer')], ); liveRegion({ politeness: 'polite' }, copied() ? 'Copié' : ''); ``` `dialog` s’appuie sur `` natif (`showModal`, Escape, `aria-modal`). `liveRegion` est un `` (ou `alert` si `assertive`). ## Helpers de contrôle (props à merger) Les helpers sont renderless : ils fournissent les attributs à merger sur vos éléments HTML, sans imposer de widget visuel. ```ts const email = fieldControl('email'); label(email.label, 'Email'); input({ ...email.input, type: 'email' }); p(email.description, 'We never share your email.'); const faq = disclosureControl('faq-1', isOpen); button({ ...faq.button, click: toggle }, 'What is Craft?'); div(faq.panel, '…'); button(buttonControl({ disabled: isSaving, keepFocusable: true }), 'Save'); ``` Un panneau fermé reçoit `hidden` et `aria-hidden`, pour qu’aucun focus ne reste à l’intérieur. `keepFocusable` pose `aria-disabled` sans `disabled` : le clic n’est pas coupé, l’auteur doit no-op le handler. Les états sont aussi exposés en `data-*`, ce qui permet une convention CSS simple et indépendante du composant : ```css button[data-disabled] { opacity: 0.5; } input[data-invalid] { border-color: var(--danger); } button[data-open] { font-weight: 600; } ``` Une live region doit être montée dès le premier rendu : ne conditionnez jamais son nœud sur le message. Le lecteur d’écran peut ainsi s’y abonner avant qu’un événement survienne. ```ts // correct — region exists at first paint liveRegion({ label: 'Notifications' }, copied() ? 'Copied' : ''); // incorrect — SR never subscribes ifBlock(copied, () => liveRegion('Copied')); ``` ## Navigation `provideCraftRouter` enregistre `CraftTitleStrategy` : le `title` Angular de la route est écrit via `BrowserDocument.setTitle`. `withA11yNavigationFocus()` (opt-in, passé à `provideCraftRouter`) déplace le focus vers `#main` / `
` après chaque navigation interne — pas au premier chargement, le skip-link s’en charge. `skipLink('main', 'Aller au contenu')` en tête du shell, avec `main({ id: 'main', tabIndex: -1 }, …)`. Pour synchroniser la langue et la direction du document depuis un générateur : ```ts yield* BrowserDocument.setLang('fr'); yield* BrowserDocument.setDir('ltr'); ``` `clickFocus` place le focus avant d’exécuter le handler, utile pour les contrôles qui ouvrent une recherche ou un dialogue : ```ts button({ type: 'button', click: clickFocus('#search-warmup', openSearch), }, 'Search'); ``` ## Tests ```ts const { getByRole, getByLabel, toBeAccessible } = await setupCraftComponentTemplateTest( Page, { context }, ); await toBeAccessible(); getByRole('button', { name: 'Save' }); getByLabel('Email'); ``` `assertAccessible` / `toBeAccessible()` couvrent les checks structurels (alt, nom accessible, tabindex, iframe title). Le contraste réel et le reste de WCAG 2.2 AA restent un job axe / AccessLint en CI applicative. ## CSS Les règles `require-focus-visible` et `require-reduced-motion` s’appliquent aux `styles` du `craftComponent` : si vous stylez `button` / `a` / `input`, définissez `:focus-visible` ; si vous animez, gatez avec `prefers-reduced-motion`. Le contraste passe par les tokens (`no-hardcoded-design-values`), pas par une deuxième linter CSS. --- --- url: https://ng-angular-stack.github.io/craft/guide/components/customization.md --- # Customizing components and directives Craft splits customization into three layers, and which one you reach for depends on how far the change should travel: | Layer | Changes | | --------------------- | ------------------------------------- | | Root-element `host` | The component's own root defaults | | Encapsulated `styles` | Its internal appearance | | Composable directives | Behaviour, reusable across components | **Start with `host`** for one component's defaults, and move to a directive only when the same customization needs to apply somewhere else too. ## Customizing the root element The component meta `host` properties define defaults for the component’s root element. The caller can extend or override them: ```ts import { craftComponent, div, h2 } from '@craft-ng/component'; const Card = craftComponent( 'Card', { host: { class: 'card card--default', attrs: { role: 'article' }, }, }, () => ({}), () => div([h2('A card')]), ); Card({ class: 'card--featured', attrs: { 'data-testid': 'featured-card' }, }); ``` Classes, attributes, styles, and events recognized as host properties are applied to the component root. Other properties remain factory props. Values can be reactive: ```ts const { active } = state('active', false, ({ set }) => ({ set })); Card({ class: () => (active() ? 'is-active' : 'is-idle'), style: () => ({ opacity: active() ? 1 : 0.6 }), }); ``` ## Customizing with styles Styles declared in `meta.styles` are shared across instances and encapsulated with `@scope`. The template root is written as `:scope`: ```typescript const Panel = craftComponent( 'Panel', { styles: ` :scope { padding: 1rem; border: 1px solid #ddd; } .title { font-weight: 700; } button { cursor: pointer; } `, }, () => ({}), () => div([h2({ class: 'title' }, 'Panel'), button('Save')]), ); ``` Styles do not leak into descendant components. Global rules such as `@keyframes` and `@font-face` cannot be nested in `@scope`, so their private names must start with the component scope. `@import` and document-root selectors are rejected. `@media`, `@supports`, and `@container` remain composable inside the scope. For the typed styling API, see [Typed CSS variables and design tokens](/guide/components/css-variables). ## Adding reusable customization with a directive A directive transforms a component’s factory and template. It is applied from left to right with `.pipe(...)`: ```ts const Highlight = craftDirective( 'Highlight', { styles: '.highlight { background: #fff3bf; }', }, (baseLogic) => baseLogic, (baseTemplate) => (context) => baseTemplate(context, { class: 'highlight' }), ); const HighlightedPanel = Panel.pipe(Highlight); ``` A directive can also add context and public props: ```ts const WithPermission = craftDirective( 'WithPermission', {}, (baseLogic) => (user: Input) => ({ ...baseLogic(user), canEdit: () => user().permissions.includes('edit'), }), (baseTemplate) => (context) => context.canEdit() ? baseTemplate(context) : [], ); const EditablePanel = Panel.pipe(WithPermission); ``` Directive styles are registered in the scope of the component that owns them. The same directive can therefore be reused by several components without introducing an HTML wrapper. ## Composing providers and exception handlers `withProviders` configures the provider scope of a component before it is invoked. `catchTag.exhaustive` is a logic boundary: each handler is a generator that can call a service or perform another logic operation. It must not return template children. Use `catchBlock.exhaustive` or `matchBlock.exhaustive` when the exception should produce DOM. ```ts import { abstract, craftException, craftService } from '@craft-ng/core'; import { catchTag, craftComponent, p, withProviders, } from '@craft-ng/component'; const noAccess = craftException({ code: 'NO_ACCESS' }); const { RestrictedData, provideRestrictedData } = craftService( { name: 'restrictedData', scope: 'abstract' }, abstract(), ); const MyRestrictedCraftComponent = craftComponent( 'MyRestrictedCraftComponent', {}, function* () { return { value: yield* RestrictedData() }; }, ({ value }) => p(`Private data: ${value}`), ); const Restricted = MyRestrictedCraftComponent.pipe( withProviders([ provideRestrictedData(() => currentUserCanRead() ? 'available' : noAccess, ), ]), catchTag.exhaustive({ NO_ACCESS: function* () { // yield* ToastService.show(() => 'No access'); }, }), ); Restricted(); ``` Providers are evaluated before the component template. If a provider reads a signal, changing that signal recreates the composed rendering, including the provider scope. The handler generator runs for the exception state. Since `catchTag` does not render a template, use `catchBlock` or `matchBlock` for a visual fallback. The component adapter reuses the exhaustive `catchTag` rules from the core and the composed component carries the exception codes produced by its initializer and providers. The providers also participate in the normal Craft DI graph, so they can satisfy dependencies used by the component and its children. The variadic component `.pipe(...)` overload is currently kept permissive to avoid excessive TypeScript instantiation depth; runtime dispatch still rejects an unhandled exception code. ## Choosing an exception utility Craft exposes three complementary utilities. The important distinction is whether the exception is handled in logic or rendered in a template: * `catchTag.exhaustive` handles component initialization exceptions in logic; * `catchBlock.exhaustive` creates a template boundary and can insert a fallback before or after its source block; * `matchBlock.exhaustive` renders a fallback from an exception value or signal. ### `catchTag.exhaustive`: logic only Handlers are generator functions. They can call services and yield other Craft operations, but they cannot return `p(...)`, an element, or any other template children. A DOM fallback belongs to `catchBlock` or `matchBlock`. ```ts const SafeComponent = MyRestrictedCraftComponent.pipe( withProviders([ provideRestrictedData(() => currentUserCanRead() ? 'available' : noAccess, ), ]), catchTag.exhaustive({ NO_ACCESS: function* (exception) { yield* ToastService.show(() => `Access denied: ${exception.code}`); }, }), ); ``` ### `catchBlock.exhaustive`: preserve a source block Apply it to a rendered VNode when the source subtree may throw. The source is kept and the fallback is inserted at the requested position. Applying it to a component in `.pipe(...)` also creates a residual component boundary and removes the handled codes from the component and route contracts. ```ts const view = SourceComponent({}).pipe( catchBlock.exhaustive( { UserNotFoundException: () => p('User not found'), }, { position: 'after' }, ), ); ``` For a template boundary, the source block remains visible by default. When `catchBlock` is piped onto a component and the exception comes from its composed scope, a function handler keeps the existing component behavior and replaces the source. A handler can keep that source visible by using the object form and setting `showSource: true`: ```ts const view = SourceComponent({}).pipe( catchBlock.exhaustive({ UserNotFoundException: { render: () => p('User not found'), showSource: true, position: 'after', }, }), ); ``` With `showSource: true`, the source and fallback are both rendered. Use `showSource: false` to hide the source explicitly. `position` can be set on each handler (`before` or `after`); the second argument remains available as a default for handlers that do not specify their own position. Existing function handlers keep their previous behavior. If the component factory or a provider fails before the template is created, there is no source block to preserve, so the fallback is rendered alone. ### `matchBlock.exhaustive`: render a resource exception Use it when a query, mutation, or another primitive exposes an exception as a signal instead of throwing from the template subtree. The block renders no children while the source is empty and switches reactively to the matching handler when an exception appears. ```ts matchBlock.exhaustive(() => userQuery.exceptions().loader, 'code', { UserNotFoundException: () => p('User not found'), UserConsentMissingException: () => p('Consent is required'), }); ``` ## What Craft handles directly Craft supports compositions that are not native properties of a standard Angular component or directive: * a Craft directive can declare `meta.styles` and contribute to the stylesheet of the component using it; Angular associates styles with a component, not with an `@Directive`; * directive styles remain encapsulated with `@scope`, without rewriting selectors or adding a wrapper; * multiple directives can compose their logic, template, host classes, and styles through `.pipe(...)`; * styles are deduplicated and reference-counted across instances, then removed when the last instance is destroyed. With standard Angular, this usually requires moving styles into a component, manually adding classes to the host, or managing stylesheet injection and cleanup yourself. Craft keeps those responsibilities in the directive runtime. ## Choosing the right level * `host`: identity, attributes, classes, or behavior of the root element; * `styles`: local, reusable component appearance; the stylesheet is shared across instances, while its rules remain limited to the component roots; * `craftDirective`: behavior or customization reusable across components; * the factory: component-specific state and dependencies. ### Understanding style scope Inside `meta.styles`, `:scope` targets every root produced by the template: ```ts import { craftComponent, div, h2, strong, } from '@craft-ng/component'; const Card = craftComponent( 'Card', { styles: ` :scope { padding: 1rem; } .title { color: navy; } .title strong { font-weight: 700; } `, }, () => ({}), () => div([h2({ class: 'title' }, [strong('Card')])]), ); ``` Craft puts an internal token on the roots and generates a scope equivalent to: ```css @scope ([data-craft-root~="Card"]) to ([data-craft-root] *) { /* Card rules */ } ``` In practice: * `:scope` targets the root itself; * `.title` targets `Card` descendants; * when a child Craft component or Angular component is encountered, its host becomes a boundary: parent rules can reach the host, but not its internal DOM; * ordinary elements do not become boundaries and do not receive an additional token; * a template returning multiple roots scopes each root, but cannot express a relationship between sibling roots such as `header + main`; * a root that is directly another Craft component can carry multiple tokens. The containing component can then reach into the child component: this is a known limitation of the current model. Scoping is structural, not based on selector rewriting: modern selectors such as `:is()`, `:where()`, `&`, and nested rules are not transformed by Craft. `@media`, `@supports`, and `@container` remain inside the scope; rules that cannot be nested there, such as `@keyframes`, `@font-face`, `@import`, and `@namespace`, are hoisted outside the `@scope` block. Directive styles use the scope of their owning component because a directive does not introduce a separate root node. A directive can add `.highlight` or modify `:scope`, but `:scope` then refers to the host component’s roots, not to a directive wrapper. Names passed to `craftComponent` and `craftDirective` must be unique and match their declaration names. The dedicated ESLint rules detect missing or inconsistent names. ## See Also * [Encapsulated styles](/guide/components/styles) * [Directives and `.pipe(...)`](/guide/components/directives) * [Content projection](/guide/components/content-projection) --- --- url: >- https://ng-angular-stack.github.io/craft/guide/components/content-projection.md --- # Content projection Projection is a **rendering context, not a category of component**. The same `craftComponent` can be rendered directly or supplied into a compatible logical slot — its definition doesn't change either way. **Use it when** a component composes content it doesn't own: a card with a caller-supplied body, a toolbar filled with actions, a dialog with its buttons. **Not when** the child is fixed — just render it. Both forms go through one primitive: ```ts renderContent(value); ``` It accepts either deferred DOM content (`RenderableContent`) or a component unit exposing a logical contract. There is **no runtime registry** like `contentChildren`, and no special projection component. ## The common case — free DOM content `ContentSlot` describes optional or free-form DOM content. `RequiredContent` adds a structural contract that TypeScript checks. ```typescript import { content, craftComponent, div, renderContent, section, type ContentSlot, type RequiredContent, } from '@craft-ng/component'; type CardInput = { readonly header?: ContentSlot; readonly body: RequiredContent<{ readonly selector: { readonly tag: 'div'; readonly class: 'card-body'; readonly 'data-slot': 'body'; }; }>; }; const Card = craftComponent( 'Card', {}, (input: CardInput) => input, ({ header, body }) => section([ header ? renderContent('header', header) : 'Default title', renderContent('body', body), ]), ); Card({ header: content(() => div('Title supplied by the caller')), body: content(() => div({ class: 'card-body', 'data-slot': 'body' }, 'Card content'), ), }); ``` The selector is analysed **statically**. This is rejected, because it does not contain `div.card-body[data-slot="body"]`: ```ts Card({ // @ts-expect-error the content does not satisfy the slot's DOM contract. body: content(() => div({ class: 'wrong-class' })), }); ``` Content can be built from arrays, conditions, loops and templates — the analysis looks for the selector in every rendered branch: ```ts const body = content(() => [ showIntro() ? div({ class: 'card-body' }, 'Introduction') : undefined, each(rows(), { track: (row) => row.id }, (row) => div({ class: 'card-body' }, row.label), ), renderTemplate(cardRowTemplate, { $implicit: selectedRow() }), ]); Card({ body }); ``` The constraint creates no wrapper and adds no runtime validation. DOM contracts and logical contracts are independent: ```text RequiredContent → the shape of the DOM supplied ProjectionOf → the logical capabilities of a component ``` ## Logical projection by contract A component becomes projectable when its logic factory returns a `contract` property, built and checked with `satisfies`. ```ts import { content, input } from '@craft-ng/component'; import { button, craftComponent, renderContent, type ContentSlot, type ProjectionContractOf, type ProjectionOf, } from '@craft-ng/component'; type ToolbarActionContract = { readonly kind: 'toolbar-action'; readonly trigger: () => void; readonly disabled: () => boolean; }; const ToolbarAction = craftComponent( 'ToolbarAction', {}, (input: { readonly key: string; readonly content: ContentSlot; readonly trigger: () => void; readonly disabled?: () => boolean; }) => ({ key: input.key, contract: { kind: 'toolbar-action', trigger: input.trigger, disabled: input.disabled ?? (() => false), } satisfies ToolbarActionContract, content: input.content, }), ({ contract, content }) => button( { type: 'button', disabled: contract.disabled, click: contract.trigger, }, renderContent(content), ), ); type ExtractedContract = ProjectionContractOf; type ToolbarActionUnit = ProjectionOf; ``` `ProjectionContractOf` extracts the type of `logicOutput.contract`. `ProjectionOf` adds the stable key the renderer expects. For generic consumers, `ProjectionSlot` directly describes a collection of compatible units. Projection therefore depends on **neither** the component's name, **nor** a `projection` metadata field, **nor** a runtime registry. ## Explicit collections, order and stable keys The consuming component receives a typed collection explicitly. Each unit must supply a **stable key**, which `each` uses to reuse, move or remove the right projection. ```ts import { craftComponent, div, each, renderContent, type ProjectionOf, } from '@craft-ng/component'; const Toolbar = craftComponent( 'Toolbar', {}, (input: { readonly actions: readonly ProjectionOf[]; }) => input, ({ actions }) => div( { role: 'toolbar' }, each(actions, { track: (action) => action.key }, (action) => renderContent(action), ), ), ); Toolbar({ actions: [ ToolbarAction({ key: 'save', content: () => 'Save', trigger: save }), ToolbarAction({ key: 'cancel', content: () => 'Cancel', trigger: close }), ], }); ``` The same `ToolbarAction` stays usable on its own: ```ts const Page = craftComponent( 'Page', {}, () => ({}), () => [ ToolbarAction({ key: 'standalone', content: () => 'Direct action', trigger: save, }), Toolbar({ actions: [ ToolbarAction({ key: 'projected', content: () => 'Projected action', trigger: save, }), ], }), ], ); ``` ## Styling projected content `contentStyles` is indexed by the content slot names the component declares. An unknown slot name is a type error. ```ts import { ContentSlot, craftComponent, input, renderContent, } from '@craft-ng/component'; const StyledCard = craftComponent( 'StyledCard', { contentStyles: { body: ':scope { display: block; color: #344054; }', }, }, (input: { readonly body: ContentSlot }) => input, ({ body }) => renderContent('body', body), ); ``` The **caller** decides explicitly whether its content accepts those styles: ```ts StyledCard({ body: content(() => div('Styled content'), { allowContainerStyles: true, }), }); // without the flag, the content renders but stays isolated StyledCard({ body: content(() => div('Rendered without the container styles')), }); ``` Exposed styles apply to ordinary DOM nodes in the fragment. They never cross the boundary of a nested Craft or Angular component: ```ts StyledCard({ body: content( () => [ div('This node can receive contentStyles.body'), NestedCraftComponent({}), // independent style boundary ], { allowContainerStyles: true }, ), }); ``` ## Pitfalls **Forgetting the stable key.** Without it the renderer cannot tell one projected unit from another across updates, and reuse breaks. **Expecting a plain component to satisfy a contract slot.** It stays perfectly usable as a direct child, but the slot rejects it: ```ts const PlainCard = craftComponent( 'PlainCard', {}, () => ({}), () => 'Card with no contract', ); Toolbar({ actions: [ // @ts-expect-error PlainCard does not expose ToolbarActionContract. PlainCard({}), ], }); ``` An incomplete contract is rejected where it is declared: ```ts const invalidContract = { kind: 'toolbar-action', // @ts-expect-error trigger and disabled are required. } satisfies ToolbarActionContract; ``` **Styling a slot that isn't one.** `contentStyles` can only reference declared content slots: ```ts import { ContentSlot, craftComponent, footer, input, renderContent, } from '@craft-ng/component'; craftComponent( 'InvalidStyles', { // @ts-expect-error "footer" is not a declared content slot. contentStyles: { footer: ':scope { color: red; }' }, }, (input: { readonly body: ContentSlot }) => input, ({ body }) => renderContent('body', body), ); ``` ::: details Combining optional content and contractual actions — a dialog A component can mix optional DOM content with several logical slots in one explicit collection: ```ts const Dialog = craftComponent( 'Dialog', {}, (input: { readonly body?: ContentSlot; readonly actions: readonly ProjectionOf[]; }) => input, ({ body, actions }) => section({ role: 'dialog' }, [ body ? renderContent(body) : [], footer( each(actions, { track: (action) => action.key }, (action) => renderContent(action), ), ), ]), ); Dialog({ body: content(() => div(['Delete the account', 'This action cannot be undone.']), ), actions: [ ToolbarAction({ key: 'cancel', content: () => 'Cancel', trigger: closeDialog }), ToolbarAction({ key: 'delete', content: () => 'Delete', trigger: deleteAccount }), ], }); ``` `closeDialog` and `deleteAccount` are captured by the caller's closures. Projection preserves the lexical context **and the injector** of wherever the unit or the content was declared. ::: ::: details Conditions, reactivity and cleanup Projections are ordinary Craft nodes, so they can sit inside conditions and templates while keeping their identity by key within a collection. Here `visible` is a callable reactive value supplied by the caller: ```ts const OptionalToolbar = craftComponent( 'OptionalToolbar', {}, (input: { readonly visible: () => boolean; readonly actions: readonly ProjectionOf[]; }) => input, ({ visible, actions }) => visible() ? each(actions, { track: (action) => action.key }, (action) => renderContent(action), ) : [], ); ``` On update the renderer adds, removes and moves projections by key. On teardown the projected content, its effects and its styles are cleaned up with the rest of the tree. ::: ## API summary * `content(renderer, options?)` — create deferred DOM content * `renderContent(value)` and `renderContent(slotName, value)` — render it * `RenderableContent`, `ContentSlot` — free-form slots * `RequiredContent` — static DOM contracts * `ProjectionContractOf` — extract a logical contract * `ProjectionOf`, `ProjectionSlot` — type projectable collections The older fragment and slot primitives are no longer part of the public API. ## See Also * [Customization](/guide/components/customization) * [Encapsulated styles](/guide/components/styles) * [Directives and `.pipe(...)`](/guide/components/directives) --- --- url: https://ng-angular-stack.github.io/craft/guide/components/styles.md --- # Encapsulated styles Styles declared in `craftComponent(name, meta, factory, template)` are shared by every instance of the component and encapsulated with CSS `@scope`. The registry keeps a single sheet per component and removes it when the last instance is destroyed. **Use it for** a component's own appearance. **Not for** application-wide styles — those belong in your global stylesheet; scoping them here just makes them harder to find. ## The common case ```ts const Card = craftComponent( 'Card', { styles: ':scope { padding: 1rem } .title { font-weight: 700 }' }, () => ({}), () => div([h2({ class: 'title' }, 'Title')]), ); ``` The template root is written `:scope`. Craft adds **no host element and no wrapper** — roots carry an internal `data-craft-root` attribute, which you must never set yourself. ## Composing styles from a directive A directive's styles compose with the component's: ```ts const Highlight = craftDirective( 'Highlight', { styles: '.highlight { background: yellow }' }, (baseLogic) => baseLogic, (baseTemplate) => (context) => baseTemplate(context, { class: 'highlight' }), ); ``` ## Pitfalls **`@scope` adds no specificity.** Adopted sheets are ordered after the document's sheets, and the `