DocsRenderAPI ReferenceRenderSpecComponent

RenderSpecComponent

The top-level entry point for rendering a @json-render/core spec as an Angular component tree.

Import

import { RenderSpecComponent } from '@ngaf/render';

Selector

render-spec

Usage

<render-spec [spec]="spec" [registry]="registry" [store]="store" />
@Component({
  imports: [RenderSpecComponent],
  template: `<render-spec [spec]="spec" [registry]="registry" />`,
})
export class MyComponent {
  spec: Spec = { /* ... */ };
  registry = defineAngularRegistry({ /* ... */ });
}

Inputs

InputTypeDefaultDescription
specSpec | nullnullThe json-render spec to render. When null, nothing is rendered.
registryAngularRegistry | undefinedundefinedComponent registry mapping element types to Angular components.
storeStateStore | undefinedundefinedState store for reactive prop resolution.
functionsRecord<string, ComputedFunction> | undefinedundefinedComputed functions for $fn prop expressions.
handlersRecord<string, (params: Record<string, unknown>) => unknown | Promise<unknown>> | undefinedundefinedEvent handlers invoked when components call emit().
loadingbooleanfalseWhether the spec is currently streaming. Passed to all rendered components as the loading input.

Resolution Chain

For registry, store, functions, and handlers, the component resolves values using this priority:

1
Input (highest priority)

Values passed as component inputs.

2
RENDER_CONFIG (from provideRender)

Global defaults provided via provideRender().

3
Internal fallback (lowest priority)

For store: an internal signalStateStore() is created from spec.state (or an empty object). For registry: an empty registry is used (no components resolve).

This means you can set defaults globally and override them per-instance:

// Global config
provideRender({
  registry: defaultRegistry,
  store: globalStore,
  handlers: { log: (p) => console.log(p) },
});
 
// Per-instance override -- only registry is overridden
<render-spec [spec]="spec" [registry]="customRegistry" />
// store, functions, and handlers fall back to global config

RENDER_CONTEXT

RenderSpecComponent provides a RENDER_CONTEXT injection token to its children via viewProviders. This context is consumed by RenderElementComponent instances and contains:

interface RenderContext {
  registry: AngularRegistry;
  store: StateStore;
  functions?: Record<string, ComputedFunction>;
  handlers?: Record<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>;
  loading?: boolean;
}

The context is a computed signal that updates when any input or config changes. Child components can inject it directly:

import { inject } from '@angular/core';
import { RENDER_CONTEXT } from '@ngaf/render';
 
const ctx = inject(RENDER_CONTEXT);
ctx.store.get('/some/path');

Template Behavior

The component renders a single <render-element> for the root element key from spec.root:

<!-- Internal template -->
@if (spec()?.root; as rootKey) {
  <render-element [elementKey]="rootKey" [spec]="spec()!" />
}

When spec is null or has no root, nothing is rendered.

Internal Store Behavior

When no store is provided (neither as input nor via RENDER_CONFIG), the component lazily creates an internal signalStateStore() from spec.state. This internal store is created once and reused across spec changes -- it is not recreated when the spec input updates.

// Spec with embedded state -- no external store needed
const spec: Spec = {
  root: 'root',
  elements: {
    root: { type: 'Text', props: { label: { $state: '/message' } } },
  },
  state: { message: 'Hello' },
};
<!-- Internal store is created from spec.state -->
<render-spec [spec]="spec" [registry]="registry" />

Change Detection

The component uses ChangeDetectionStrategy.OnPush. All reactive updates flow through Angular Signals, ensuring efficient change detection without zone-based triggers.

Complete Example

import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import {
  RenderSpecComponent,
  defineAngularRegistry,
  signalStateStore,
} from '@ngaf/render';
import type { Spec } from '@json-render/core';
import { TextComponent } from './text.component';
import { ButtonComponent } from './button.component';
 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RenderSpecComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <render-spec
      [spec]="spec"
      [registry]="registry"
      [store]="store"
      [handlers]="handlers"
      [loading]="isLoading()"
    />
  `,
})
export class AppComponent {
  isLoading = signal(false);
 
  registry = defineAngularRegistry({
    Text: TextComponent,
    Button: ButtonComponent,
  });
 
  store = signalStateStore({ count: 0 });
 
  handlers = {
    increment: () => {
      const count = this.store.get('/count') as number;
      this.store.set('/count', count + 1);
    },
  };
 
  spec: Spec = {
    root: 'app',
    elements: {
      app: {
        type: 'Text',
        props: { label: { $state: '/count' } },
      },
    },
  };
}