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' } },
      },
    },
  };
}