Quick Start
Render a JSON spec as a live Angular component tree in 5 minutes.
Angular 20+ project with @threadplane/render installed. See the Installation guide if you need setup help.
Let's start with a simple Angular component for the spec to render. Every rendered component receives inputs matching the AngularComponentInputs interface -- your custom props are spread alongside the standard inputs.
// text.component.ts
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
import type { Spec } from '@json-render/core';
@Component({
selector: 'app-text',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>{{ label() }}</p>`,
})
export class TextComponent {
readonly label = input<string>('');
readonly childKeys = input<string[]>([]);
readonly spec = input.required<Spec>();
}Map element type names to Angular component classes using defineAngularRegistry().
// ui-registry.ts
import { defineAngularRegistry } from '@threadplane/render';
import { TextComponent } from './text.component';
export const uiRegistry = defineAngularRegistry({
Text: TextComponent,
});Now let's describe the UI tree itself. A spec is a flat map of elements, and the root key points to the entry element.
// app.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { RenderSpecComponent } from '@threadplane/render';
import type { Spec } from '@json-render/core';
import { uiRegistry } from './ui-registry';
@Component({
selector: 'app-root',
standalone: true,
imports: [RenderSpecComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<render-spec [spec]="spec" [registry]="registry" />
`,
})
export class AppComponent {
registry = uiRegistry;
spec: Spec = {
root: 'greeting',
elements: {
greeting: {
type: 'Text',
props: { label: 'Hello from @threadplane/render!' },
},
},
};
}ng serveOpen http://localhost:4200. You should see "Hello from @threadplane/render!" rendered by your TextComponent.
Adding Reactive State
Specs get a lot more interesting once you connect them to a state store. Props with $state expressions read from the store reactively.
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { RenderSpecComponent, signalStateStore } from '@threadplane/render';
import type { Spec } from '@json-render/core';
import { uiRegistry } from './ui-registry';
@Component({
selector: 'app-root',
standalone: true,
imports: [RenderSpecComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<render-spec [spec]="spec" [registry]="registry" [store]="store" />
<button (click)="updateName()">Change Name</button>
`,
})
export class AppComponent {
registry = uiRegistry;
store = signalStateStore({ name: 'World' });
spec: Spec = {
root: 'greeting',
elements: {
greeting: {
type: 'Text',
props: { label: { $state: '/name' } },
},
},
};
updateName() {
this.store.set('/name', 'Angular');
}
}Clicking the button updates the store, which reactively updates the rendered TextComponent label from "World" to "Angular".
Using Spec-Embedded State
You can also embed initial state right in the spec. When you don't pass an external store, RenderSpecComponent creates an internal signalStateStore from spec.state for you:
spec: Spec = {
root: 'greeting',
elements: {
greeting: {
type: 'Text',
props: { label: { $state: '/message' } },
},
},
state: {
message: 'State embedded in the spec!',
},
};<!-- No [store] input needed -- an internal store is created from spec.state -->
<render-spec [spec]="spec" [registry]="registry" />