Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 114 additions & 153 deletions apps/website/content/docs/render/api/provide-render.mdx
Original file line number Diff line number Diff line change
@@ -1,220 +1,181 @@
---
description: Register the registry, store, computed functions, and handlers that every render surface in an application uses by default.
---

# provideRender()

Registers global default configuration for `@threadplane/render` via Angular's dependency injection system.
`provideRender()` registers the defaults that every `<render-spec>` in an application falls back to: a component registry, a state store, the named functions a spec may call through `$computed`, and the handlers its actions dispatch to. It returns `EnvironmentProviders`, so it belongs in `ApplicationConfig.providers` or in a `bootstrapApplication()` call. The running example is the computed-functions demo, whose four functions are registered exactly this way, and this page walks the files that make it work.

## Import
## What the demo does

```typescript
import { provideRender, RENDER_CONFIG } from '@threadplane/render';
```
The Run tab shows a split view. On the left is a live render surface; on the right is the spec JSON that feeds it, arriving character by character. Nothing streams until you press play in the transport bar at the bottom, which also lets you scrub the timeline and change the speed.

## Signature
Press play and the first spec streams in: `hello world` comes out uppercased and `streaming` comes out reversed. Switch to Data Display and an ISO timestamp comes out as a local date while `7 x 6` comes out as `42`. None of those results are in the spec. The spec asks for a function by name, and the four functions registered in `provideRender()` produce the text. The Spec tabs in the header hold three specs -- Text Transforms, Data Display, and Mixed Functions -- and each tab restarts the stream with a different mix of the same four functions.

```typescript
function provideRender(config: RenderConfig): EnvironmentProviders;
```
## How it is built

### Parameters
Four pieces carry the feature: an application config that registers the functions, a registry and store held by the demo component, the `<render-spec>` element that mounts the surface, and a view component that receives the finished value. Open the Code tab to read them in place.

| Parameter | Type | Description |
|-----------|------|-------------|
| `config` | `RenderConfig` | Configuration object with default registry, store, functions, and handlers |
### Registering the computed functions

### Returns
The whole application config is one call. `provideRender()` receives a `functions` map, and every key in it becomes a name a spec may call.

`EnvironmentProviders` -- suitable for use in `ApplicationConfig.providers` or `bootstrapApplication()`.
<ExampleCode file="app.config.ts" title="app.config.ts" />

## RenderConfig
Each function takes a single `args` object and returns a value, which is the `ComputedFunction` contract from `@json-render/core`. The argument names are yours: `formatDate` and `uppercase` and `reverse` read `args['value']`, and `multiply` reads `args['a']` and `args['b']`, so a spec that calls `multiply` has to supply those two keys.

<Callout type="info" title="A computed function runs on every resolution pass">
Prop resolution runs whenever the spec or the bound state changes, which during a stream is many times a second. Keep these functions pure and cheap, and memoize anything expensive outside the function body.
</Callout>

A spec calls one of them with a `$computed` expression in place of a literal prop value. The demo's first spec asks for two of the four:

```typescript
interface RenderConfig {
registry?: AngularRegistry;
store?: StateStore;
functions?: Record<string, ComputedFunction>;
handlers?: Record<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>;
}
const upper = {
type: 'Value',
props: {
label: 'Uppercase',
value: { $computed: 'uppercase', args: { value: 'hello world' } },
},
};

const reversed = {
type: 'Value',
props: {
label: 'Reversed',
value: { $computed: 'reverse', args: { value: 'streaming' } },
},
};
```

| Property | Type | Description |
|----------|------|-------------|
| `registry` | `AngularRegistry` | Default component registry for all `<render-spec>` instances |
| `store` | `StateStore` | Default state store for all `<render-spec>` instances |
| `functions` | `Record<string, ComputedFunction>` | Default computed functions for `$computed` prop expressions |
| `handlers` | `Record<string, Handler>` | Default event handlers for action dispatch |
The `args` values are themselves prop expressions, so an argument may be a literal, as it is here, or a `$state` reference that reads from the store.

All properties are optional. Only provide the defaults you need.
### The registry and store the surface renders with

## RENDER_CONFIG Token
This example puts its functions in the global config and keeps the other two pieces local. The component builds a registry of three inline view components with `defineAngularRegistry()` and an empty store with `signalStateStore()`.

The configuration is stored in the `RENDER_CONFIG` injection token:
<ExampleCode file="computed-functions.component.ts" region="registry-and-store" title="computed-functions.component.ts -- registry and store" />

```typescript
import { InjectionToken } from '@angular/core';
Both are equally valid in `provideRender()`. Keeping them on the component is what you do when one surface needs its own component set, which is the case here because the three view components exist only for this demo.

const RENDER_CONFIG = new InjectionToken<RenderConfig>('RENDER_CONFIG');
```
### Mounting the surface

You can inject it directly if needed:
`<render-spec>` takes the spec and, because this example did not put them in the global config, the registry and store as inputs. The `loading` input tells the registered components that the spec is still arriving, which is how the skeleton rows appear ahead of the real values.

```typescript
import { inject } from '@angular/core';
import { RENDER_CONFIG } from '@threadplane/render';
<ExampleCode file="computed-functions.component.ts" region="live-output" title="computed-functions.component.ts -- the render surface" />

const config = inject(RENDER_CONFIG, { optional: true });
// null if provideRender() was not called
```
No `functions` input appears here, so the component falls back to the map registered in `provideRender()`.

## Usage
### What a view component receives

### Basic Setup
A view component never sees a `$computed` expression. It declares plain inputs and receives resolved values.

```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRender, defineAngularRegistry } from '@threadplane/render';
import { TextComponent } from './components/text.component';
import { CardComponent } from './components/card.component';

export const appConfig: ApplicationConfig = {
providers: [
provideRender({
registry: defineAngularRegistry({
Text: TextComponent,
Card: CardComponent,
}),
}),
],
};
```
<ExampleCode file="computed-functions.component.ts" region="value-inputs" title="computed-functions.component.ts -- the Value view" />

### With Store and Handlers
`label` and `value` are the props the spec sets; `childKeys`, `spec`, `bindings`, `emit`, and `loading` are supplied by the render engine to every registered component. `value` is typed as `unknown` and coerced for display because mid-stream a `$computed` expression can still be half-parsed, and the demo prefers a skeleton row over rendering a partial object.

```typescript
import {
provideRender,
defineAngularRegistry,
signalStateStore,
} from '@threadplane/render';

const globalStore = signalStateStore({ theme: 'light' });

export const appConfig: ApplicationConfig = {
providers: [
provideRender({
registry: defineAngularRegistry({
Text: TextComponent,
Card: CardComponent,
}),
store: globalStore,
functions: {
uppercase: (args: Record<string, unknown>) =>
String(args['text']).toUpperCase(),
},
handlers: {
toggleTheme: () => {
const current = globalStore.get('/theme');
globalStore.set('/theme', current === 'light' ? 'dark' : 'light');
},
},
}),
],
};
```
### The graph that ships with the example

### Registry Only
The example also carries a LangGraph graph. It plays no part in the render pipeline: nothing in the Angular application connects to it, and the spec that streams on the left comes from a local simulator, not from an agent. The graph is a single node that answers questions about computed functions, and it is included here because the Code tab shows it.

If you only need a global registry and want to provide stores per-instance:
<ExampleCode file="graph.py" title="graph.py" />

## Import

```typescript
provideRender({
registry: defineAngularRegistry({
Text: TextComponent,
Card: CardComponent,
Button: ButtonComponent,
}),
})
import { provideRender, RENDER_CONFIG } from '@threadplane/render';
```

## Resolution Priority

`RenderSpecComponent` resolves each configuration value using this priority:

| Priority | Source | Description |
|----------|--------|-------------|
| 1 (highest) | Component input | `[registry]`, `[store]`, `[functions]`, `[handlers]` on `<render-spec>` |
| 2 | `RENDER_CONFIG` | Global defaults from `provideRender()` |
| 3 (lowest) | Internal fallback | Empty registry, internal `signalStateStore()` from `spec.state` |

This means inputs always win over global config:
## Signature

```typescript
// Global config
provideRender({ registry: registryA, store: storeA });

// In template -- registryB overrides registryA, but storeA is still used
<render-spec [spec]="spec" [registry]="registryB" />
function provideRender(config: RenderConfig): EnvironmentProviders;
```

## Global vs Component-Level Config
The returned providers carry the `RENDER_CONFIG` value and the internal lifecycle service that coordinates mount and unmount events across dynamically rendered components. Call `provideRender()` once per application.

<Tabs>
<Tab label="Global config">

Use `provideRender()` when you want shared defaults across your entire application:
## RenderConfig

```typescript
// All <render-spec> instances use this registry by default
provideRender({ registry: myRegistry })
interface RenderConfig {
telemetry?: boolean;
registry?: AngularRegistry;
store?: StateStore;
functions?: Record<string, ComputedFunction>;
handlers?: Record<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>;
}
```

This is ideal when you have a single component library that all specs should use.
| Property | Type | Description |
|----------|------|-------------|
| `telemetry` | `boolean` | Set false to disable automatic development browser collection for this render tree |
| `registry` | `AngularRegistry` | Default component registry for all `<render-spec>` instances |
| `store` | `StateStore` | Default state store for all `<render-spec>` instances |
| `functions` | `Record<string, ComputedFunction>` | Named functions a spec may call through `$computed` |
| `handlers` | `Record<string, (params: Record<string, unknown>) => unknown \| Promise<unknown>>` | Named action handlers invoked when interactive elements fire |

Every property is optional. Provide only the defaults you need, as the example does with `functions` alone.

</Tab>
<Tab label="Component-level config">
## The RENDER_CONFIG token

Pass inputs directly to `<render-spec>` when different parts of your app need different configurations:
The configuration object is stored under the `RENDER_CONFIG` injection token, which is exported alongside `provideRender()`. Inject it when you need to read the defaults yourself:

```typescript
// Dashboard uses one registry
<render-spec [spec]="dashboardSpec" [registry]="dashboardRegistry" />
import { inject } from '@angular/core';
import { RENDER_CONFIG } from '@threadplane/render';

// Form builder uses a different registry
<render-spec [spec]="formSpec" [registry]="formRegistry" />
const config = inject(RENDER_CONFIG, { optional: true });
// null when provideRender() was not called
```

This is useful when you have multiple rendering contexts with different component sets.
## Resolution priority

`RenderSpecComponent` resolves each value independently, and an input always wins over the global default.

</Tab>
<Tab label="Combined">
| Value | Priority |
|-------|----------|
| `registry` | `[registry]` input, then `RENDER_CONFIG`, then the `VIEW_REGISTRY` token from `provideViews()`, then an empty registry |
| `store` | `[store]` input, then `RENDER_CONFIG`, then an internal `signalStateStore()` seeded from `spec.state` |
| `functions` | `[functions]` input, then `RENDER_CONFIG` |
| `handlers` | `[handlers]` input, then `RENDER_CONFIG` |

Use both for a layered approach -- global defaults with per-instance overrides:
Because the fallbacks are per value, a surface may take one piece from the global config and override another. The example does exactly that in reverse: it registers `functions` globally and passes `registry` and `store` as inputs.

```typescript
// Global: shared registry and handlers
// Global: one registry and one handler map for the whole application
provideRender({
registry: baseRegistry,
handlers: { log: (p) => console.log(p) },
handlers: { log: (params: Record<string, unknown>) => console.log(params) },
});
```

// Component: override store, keep global registry and handlers
```html
<!-- This surface keeps the global registry and handlers, but binds its own store -->
<render-spec [spec]="spec" [store]="localStore" />
```

</Tab>
</Tabs>
## Calling provideRender is optional

## Optional Provider

`provideRender()` is not required. If you skip it, `RenderSpecComponent` requires you to pass `registry` and `store` as inputs (or relies on the internal store fallback).
`provideRender()` is not required. Skip it and `<render-spec>` still renders, as long as the registry reaches it another way -- an input, or the `VIEW_REGISTRY` token -- and the internal store fallback covers state.

```html
<!-- Works without provideRender() -- all config via inputs -->
<!-- Works without provideRender() -- every value arrives as an input -->
<render-spec [spec]="spec" [registry]="registry" [store]="store" />
```

## Related

- [RenderSpecComponent API](/docs/render/api/render-spec-component) -- how the component uses `RENDER_CONFIG`
- [defineAngularRegistry()](/docs/render/api/define-angular-registry) -- creating registries to pass to config
- [signalStateStore()](/docs/render/api/signal-state-store) -- creating stores to pass to config
- [Installation](/docs/render/getting-started/installation) -- initial setup with `provideRender()`
## What's Next

<CardGroup cols={2}>
<Card title="RenderSpecComponent" href="/docs/render/api/render-spec-component">
The inputs and outputs of the element that mounts a spec.
</Card>
<Card title="defineAngularRegistry()" href="/docs/render/api/define-angular-registry">
Build the registry you pass to the config or to an input.
</Card>
<Card title="signalStateStore()" href="/docs/render/api/signal-state-store">
Create the signal-backed store that `$bindState` paths read and write.
</Card>
<Card title="Specs and elements" href="/docs/render/guides/specs">
The spec shape that `$computed` expressions live in.
</Card>
</CardGroup>
Loading
Loading