@ng-json-render/core 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @ng-json-render/core
2
+
3
+ Angular renderer for [json-render](https://json-render.dev) — safely render
4
+ AI-agent-generated and server-driven UIs from a JSON spec into **native Angular
5
+ components**. No iframes, no `eval`.
6
+
7
+ Built on the framework-agnostic `@json-render/core` (catalog, spec, streaming,
8
+ expression evaluation), this package adds the Angular rendering layer: signals,
9
+ standalone components, dynamic instantiation, and DI-based events.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm i @ng-json-render/core @ng-json-render/primitives @json-render/core
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { Component, signal } from '@angular/core';
21
+ import { JrRenderer, type Spec } from '@ng-json-render/core';
22
+ import { primitivesRegistry } from '@ng-json-render/primitives';
23
+
24
+ @Component({
25
+ selector: 'app-root',
26
+ imports: [JrRenderer],
27
+ template: `<jr-renderer [spec]="spec()" [registry]="registry" (action)="onAction($event)" />`,
28
+ })
29
+ export class App {
30
+ registry = primitivesRegistry;
31
+ spec = signal<Spec>({
32
+ root: 'root',
33
+ elements: {
34
+ root: { type: 'Stack', props: { gap: 16 }, children: ['title', 'save'] },
35
+ title: { type: 'Text', props: { value: 'Hello', weight: 'bold' } },
36
+ save: { type: 'Button', props: { label: 'Save' }, on: { press: { action: 'save' } } },
37
+ },
38
+ });
39
+ onAction(e) { console.log(e.action, e.nodeId); }
40
+ }
41
+ ```
42
+
43
+ ## Authoring components
44
+
45
+ A catalog component is a standard standalone component. Props map to discrete
46
+ `input()`s, children go through `<ng-content>`, and events are raised via the
47
+ injected `JR_CONTEXT`:
48
+
49
+ ```ts
50
+ import { Component, inject, input } from '@angular/core';
51
+ import { JR_CONTEXT } from '@ng-json-render/core';
52
+
53
+ @Component({
54
+ selector: 'jr-button',
55
+ template: `<button (click)="ctx.emit('press')">{{ label() }}<ng-content /></button>`,
56
+ })
57
+ export class MyButton {
58
+ label = input('');
59
+ protected ctx = inject(JR_CONTEXT);
60
+ }
61
+ ```
62
+
63
+ Register it with `defineRegistry({ Button: MyButton })` and pass to `<jr-renderer>`
64
+ or `provideJsonRender({ registry })`.
65
+
66
+ ## Data binding
67
+
68
+ Props support the full `@json-render/core` expression language — `$state`
69
+ paths, `$cond/$then/$else`, `$template`, and directives — resolved against the
70
+ spec's `state`. Provide/override state via the `[state]` input.
71
+
72
+ ## Testing
73
+
74
+ `@ng-json-render/core/testing` exports a `renderSpec()` harness for TestBed:
75
+
76
+ ```ts
77
+ import { renderSpec } from '@ng-json-render/core/testing';
78
+
79
+ const r = renderSpec(spec, { registry });
80
+ expect(r.query('button')?.textContent).toContain('Save');
81
+ ```
82
+
83
+ ## Status
84
+
85
+ M1 (dynamic renderer + primitives + static/bound specs) is implemented.
86
+ Streaming, two-way form binding, and a broader catalog are on the roadmap.
@@ -0,0 +1,45 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { provideJsonRender, JrRenderer } from '@ng-json-render/core';
3
+
4
+ /**
5
+ * Mount a {@link Spec} in a TestBed and return helpers for asserting the output.
6
+ *
7
+ * ```ts
8
+ * const r = renderSpec(spec, { registry });
9
+ * expect(r.query('button')?.textContent).toContain('Save');
10
+ * ```
11
+ */
12
+ function renderSpec(spec, options) {
13
+ TestBed.configureTestingModule({
14
+ providers: options.actions
15
+ ? [provideJsonRender({ actions: options.actions })]
16
+ : [],
17
+ });
18
+ const fixture = TestBed.createComponent(JrRenderer);
19
+ const actions = [];
20
+ fixture.componentInstance.action.subscribe((e) => actions.push(e));
21
+ fixture.componentRef.setInput('registry', options.registry);
22
+ if (options.state)
23
+ fixture.componentRef.setInput('state', options.state);
24
+ fixture.componentRef.setInput('spec', spec);
25
+ fixture.detectChanges();
26
+ const host = fixture.nativeElement;
27
+ return {
28
+ fixture,
29
+ host,
30
+ actions,
31
+ setSpec(next) {
32
+ fixture.componentRef.setInput('spec', next);
33
+ fixture.detectChanges();
34
+ },
35
+ query: (selector) => host.querySelector(selector),
36
+ queryAll: (selector) => Array.from(host.querySelectorAll(selector)),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Generated bundle index. Do not edit.
42
+ */
43
+
44
+ export { renderSpec };
45
+ //# sourceMappingURL=ng-json-render-core-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-json-render-core-testing.mjs","sources":["../../../../libs/core/testing/src/index.ts","../../../../libs/core/testing/src/ng-json-render-core-testing.ts"],"sourcesContent":["import { ComponentFixture, TestBed } from '@angular/core/testing';\nimport {\n type JrActionEvent,\n type JrActionHandler,\n type JrRegistry,\n JrRenderer,\n type Spec,\n provideJsonRender,\n} from '@ng-json-render/core';\n\n/** Options for {@link renderSpec}. */\nexport interface RenderSpecOptions {\n /** Registry mapping component types to Angular classes. */\n registry: JrRegistry;\n /** Optional action handlers, provided via `provideJsonRender`. */\n actions?: Record<string, JrActionHandler>;\n /** Optional initial state. */\n state?: Record<string, unknown>;\n}\n\n/** Handle returned by {@link renderSpec}. */\nexport interface RenderSpecResult {\n readonly fixture: ComponentFixture<JrRenderer>;\n /** Root DOM element of the mounted renderer. */\n readonly host: HTMLElement;\n /** Actions captured from the renderer's `(action)` output, in order. */\n readonly actions: readonly JrActionEvent[];\n /** Replace the spec and run change detection. */\n setSpec(next: Spec): void;\n /** Query the rendered DOM. */\n query(selector: string): HTMLElement | null;\n queryAll(selector: string): HTMLElement[];\n}\n\n/**\n * Mount a {@link Spec} in a TestBed and return helpers for asserting the output.\n *\n * ```ts\n * const r = renderSpec(spec, { registry });\n * expect(r.query('button')?.textContent).toContain('Save');\n * ```\n */\nexport function renderSpec(\n spec: Spec,\n options: RenderSpecOptions,\n): RenderSpecResult {\n TestBed.configureTestingModule({\n providers: options.actions\n ? [provideJsonRender({ actions: options.actions })]\n : [],\n });\n\n const fixture = TestBed.createComponent(JrRenderer);\n const actions: JrActionEvent[] = [];\n fixture.componentInstance.action.subscribe((e) => actions.push(e));\n\n fixture.componentRef.setInput('registry', options.registry);\n if (options.state) fixture.componentRef.setInput('state', options.state);\n fixture.componentRef.setInput('spec', spec);\n fixture.detectChanges();\n\n const host = fixture.nativeElement as HTMLElement;\n return {\n fixture,\n host,\n actions,\n setSpec(next: Spec) {\n fixture.componentRef.setInput('spec', next);\n fixture.detectChanges();\n },\n query: (selector) => host.querySelector<HTMLElement>(selector),\n queryAll: (selector) =>\n Array.from(host.querySelectorAll<HTMLElement>(selector)),\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAkCA;;;;;;;AAOG;AACG,SAAU,UAAU,CACxB,IAAU,EACV,OAA0B,EAAA;IAE1B,OAAO,CAAC,sBAAsB,CAAC;QAC7B,SAAS,EAAE,OAAO,CAAC;AACjB,cAAE,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;AAClD,cAAE,EAAE;AACP,KAAA,CAAC;IAEF,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC;IACnD,MAAM,OAAO,GAAoB,EAAE;AACnC,IAAA,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAElE,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC;IAC3D,IAAI,OAAO,CAAC,KAAK;QAAE,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC;IACxE,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAC3C,OAAO,CAAC,aAAa,EAAE;AAEvB,IAAA,MAAM,IAAI,GAAG,OAAO,CAAC,aAA4B;IACjD,OAAO;QACL,OAAO;QACP,IAAI;QACJ,OAAO;AACP,QAAA,OAAO,CAAC,IAAU,EAAA;YAChB,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;YAC3C,OAAO,CAAC,aAAa,EAAE;QACzB,CAAC;QACD,KAAK,EAAE,CAAC,QAAQ,KAAK,IAAI,CAAC,aAAa,CAAc,QAAQ,CAAC;AAC9D,QAAA,QAAQ,EAAE,CAAC,QAAQ,KACjB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAc,QAAQ,CAAC,CAAC;KAC3D;AACH;;AC1EA;;AAEG;;;;"}
@@ -0,0 +1,331 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, makeEnvironmentProviders, signal, Injectable, input, output, viewChild, ViewContainerRef, EnvironmentInjector, Injector, effect, untracked, reflectComponentType, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { createStateStore, resolveElementProps, resolveBindings, evaluateVisibility } from '@json-render/core';
4
+ export { compileSpecStream, createSpecStreamCompiler, defineCatalog, defineSchema, validateSpec } from '@json-render/core';
5
+
6
+ /** Injection token providing the per-node {@link JrContext}. */
7
+ const JR_CONTEXT = new InjectionToken('JR_CONTEXT');
8
+ /** Convenience helper to inject the current {@link JrContext} from a component. */
9
+ function injectJrContext() {
10
+ return inject(JR_CONTEXT);
11
+ }
12
+
13
+ /** DI token for a default {@link JrRegistry} provided via `provideJsonRender`. */
14
+ const JR_REGISTRY = new InjectionToken('JR_REGISTRY');
15
+ /**
16
+ * Build a {@link JrRegistry} from a map of catalog `type` → Angular component class.
17
+ *
18
+ * ```ts
19
+ * const registry = defineRegistry({
20
+ * Stack: JrStack,
21
+ * Text: JrText,
22
+ * Button: JrButton,
23
+ * });
24
+ * ```
25
+ */
26
+ function defineRegistry(components) {
27
+ return { components: { ...components } };
28
+ }
29
+ /** Merge multiple registries; later entries win on conflicting keys. */
30
+ function mergeRegistries(...registries) {
31
+ return {
32
+ components: Object.assign({}, ...registries.map((r) => r.components)),
33
+ };
34
+ }
35
+
36
+ /** DI token for the {@link JrActionDispatcher}. */
37
+ const JR_ACTION_DISPATCHER = new InjectionToken('JR_ACTION_DISPATCHER');
38
+ /**
39
+ * Register json-render defaults for the application (or a route).
40
+ *
41
+ * ```ts
42
+ * export const appConfig: ApplicationConfig = {
43
+ * providers: [
44
+ * provideJsonRender({
45
+ * registry: primitivesRegistry,
46
+ * actions: { export_report: async () => { ... } },
47
+ * }),
48
+ * ],
49
+ * };
50
+ * ```
51
+ */
52
+ function provideJsonRender(options = {}) {
53
+ const providers = [];
54
+ if (options.registry) {
55
+ providers.push({ provide: JR_REGISTRY, useValue: options.registry });
56
+ }
57
+ const actions = options.actions;
58
+ if (actions) {
59
+ const dispatcher = {
60
+ dispatch: (ctx) => actions[ctx.action]?.(ctx),
61
+ };
62
+ providers.push({ provide: JR_ACTION_DISPATCHER, useValue: dispatcher });
63
+ }
64
+ return makeEnvironmentProviders(providers);
65
+ }
66
+
67
+ /**
68
+ * Signal-based store that owns the current {@link Spec} and its runtime state.
69
+ *
70
+ * All calls into `@json-render/core` state utilities are funnelled through here
71
+ * so the rest of the engine depends on a single, stable surface. State changes
72
+ * from `@json-render/core`'s in-memory store are mirrored into an Angular signal
73
+ * (`state`) so templates and prop resolution stay reactive under zoneless CD.
74
+ */
75
+ class JrStateStore {
76
+ _spec = signal(null, ...(ngDevMode ? [{ debugName: "_spec" }] : /* istanbul ignore next */ []));
77
+ /** The current spec, or `null` before one is set. */
78
+ spec = this._spec.asReadonly();
79
+ // `equal: () => false` forces the signal to notify on every store change even
80
+ // when the core store mutates the snapshot in place.
81
+ _state = signal({}, { ...(ngDevMode ? { debugName: "_state" } : /* istanbul ignore next */ {}), equal: () => false });
82
+ /** The current runtime state model. */
83
+ state = this._state.asReadonly();
84
+ store = createStateStore({});
85
+ unsubscribe;
86
+ /** Replace the spec and re-seed runtime state from `spec.state`. */
87
+ setSpec(spec) {
88
+ this._spec.set(spec);
89
+ this.unsubscribe?.();
90
+ this.store = createStateStore(spec.state ?? {});
91
+ this.unsubscribe = this.store.subscribe(() => this._state.set(this.store.getSnapshot()));
92
+ this._state.set(this.store.getSnapshot());
93
+ }
94
+ /** Merge additional state on top of the current model. */
95
+ patchState(state) {
96
+ this.store.update(state);
97
+ }
98
+ /** Look up an element by id in the current spec. */
99
+ getElement(id) {
100
+ return this._spec()?.elements[id];
101
+ }
102
+ /** Read a value from state by JSON Pointer path. */
103
+ getStatePath(path) {
104
+ return this.store.get(path);
105
+ }
106
+ /** Write a value into state by JSON Pointer path (notifies subscribers). */
107
+ setStatePath(path, value) {
108
+ this.store.set(path, value);
109
+ }
110
+ /** Current full state snapshot (non-reactive read). */
111
+ snapshot() {
112
+ return this.store.getSnapshot();
113
+ }
114
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JrStateStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
115
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JrStateStore });
116
+ }
117
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JrStateStore, decorators: [{
118
+ type: Injectable
119
+ }] });
120
+
121
+ const ROOT_SCOPE = { item: undefined, index: -1 };
122
+ /**
123
+ * Renders a json-render {@link Spec} into native Angular components.
124
+ *
125
+ * ```html
126
+ * <jr-renderer [spec]="spec()" [registry]="registry" (action)="onAction($event)" />
127
+ * ```
128
+ *
129
+ * The spec is a flat tree of typed elements; each `type` is resolved against the
130
+ * {@link JrRegistry} (from the `[registry]` input or `provideJsonRender`) and
131
+ * instantiated dynamically. Children are projected into each component's
132
+ * `<ng-content>`.
133
+ *
134
+ * Structural changes (the spec itself) rebuild the tree; state changes update
135
+ * individual nodes' inputs in place via per-node effects — so bound inputs stay
136
+ * live and keep focus. Props declaring `$bindState` two-way bind to a component's
137
+ * `model()` (Signal Forms `FormValueControl` / `FormCheckboxControl`).
138
+ */
139
+ class JrRenderer {
140
+ /** The spec to render. */
141
+ spec = input(null, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
142
+ /** Registry mapping component types to Angular classes (overrides the DI default). */
143
+ registry = input(null, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
144
+ /** Initial/overriding runtime state merged on top of `spec.state`. */
145
+ state = input(undefined, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
146
+ /** Emits whenever a rendered component raises an action. */
147
+ action = output();
148
+ anchor = viewChild.required('anchor', {
149
+ read: ViewContainerRef,
150
+ });
151
+ store = inject(JrStateStore);
152
+ envInjector = inject(EnvironmentInjector);
153
+ injector = inject(Injector);
154
+ fallbackRegistry = inject(JR_REGISTRY, { optional: true });
155
+ dispatcher = inject(JR_ACTION_DISPATCHER, {
156
+ optional: true,
157
+ });
158
+ /** Per-node effects (prop resolution + two-way bindings), torn down on rebuild. */
159
+ effects = [];
160
+ constructor() {
161
+ // Feed spec/state inputs into the store.
162
+ effect(() => {
163
+ const spec = this.spec();
164
+ if (!spec)
165
+ return;
166
+ const extra = this.state();
167
+ const merged = extra
168
+ ? { ...spec, state: { ...(spec.state ?? {}), ...extra } }
169
+ : spec;
170
+ untracked(() => this.store.setSpec(merged));
171
+ });
172
+ // Rebuild the DOM only when the spec (structure) changes. State-driven prop
173
+ // updates are handled per node, without rebuilding.
174
+ effect((onCleanup) => {
175
+ const vcr = this.anchor();
176
+ const spec = this.store.spec();
177
+ untracked(() => this.render(vcr, spec));
178
+ onCleanup(() => this.teardown(vcr));
179
+ });
180
+ }
181
+ render(vcr, spec) {
182
+ this.teardown(vcr);
183
+ if (!spec?.root)
184
+ return;
185
+ const registry = this.registry() ?? this.fallbackRegistry;
186
+ if (!registry) {
187
+ console.warn('[json-render] No registry provided (set [registry] or use provideJsonRender).');
188
+ return;
189
+ }
190
+ this.buildNode(vcr, spec.root, registry, ROOT_SCOPE);
191
+ }
192
+ buildNode(vcr, id, registry, scope) {
193
+ const element = this.store.getElement(id);
194
+ if (!element)
195
+ return null;
196
+ if (element.visible !== undefined && !this.isVisible(element, scope)) {
197
+ return null;
198
+ }
199
+ const cmp = registry.components[element.type];
200
+ if (!cmp) {
201
+ console.warn(`[json-render] No component registered for type "${element.type}".`);
202
+ return null;
203
+ }
204
+ // Build children first so their host nodes can be projected into <ng-content>.
205
+ const childNodes = [];
206
+ for (const childId of element.children ?? []) {
207
+ const childRef = this.buildNode(vcr, childId, registry, scope);
208
+ if (childRef)
209
+ childNodes.push(childRef.location.nativeElement);
210
+ }
211
+ const ref = vcr.createComponent(cmp, {
212
+ injector: this.makeNodeInjector(id, element, scope),
213
+ environmentInjector: this.envInjector,
214
+ projectableNodes: [childNodes],
215
+ });
216
+ const mirror = reflectComponentType(cmp);
217
+ const inputNames = new Set(mirror?.inputs.map((i) => i.templateName) ?? []);
218
+ this.bindProps(ref, element, scope, inputNames);
219
+ this.bindTwoWay(ref, element, mirror, inputNames);
220
+ return ref;
221
+ }
222
+ /** Reactively resolve and apply props; re-runs when referenced state changes. */
223
+ bindProps(ref, element, scope, inputNames) {
224
+ const eff = effect(() => {
225
+ const resolved = resolveElementProps(element.props ?? {}, {
226
+ stateModel: this.store.state(),
227
+ repeatItem: scope.item,
228
+ repeatIndex: scope.index,
229
+ });
230
+ untracked(() => {
231
+ for (const [key, value] of Object.entries(resolved)) {
232
+ if (inputNames.has(key))
233
+ ref.setInput(key, value);
234
+ }
235
+ if (inputNames.has('props'))
236
+ ref.setInput('props', resolved);
237
+ });
238
+ }, { ...(ngDevMode ? { debugName: "eff" } : /* istanbul ignore next */ {}), injector: this.injector });
239
+ this.effects.push(eff);
240
+ }
241
+ /**
242
+ * Wire two-way `$bindState` props to a component's `model()` output. Works with
243
+ * any Signal Forms control (`value`/`checked` model), writing edits back to state.
244
+ */
245
+ bindTwoWay(ref, element, mirror, inputNames) {
246
+ if (!mirror)
247
+ return;
248
+ const bindings = resolveBindings(element.props ?? {}, {
249
+ stateModel: this.store.snapshot(),
250
+ });
251
+ if (!bindings)
252
+ return;
253
+ const outputs = new Set(mirror.outputs.map((o) => o.templateName));
254
+ const instance = ref.instance;
255
+ for (const [prop, path] of Object.entries(bindings)) {
256
+ // A model() prop exposes a matching `<prop>Change` output.
257
+ if (!inputNames.has(prop) || !outputs.has(`${prop}Change`))
258
+ continue;
259
+ const modelSignal = instance[prop];
260
+ if (typeof modelSignal !== 'function')
261
+ continue;
262
+ const eff = effect(() => {
263
+ const next = modelSignal();
264
+ untracked(() => {
265
+ if (this.store.getStatePath(path) !== next) {
266
+ this.store.setStatePath(path, next);
267
+ }
268
+ });
269
+ }, { ...(ngDevMode ? { debugName: "eff" } : /* istanbul ignore next */ {}), injector: this.injector });
270
+ this.effects.push(eff);
271
+ }
272
+ }
273
+ makeNodeInjector(id, element, scope) {
274
+ const item = signal(scope.item, ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
275
+ const index = signal(scope.index, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
276
+ const context = {
277
+ nodeId: id,
278
+ item: item.asReadonly(),
279
+ index: index.asReadonly(),
280
+ emit: (event, payload) => this.handleEmit(id, element, event, payload),
281
+ };
282
+ return Injector.create({
283
+ providers: [{ provide: JR_CONTEXT, useValue: context }],
284
+ parent: this.injector,
285
+ });
286
+ }
287
+ isVisible(element, scope) {
288
+ return evaluateVisibility(element.visible, {
289
+ stateModel: this.store.snapshot(),
290
+ repeatItem: scope.item,
291
+ repeatIndex: scope.index,
292
+ });
293
+ }
294
+ handleEmit(nodeId, element, event, payload) {
295
+ const binding = element.on?.[event];
296
+ const action = Array.isArray(binding)
297
+ ? (binding[0]?.action ?? event)
298
+ : (binding?.action ?? event);
299
+ const ctx = { action, payload, nodeId, element };
300
+ void this.dispatcher?.dispatch(ctx);
301
+ const spec = this.store.spec();
302
+ if (spec)
303
+ this.action.emit({ ...ctx, spec });
304
+ }
305
+ teardown(vcr) {
306
+ for (const eff of this.effects)
307
+ eff.destroy();
308
+ this.effects.length = 0;
309
+ vcr.clear();
310
+ }
311
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JrRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
312
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.17", type: JrRenderer, isStandalone: true, selector: "jr-renderer", inputs: { spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: false, transformFunction: null }, registry: { classPropertyName: "registry", publicName: "registry", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, providers: [JrStateStore], viewQueries: [{ propertyName: "anchor", first: true, predicate: ["anchor"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: `<ng-container #anchor />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
313
+ }
314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: JrRenderer, decorators: [{
315
+ type: Component,
316
+ args: [{
317
+ selector: 'jr-renderer',
318
+ template: `<ng-container #anchor />`,
319
+ changeDetection: ChangeDetectionStrategy.OnPush,
320
+ providers: [JrStateStore],
321
+ }]
322
+ }], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: false }] }], registry: [{ type: i0.Input, args: [{ isSignal: true, alias: "registry", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], action: [{ type: i0.Output, args: ["action"] }], anchor: [{ type: i0.ViewChild, args: ['anchor', { ...{
323
+ read: ViewContainerRef,
324
+ }, isSignal: true }] }] } });
325
+
326
+ /**
327
+ * Generated bundle index. Do not edit.
328
+ */
329
+
330
+ export { JR_ACTION_DISPATCHER, JR_CONTEXT, JR_REGISTRY, JrRenderer, JrStateStore, defineRegistry, injectJrContext, mergeRegistries, provideJsonRender };
331
+ //# sourceMappingURL=ng-json-render-core.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-json-render-core.mjs","sources":["../../../../libs/core/src/lib/jr-context.ts","../../../../libs/core/src/lib/registry.ts","../../../../libs/core/src/lib/providers.ts","../../../../libs/core/src/lib/state-store.ts","../../../../libs/core/src/lib/jr-renderer.ts","../../../../libs/core/src/ng-json-render-core.ts"],"sourcesContent":["import { InjectionToken, type Signal, inject } from '@angular/core';\n\n/**\n * Per-node context made available to every rendered catalog component via DI.\n *\n * A component injects it to raise events and to read its data-binding scope:\n *\n * ```ts\n * @Component({ selector: 'jr-button', template: `<button (click)=\"ctx.emit('press')\"><ng-content/></button>` })\n * export class JrButton { protected ctx = inject(JR_CONTEXT); }\n * ```\n */\nexport interface JrContext {\n /** Id of the spec element this component instance renders. */\n readonly nodeId: string;\n /** The current `$item` when inside a `repeat` scope, otherwise `undefined`. */\n readonly item: Signal<unknown>;\n /** The current `$index` when inside a `repeat` scope, otherwise `-1`. */\n readonly index: Signal<number>;\n /** Raise a named event; routed to the element's `on` bindings / action handlers. */\n emit(event: string, payload?: unknown): void;\n}\n\n/** Injection token providing the per-node {@link JrContext}. */\nexport const JR_CONTEXT = new InjectionToken<JrContext>('JR_CONTEXT');\n\n/** Convenience helper to inject the current {@link JrContext} from a component. */\nexport function injectJrContext(): JrContext {\n return inject(JR_CONTEXT);\n}\n","import { InjectionToken, type Type } from '@angular/core';\nimport type { JrRegistry } from './types';\n\n/** DI token for a default {@link JrRegistry} provided via `provideJsonRender`. */\nexport const JR_REGISTRY = new InjectionToken<JrRegistry>('JR_REGISTRY');\n\n/**\n * Build a {@link JrRegistry} from a map of catalog `type` → Angular component class.\n *\n * ```ts\n * const registry = defineRegistry({\n * Stack: JrStack,\n * Text: JrText,\n * Button: JrButton,\n * });\n * ```\n */\nexport function defineRegistry(\n components: Record<string, Type<unknown>>,\n): JrRegistry {\n return { components: { ...components } };\n}\n\n/** Merge multiple registries; later entries win on conflicting keys. */\nexport function mergeRegistries(...registries: JrRegistry[]): JrRegistry {\n return {\n components: Object.assign({}, ...registries.map((r) => r.components)),\n };\n}\n","import {\n type EnvironmentProviders,\n InjectionToken,\n makeEnvironmentProviders,\n} from '@angular/core';\nimport { JR_REGISTRY } from './registry';\nimport type { JrActionContext, JrActionHandler, JrRegistry } from './types';\n\n/** A dispatcher routes emitted actions to their handlers. */\nexport interface JrActionDispatcher {\n dispatch(ctx: JrActionContext): void | Promise<void>;\n}\n\n/** DI token for the {@link JrActionDispatcher}. */\nexport const JR_ACTION_DISPATCHER = new InjectionToken<JrActionDispatcher>(\n 'JR_ACTION_DISPATCHER',\n);\n\n/** Options for {@link provideJsonRender}. */\nexport interface ProvideJsonRenderOptions {\n /** A default registry used by `JrRenderer` when no `[registry]` input is set. */\n registry?: JrRegistry;\n /** Map of action name → handler, invoked when a component emits that action. */\n actions?: Record<string, JrActionHandler>;\n}\n\n/**\n * Register json-render defaults for the application (or a route).\n *\n * ```ts\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideJsonRender({\n * registry: primitivesRegistry,\n * actions: { export_report: async () => { ... } },\n * }),\n * ],\n * };\n * ```\n */\nexport function provideJsonRender(\n options: ProvideJsonRenderOptions = {},\n): EnvironmentProviders {\n const providers = [];\n\n if (options.registry) {\n providers.push({ provide: JR_REGISTRY, useValue: options.registry });\n }\n\n const actions = options.actions;\n if (actions) {\n const dispatcher: JrActionDispatcher = {\n dispatch: (ctx) => actions[ctx.action]?.(ctx),\n };\n providers.push({ provide: JR_ACTION_DISPATCHER, useValue: dispatcher });\n }\n\n return makeEnvironmentProviders(providers);\n}\n","import { Injectable, type Signal, signal } from '@angular/core';\nimport {\n type Spec,\n type StateModel,\n type StateStore,\n type UIElement,\n createStateStore,\n} from '@json-render/core';\n\n/**\n * Signal-based store that owns the current {@link Spec} and its runtime state.\n *\n * All calls into `@json-render/core` state utilities are funnelled through here\n * so the rest of the engine depends on a single, stable surface. State changes\n * from `@json-render/core`'s in-memory store are mirrored into an Angular signal\n * (`state`) so templates and prop resolution stay reactive under zoneless CD.\n */\n@Injectable()\nexport class JrStateStore {\n private readonly _spec = signal<Spec | null>(null);\n /** The current spec, or `null` before one is set. */\n readonly spec: Signal<Spec | null> = this._spec.asReadonly();\n\n // `equal: () => false` forces the signal to notify on every store change even\n // when the core store mutates the snapshot in place.\n private readonly _state = signal<StateModel>(\n {},\n { equal: () => false },\n );\n /** The current runtime state model. */\n readonly state: Signal<StateModel> = this._state.asReadonly();\n\n private store: StateStore = createStateStore({});\n private unsubscribe?: () => void;\n\n /** Replace the spec and re-seed runtime state from `spec.state`. */\n setSpec(spec: Spec): void {\n this._spec.set(spec);\n this.unsubscribe?.();\n this.store = createStateStore(spec.state ?? {});\n this.unsubscribe = this.store.subscribe(() =>\n this._state.set(this.store.getSnapshot()),\n );\n this._state.set(this.store.getSnapshot());\n }\n\n /** Merge additional state on top of the current model. */\n patchState(state: StateModel): void {\n this.store.update(state);\n }\n\n /** Look up an element by id in the current spec. */\n getElement(id: string): UIElement | undefined {\n return this._spec()?.elements[id];\n }\n\n /** Read a value from state by JSON Pointer path. */\n getStatePath(path: string): unknown {\n return this.store.get(path);\n }\n\n /** Write a value into state by JSON Pointer path (notifies subscribers). */\n setStatePath(path: string, value: unknown): void {\n this.store.set(path, value);\n }\n\n /** Current full state snapshot (non-reactive read). */\n snapshot(): StateModel {\n return this.store.getSnapshot();\n }\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n type ComponentRef,\n type EffectRef,\n EnvironmentInjector,\n Injector,\n ViewContainerRef,\n effect,\n inject,\n input,\n output,\n reflectComponentType,\n signal,\n untracked,\n viewChild,\n} from '@angular/core';\nimport {\n type Spec,\n type StateModel,\n type UIElement,\n evaluateVisibility,\n resolveBindings,\n resolveElementProps,\n} from '@json-render/core';\nimport { JR_CONTEXT, type JrContext } from './jr-context';\nimport { JR_ACTION_DISPATCHER } from './providers';\nimport { JR_REGISTRY } from './registry';\nimport { JrStateStore } from './state-store';\nimport type { JrActionEvent, JrRegistry } from './types';\n\n/** Iteration scope threaded through the recursive build (`$item` / `$index`). */\ninterface RenderScope {\n readonly item: unknown;\n readonly index: number;\n}\n\nconst ROOT_SCOPE: RenderScope = { item: undefined, index: -1 };\n\n/**\n * Renders a json-render {@link Spec} into native Angular components.\n *\n * ```html\n * <jr-renderer [spec]=\"spec()\" [registry]=\"registry\" (action)=\"onAction($event)\" />\n * ```\n *\n * The spec is a flat tree of typed elements; each `type` is resolved against the\n * {@link JrRegistry} (from the `[registry]` input or `provideJsonRender`) and\n * instantiated dynamically. Children are projected into each component's\n * `<ng-content>`.\n *\n * Structural changes (the spec itself) rebuild the tree; state changes update\n * individual nodes' inputs in place via per-node effects — so bound inputs stay\n * live and keep focus. Props declaring `$bindState` two-way bind to a component's\n * `model()` (Signal Forms `FormValueControl` / `FormCheckboxControl`).\n */\n@Component({\n selector: 'jr-renderer',\n template: `<ng-container #anchor />`,\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [JrStateStore],\n})\nexport class JrRenderer {\n /** The spec to render. */\n readonly spec = input<Spec | null>(null);\n /** Registry mapping component types to Angular classes (overrides the DI default). */\n readonly registry = input<JrRegistry | null>(null);\n /** Initial/overriding runtime state merged on top of `spec.state`. */\n readonly state = input<StateModel | undefined>(undefined);\n /** Emits whenever a rendered component raises an action. */\n readonly action = output<JrActionEvent>();\n\n private readonly anchor = viewChild.required('anchor', {\n read: ViewContainerRef,\n });\n\n private readonly store = inject(JrStateStore);\n private readonly envInjector = inject(EnvironmentInjector);\n private readonly injector = inject(Injector);\n private readonly fallbackRegistry = inject(JR_REGISTRY, { optional: true });\n private readonly dispatcher = inject(JR_ACTION_DISPATCHER, {\n optional: true,\n });\n\n /** Per-node effects (prop resolution + two-way bindings), torn down on rebuild. */\n private readonly effects: EffectRef[] = [];\n\n constructor() {\n // Feed spec/state inputs into the store.\n effect(() => {\n const spec = this.spec();\n if (!spec) return;\n const extra = this.state();\n const merged: Spec = extra\n ? { ...spec, state: { ...(spec.state ?? {}), ...extra } }\n : spec;\n untracked(() => this.store.setSpec(merged));\n });\n\n // Rebuild the DOM only when the spec (structure) changes. State-driven prop\n // updates are handled per node, without rebuilding.\n effect((onCleanup) => {\n const vcr = this.anchor();\n const spec = this.store.spec();\n untracked(() => this.render(vcr, spec));\n onCleanup(() => this.teardown(vcr));\n });\n }\n\n private render(vcr: ViewContainerRef, spec: Spec | null): void {\n this.teardown(vcr);\n if (!spec?.root) return;\n const registry = this.registry() ?? this.fallbackRegistry;\n if (!registry) {\n console.warn(\n '[json-render] No registry provided (set [registry] or use provideJsonRender).',\n );\n return;\n }\n this.buildNode(vcr, spec.root, registry, ROOT_SCOPE);\n }\n\n private buildNode(\n vcr: ViewContainerRef,\n id: string,\n registry: JrRegistry,\n scope: RenderScope,\n ): ComponentRef<unknown> | null {\n const element = this.store.getElement(id);\n if (!element) return null;\n\n if (element.visible !== undefined && !this.isVisible(element, scope)) {\n return null;\n }\n\n const cmp = registry.components[element.type];\n if (!cmp) {\n console.warn(\n `[json-render] No component registered for type \"${element.type}\".`,\n );\n return null;\n }\n\n // Build children first so their host nodes can be projected into <ng-content>.\n const childNodes: Node[] = [];\n for (const childId of element.children ?? []) {\n const childRef = this.buildNode(vcr, childId, registry, scope);\n if (childRef) childNodes.push(childRef.location.nativeElement as Node);\n }\n\n const ref = vcr.createComponent(cmp, {\n injector: this.makeNodeInjector(id, element, scope),\n environmentInjector: this.envInjector,\n projectableNodes: [childNodes],\n });\n\n const mirror = reflectComponentType(cmp);\n const inputNames = new Set(mirror?.inputs.map((i) => i.templateName) ?? []);\n this.bindProps(ref, element, scope, inputNames);\n this.bindTwoWay(ref, element, mirror, inputNames);\n return ref;\n }\n\n /** Reactively resolve and apply props; re-runs when referenced state changes. */\n private bindProps(\n ref: ComponentRef<unknown>,\n element: UIElement,\n scope: RenderScope,\n inputNames: ReadonlySet<string>,\n ): void {\n const eff = effect(\n () => {\n const resolved = resolveElementProps(element.props ?? {}, {\n stateModel: this.store.state(),\n repeatItem: scope.item,\n repeatIndex: scope.index,\n });\n untracked(() => {\n for (const [key, value] of Object.entries(resolved)) {\n if (inputNames.has(key)) ref.setInput(key, value);\n }\n if (inputNames.has('props')) ref.setInput('props', resolved);\n });\n },\n { injector: this.injector },\n );\n this.effects.push(eff);\n }\n\n /**\n * Wire two-way `$bindState` props to a component's `model()` output. Works with\n * any Signal Forms control (`value`/`checked` model), writing edits back to state.\n */\n private bindTwoWay(\n ref: ComponentRef<unknown>,\n element: UIElement,\n mirror: ReturnType<typeof reflectComponentType>,\n inputNames: ReadonlySet<string>,\n ): void {\n if (!mirror) return;\n const bindings = resolveBindings(element.props ?? {}, {\n stateModel: this.store.snapshot(),\n });\n if (!bindings) return;\n\n const outputs = new Set(mirror.outputs.map((o) => o.templateName));\n const instance = ref.instance as Record<string, unknown>;\n\n for (const [prop, path] of Object.entries(bindings)) {\n // A model() prop exposes a matching `<prop>Change` output.\n if (!inputNames.has(prop) || !outputs.has(`${prop}Change`)) continue;\n const modelSignal = instance[prop];\n if (typeof modelSignal !== 'function') continue;\n\n const eff = effect(\n () => {\n const next = (modelSignal as () => unknown)();\n untracked(() => {\n if (this.store.getStatePath(path) !== next) {\n this.store.setStatePath(path, next);\n }\n });\n },\n { injector: this.injector },\n );\n this.effects.push(eff);\n }\n }\n\n private makeNodeInjector(\n id: string,\n element: UIElement,\n scope: RenderScope,\n ): Injector {\n const item = signal(scope.item);\n const index = signal(scope.index);\n const context: JrContext = {\n nodeId: id,\n item: item.asReadonly(),\n index: index.asReadonly(),\n emit: (event, payload) => this.handleEmit(id, element, event, payload),\n };\n return Injector.create({\n providers: [{ provide: JR_CONTEXT, useValue: context }],\n parent: this.injector,\n });\n }\n\n private isVisible(element: UIElement, scope: RenderScope): boolean {\n return evaluateVisibility(element.visible, {\n stateModel: this.store.snapshot(),\n repeatItem: scope.item,\n repeatIndex: scope.index,\n });\n }\n\n private handleEmit(\n nodeId: string,\n element: UIElement,\n event: string,\n payload: unknown,\n ): void {\n const binding = element.on?.[event];\n const action = Array.isArray(binding)\n ? (binding[0]?.action ?? event)\n : (binding?.action ?? event);\n\n const ctx = { action, payload, nodeId, element };\n void this.dispatcher?.dispatch(ctx);\n\n const spec = this.store.spec();\n if (spec) this.action.emit({ ...ctx, spec });\n }\n\n private teardown(vcr: ViewContainerRef): void {\n for (const eff of this.effects) eff.destroy();\n this.effects.length = 0;\n vcr.clear();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAuBA;MACa,UAAU,GAAG,IAAI,cAAc,CAAY,YAAY;AAEpE;SACgB,eAAe,GAAA;AAC7B,IAAA,OAAO,MAAM,CAAC,UAAU,CAAC;AAC3B;;AC1BA;MACa,WAAW,GAAG,IAAI,cAAc,CAAa,aAAa;AAEvE;;;;;;;;;;AAUG;AACG,SAAU,cAAc,CAC5B,UAAyC,EAAA;IAEzC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,UAAU,EAAE,EAAE;AAC1C;AAEA;AACM,SAAU,eAAe,CAAC,GAAG,UAAwB,EAAA;IACzD,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;KACtE;AACH;;ACfA;MACa,oBAAoB,GAAG,IAAI,cAAc,CACpD,sBAAsB;AAWxB;;;;;;;;;;;;;AAaG;AACG,SAAU,iBAAiB,CAC/B,OAAA,GAAoC,EAAE,EAAA;IAEtC,MAAM,SAAS,GAAG,EAAE;AAEpB,IAAA,IAAI,OAAO,CAAC,QAAQ,EAAE;AACpB,QAAA,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IACtE;AAEA,IAAA,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO;IAC/B,IAAI,OAAO,EAAE;AACX,QAAA,MAAM,UAAU,GAAuB;AACrC,YAAA,QAAQ,EAAE,CAAC,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;SAC9C;AACD,QAAA,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IACzE;AAEA,IAAA,OAAO,wBAAwB,CAAC,SAAS,CAAC;AAC5C;;ACjDA;;;;;;;AAOG;MAEU,YAAY,CAAA;AACN,IAAA,KAAK,GAAG,MAAM,CAAc,IAAI,4EAAC;;AAEzC,IAAA,IAAI,GAAwB,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE;;;AAI3C,IAAA,MAAM,GAAG,MAAM,CAC9B,EAAE,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,QAAA,EAAA,8BAAA,EAAA,CAAA,EACA,KAAK,EAAE,MAAM,KAAK,EAAA,CACrB;;AAEQ,IAAA,KAAK,GAAuB,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE;AAErD,IAAA,KAAK,GAAe,gBAAgB,CAAC,EAAE,CAAC;AACxC,IAAA,WAAW;;AAGnB,IAAA,OAAO,CAAC,IAAU,EAAA;AAChB,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;AACpB,QAAA,IAAI,CAAC,WAAW,IAAI;QACpB,IAAI,CAAC,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MACtC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAC1C;AACD,QAAA,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;IAC3C;;AAGA,IAAA,UAAU,CAAC,KAAiB,EAAA;AAC1B,QAAA,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC;IAC1B;;AAGA,IAAA,UAAU,CAAC,EAAU,EAAA;QACnB,OAAO,IAAI,CAAC,KAAK,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC;IACnC;;AAGA,IAAA,YAAY,CAAC,IAAY,EAAA;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;IAC7B;;IAGA,YAAY,CAAC,IAAY,EAAE,KAAc,EAAA;QACvC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC;IAC7B;;IAGA,QAAQ,GAAA;AACN,QAAA,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;IACjC;wGAnDW,YAAY,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;4GAAZ,YAAY,EAAA,CAAA;;4FAAZ,YAAY,EAAA,UAAA,EAAA,CAAA;kBADxB;;;ACoBD,MAAM,UAAU,GAAgB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE;AAE9D;;;;;;;;;;;;;;;;AAgBG;MAOU,UAAU,CAAA;;AAEZ,IAAA,IAAI,GAAG,KAAK,CAAc,IAAI,2EAAC;;AAE/B,IAAA,QAAQ,GAAG,KAAK,CAAoB,IAAI,+EAAC;;AAEzC,IAAA,KAAK,GAAG,KAAK,CAAyB,SAAS,4EAAC;;IAEhD,MAAM,GAAG,MAAM,EAAiB;AAExB,IAAA,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE;AACrD,QAAA,IAAI,EAAE,gBAAgB;AACvB,KAAA,CAAC;AAEe,IAAA,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC;AAC5B,IAAA,WAAW,GAAG,MAAM,CAAC,mBAAmB,CAAC;AACzC,IAAA,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC3B,gBAAgB,GAAG,MAAM,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC1D,IAAA,UAAU,GAAG,MAAM,CAAC,oBAAoB,EAAE;AACzD,QAAA,QAAQ,EAAE,IAAI;AACf,KAAA,CAAC;;IAGe,OAAO,GAAgB,EAAE;AAE1C,IAAA,WAAA,GAAA;;QAEE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE;AACxB,YAAA,IAAI,CAAC,IAAI;gBAAE;AACX,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;YAC1B,MAAM,MAAM,GAAS;kBACjB,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE;kBACrD,IAAI;AACR,YAAA,SAAS,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAC7C,QAAA,CAAC,CAAC;;;AAIF,QAAA,MAAM,CAAC,CAAC,SAAS,KAAI;AACnB,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;AAC9B,YAAA,SAAS,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACvC,SAAS,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACrC,QAAA,CAAC,CAAC;IACJ;IAEQ,MAAM,CAAC,GAAqB,EAAE,IAAiB,EAAA;AACrD,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAClB,IAAI,CAAC,IAAI,EAAE,IAAI;YAAE;QACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC,gBAAgB;QACzD,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,OAAO,CAAC,IAAI,CACV,+EAA+E,CAChF;YACD;QACF;AACA,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC;IACtD;AAEQ,IAAA,SAAS,CACf,GAAqB,EACrB,EAAU,EACV,QAAoB,EACpB,KAAkB,EAAA;QAElB,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;AACzC,QAAA,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,IAAI;AAEzB,QAAA,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;AACpE,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC;QAC7C,IAAI,CAAC,GAAG,EAAE;YACR,OAAO,CAAC,IAAI,CACV,CAAA,gDAAA,EAAmD,OAAO,CAAC,IAAI,CAAA,EAAA,CAAI,CACpE;AACD,YAAA,OAAO,IAAI;QACb;;QAGA,MAAM,UAAU,GAAW,EAAE;QAC7B,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE;AAC5C,YAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC;AAC9D,YAAA,IAAI,QAAQ;gBAAE,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAqB,CAAC;QACxE;AAEA,QAAA,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,EAAE;YACnC,QAAQ,EAAE,IAAI,CAAC,gBAAgB,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC;YACnD,mBAAmB,EAAE,IAAI,CAAC,WAAW;YACrC,gBAAgB,EAAE,CAAC,UAAU,CAAC;AAC/B,SAAA,CAAC;AAEF,QAAA,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAC3E,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC;QAC/C,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC;AACjD,QAAA,OAAO,GAAG;IACZ;;AAGQ,IAAA,SAAS,CACf,GAA0B,EAC1B,OAAkB,EAClB,KAAkB,EAClB,UAA+B,EAAA;AAE/B,QAAA,MAAM,GAAG,GAAG,MAAM,CAChB,MAAK;YACH,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,EAAE;AACxD,gBAAA,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;gBAC9B,UAAU,EAAE,KAAK,CAAC,IAAI;gBACtB,WAAW,EAAE,KAAK,CAAC,KAAK;AACzB,aAAA,CAAC;YACF,SAAS,CAAC,MAAK;AACb,gBAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;AACnD,oBAAA,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;AAAE,wBAAA,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC;gBACnD;AACA,gBAAA,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC;AAAE,oBAAA,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;AAC9D,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,2EACC,QAAQ,EAAE,IAAI,CAAC,QAAQ,GAC1B;AACD,QAAA,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB;AAEA;;;AAGG;AACK,IAAA,UAAU,CAChB,GAA0B,EAC1B,OAAkB,EAClB,MAA+C,EAC/C,UAA+B,EAAA;AAE/B,QAAA,IAAI,CAAC,MAAM;YAAE;QACb,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,EAAE;AACpD,YAAA,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE;AAClC,SAAA,CAAC;AACF,QAAA,IAAI,CAAC,QAAQ;YAAE;QAEf,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;AAClE,QAAA,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAmC;AAExD,QAAA,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;;AAEnD,YAAA,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA,EAAG,IAAI,QAAQ,CAAC;gBAAE;AAC5D,YAAA,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC;YAClC,IAAI,OAAO,WAAW,KAAK,UAAU;gBAAE;AAEvC,YAAA,MAAM,GAAG,GAAG,MAAM,CAChB,MAAK;AACH,gBAAA,MAAM,IAAI,GAAI,WAA6B,EAAE;gBAC7C,SAAS,CAAC,MAAK;oBACb,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;wBAC1C,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC;oBACrC;AACF,gBAAA,CAAC,CAAC;AACJ,YAAA,CAAC,2EACC,QAAQ,EAAE,IAAI,CAAC,QAAQ,GAC1B;AACD,YAAA,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QACxB;IACF;AAEQ,IAAA,gBAAgB,CACtB,EAAU,EACV,OAAkB,EAClB,KAAkB,EAAA;QAElB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,2EAAC;QAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,4EAAC;AACjC,QAAA,MAAM,OAAO,GAAc;AACzB,YAAA,MAAM,EAAE,EAAE;AACV,YAAA,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE;AACvB,YAAA,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE;AACzB,YAAA,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;SACvE;QACD,OAAO,QAAQ,CAAC,MAAM,CAAC;YACrB,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;YACvD,MAAM,EAAE,IAAI,CAAC,QAAQ;AACtB,SAAA,CAAC;IACJ;IAEQ,SAAS,CAAC,OAAkB,EAAE,KAAkB,EAAA;AACtD,QAAA,OAAO,kBAAkB,CAAC,OAAO,CAAC,OAAO,EAAE;AACzC,YAAA,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE;YACjC,UAAU,EAAE,KAAK,CAAC,IAAI;YACtB,WAAW,EAAE,KAAK,CAAC,KAAK;AACzB,SAAA,CAAC;IACJ;AAEQ,IAAA,UAAU,CAChB,MAAc,EACd,OAAkB,EAClB,KAAa,EACb,OAAgB,EAAA;QAEhB,MAAM,OAAO,GAAG,OAAO,CAAC,EAAE,GAAG,KAAK,CAAC;AACnC,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO;eAC/B,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,KAAK;eAC3B,OAAO,EAAE,MAAM,IAAI,KAAK,CAAC;QAE9B,MAAM,GAAG,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAChD,KAAK,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC;QAEnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;AAC9B,QAAA,IAAI,IAAI;AAAE,YAAA,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;IAC9C;AAEQ,IAAA,QAAQ,CAAC,GAAqB,EAAA;AACpC,QAAA,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO;YAAE,GAAG,CAAC,OAAO,EAAE;AAC7C,QAAA,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QACvB,GAAG,CAAC,KAAK,EAAE;IACb;wGAxNW,UAAU,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAV,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,UAAU,mdAFV,CAAC,YAAY,CAAC,EAAA,WAAA,EAAA,CAAA,EAAA,YAAA,EAAA,QAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAAA,CAAA,QAAA,CAAA,EAAA,WAAA,EAAA,IAAA,EAAA,IAAA,EAajB,gBAAgB,6CAfd,CAAA,wBAAA,CAA0B,EAAA,QAAA,EAAA,IAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAIzB,UAAU,EAAA,UAAA,EAAA,CAAA;kBANtB,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,aAAa;AACvB,oBAAA,QAAQ,EAAE,CAAA,wBAAA,CAA0B;oBACpC,eAAe,EAAE,uBAAuB,CAAC,MAAM;oBAC/C,SAAS,EAAE,CAAC,YAAY,CAAC;AAC1B,iBAAA;AAW8C,SAAA,CAAA,EAAA,cAAA,EAAA,MAAA,EAAA,EAAA,cAAA,EAAA,EAAA,IAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,MAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,QAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,UAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,KAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,MAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,QAAA,CAAA,EAAA,CAAA,EAAA,MAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,SAAA,EAAA,IAAA,EAAA,CAAA,QAAQ,EAAA,EAAA,GAAE;AACrD,4BAAA,IAAI,EAAE,gBAAgB;AACvB,yBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;AC1EH;;AAEG;;;;"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ng-json-render/core",
3
+ "version": "0.0.2",
4
+ "description": "Angular renderer for json-render — safely render AI-agent-generated & server-driven UIs from a JSON spec.",
5
+ "license": "MIT",
6
+ "peerDependencies": {
7
+ "@angular/core": ">=19.0.0 <23.0.0",
8
+ "@json-render/core": "^0.19.0"
9
+ },
10
+ "sideEffects": false,
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "module": "fesm2022/ng-json-render-core.mjs",
15
+ "typings": "types/ng-json-render-core.d.ts",
16
+ "exports": {
17
+ "./package.json": {
18
+ "default": "./package.json"
19
+ },
20
+ ".": {
21
+ "types": "./types/ng-json-render-core.d.ts",
22
+ "default": "./fesm2022/ng-json-render-core.mjs"
23
+ },
24
+ "./testing": {
25
+ "types": "./types/ng-json-render-core-testing.d.ts",
26
+ "default": "./fesm2022/ng-json-render-core-testing.mjs"
27
+ }
28
+ },
29
+ "type": "module",
30
+ "dependencies": {
31
+ "tslib": "^2.3.0"
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ # @ng-json-render/core/testing
2
+
3
+ Secondary entry point of `@ng-json-render/core`. It can be used by importing from `@ng-json-render/core/testing`.
@@ -0,0 +1,37 @@
1
+ import { ComponentFixture } from '@angular/core/testing';
2
+ import { JrRegistry, JrActionHandler, JrRenderer, JrActionEvent, Spec } from '@ng-json-render/core';
3
+
4
+ /** Options for {@link renderSpec}. */
5
+ interface RenderSpecOptions {
6
+ /** Registry mapping component types to Angular classes. */
7
+ registry: JrRegistry;
8
+ /** Optional action handlers, provided via `provideJsonRender`. */
9
+ actions?: Record<string, JrActionHandler>;
10
+ /** Optional initial state. */
11
+ state?: Record<string, unknown>;
12
+ }
13
+ /** Handle returned by {@link renderSpec}. */
14
+ interface RenderSpecResult {
15
+ readonly fixture: ComponentFixture<JrRenderer>;
16
+ /** Root DOM element of the mounted renderer. */
17
+ readonly host: HTMLElement;
18
+ /** Actions captured from the renderer's `(action)` output, in order. */
19
+ readonly actions: readonly JrActionEvent[];
20
+ /** Replace the spec and run change detection. */
21
+ setSpec(next: Spec): void;
22
+ /** Query the rendered DOM. */
23
+ query(selector: string): HTMLElement | null;
24
+ queryAll(selector: string): HTMLElement[];
25
+ }
26
+ /**
27
+ * Mount a {@link Spec} in a TestBed and return helpers for asserting the output.
28
+ *
29
+ * ```ts
30
+ * const r = renderSpec(spec, { registry });
31
+ * expect(r.query('button')?.textContent).toContain('Save');
32
+ * ```
33
+ */
34
+ declare function renderSpec(spec: Spec, options: RenderSpecOptions): RenderSpecResult;
35
+
36
+ export { renderSpec };
37
+ export type { RenderSpecOptions, RenderSpecResult };
@@ -0,0 +1,191 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { Type, InjectionToken, Signal, EnvironmentProviders } from '@angular/core';
3
+ import { UIElement, Spec, StateModel } from '@json-render/core';
4
+ export { Catalog, Schema, Spec, UIElement, compileSpecStream, createSpecStreamCompiler, defineCatalog, defineSchema, validateSpec } from '@json-render/core';
5
+
6
+ /**
7
+ * A registry maps catalog component `type` strings to the Angular component
8
+ * classes that render them. Built with {@link defineRegistry}.
9
+ */
10
+ interface JrRegistry {
11
+ readonly components: Readonly<Record<string, Type<unknown>>>;
12
+ }
13
+ /**
14
+ * Handler for an action raised by a component's `emit(event, payload)`.
15
+ * Registered via `provideJsonRender({ actions })`.
16
+ */
17
+ type JrActionHandler = (ctx: JrActionContext) => void | Promise<void>;
18
+ /** Context passed to a {@link JrActionHandler} when an action fires. */
19
+ interface JrActionContext {
20
+ /** The action name (event name, or the `on` binding's action). */
21
+ readonly action: string;
22
+ /** Payload supplied by the emitting component. */
23
+ readonly payload: unknown;
24
+ /** Id of the spec element that raised the action. */
25
+ readonly nodeId: string;
26
+ /** The element that raised the action. */
27
+ readonly element: UIElement;
28
+ }
29
+ /** An event surfaced on `JrRenderer`'s `(action)` output. */
30
+ interface JrActionEvent extends JrActionContext {
31
+ readonly spec: Spec;
32
+ }
33
+
34
+ /**
35
+ * Renders a json-render {@link Spec} into native Angular components.
36
+ *
37
+ * ```html
38
+ * <jr-renderer [spec]="spec()" [registry]="registry" (action)="onAction($event)" />
39
+ * ```
40
+ *
41
+ * The spec is a flat tree of typed elements; each `type` is resolved against the
42
+ * {@link JrRegistry} (from the `[registry]` input or `provideJsonRender`) and
43
+ * instantiated dynamically. Children are projected into each component's
44
+ * `<ng-content>`.
45
+ *
46
+ * Structural changes (the spec itself) rebuild the tree; state changes update
47
+ * individual nodes' inputs in place via per-node effects — so bound inputs stay
48
+ * live and keep focus. Props declaring `$bindState` two-way bind to a component's
49
+ * `model()` (Signal Forms `FormValueControl` / `FormCheckboxControl`).
50
+ */
51
+ declare class JrRenderer {
52
+ /** The spec to render. */
53
+ readonly spec: _angular_core.InputSignal<Spec | null>;
54
+ /** Registry mapping component types to Angular classes (overrides the DI default). */
55
+ readonly registry: _angular_core.InputSignal<JrRegistry | null>;
56
+ /** Initial/overriding runtime state merged on top of `spec.state`. */
57
+ readonly state: _angular_core.InputSignal<StateModel | undefined>;
58
+ /** Emits whenever a rendered component raises an action. */
59
+ readonly action: _angular_core.OutputEmitterRef<JrActionEvent>;
60
+ private readonly anchor;
61
+ private readonly store;
62
+ private readonly envInjector;
63
+ private readonly injector;
64
+ private readonly fallbackRegistry;
65
+ private readonly dispatcher;
66
+ /** Per-node effects (prop resolution + two-way bindings), torn down on rebuild. */
67
+ private readonly effects;
68
+ constructor();
69
+ private render;
70
+ private buildNode;
71
+ /** Reactively resolve and apply props; re-runs when referenced state changes. */
72
+ private bindProps;
73
+ /**
74
+ * Wire two-way `$bindState` props to a component's `model()` output. Works with
75
+ * any Signal Forms control (`value`/`checked` model), writing edits back to state.
76
+ */
77
+ private bindTwoWay;
78
+ private makeNodeInjector;
79
+ private isVisible;
80
+ private handleEmit;
81
+ private teardown;
82
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<JrRenderer, never>;
83
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<JrRenderer, "jr-renderer", never, { "spec": { "alias": "spec"; "required": false; "isSignal": true; }; "registry": { "alias": "registry"; "required": false; "isSignal": true; }; "state": { "alias": "state"; "required": false; "isSignal": true; }; }, { "action": "action"; }, never, never, true, never>;
84
+ }
85
+
86
+ /**
87
+ * Per-node context made available to every rendered catalog component via DI.
88
+ *
89
+ * A component injects it to raise events and to read its data-binding scope:
90
+ *
91
+ * ```ts
92
+ * @Component({ selector: 'jr-button', template: `<button (click)="ctx.emit('press')"><ng-content/></button>` })
93
+ * export class JrButton { protected ctx = inject(JR_CONTEXT); }
94
+ * ```
95
+ */
96
+ interface JrContext {
97
+ /** Id of the spec element this component instance renders. */
98
+ readonly nodeId: string;
99
+ /** The current `$item` when inside a `repeat` scope, otherwise `undefined`. */
100
+ readonly item: Signal<unknown>;
101
+ /** The current `$index` when inside a `repeat` scope, otherwise `-1`. */
102
+ readonly index: Signal<number>;
103
+ /** Raise a named event; routed to the element's `on` bindings / action handlers. */
104
+ emit(event: string, payload?: unknown): void;
105
+ }
106
+ /** Injection token providing the per-node {@link JrContext}. */
107
+ declare const JR_CONTEXT: InjectionToken<JrContext>;
108
+ /** Convenience helper to inject the current {@link JrContext} from a component. */
109
+ declare function injectJrContext(): JrContext;
110
+
111
+ /**
112
+ * Signal-based store that owns the current {@link Spec} and its runtime state.
113
+ *
114
+ * All calls into `@json-render/core` state utilities are funnelled through here
115
+ * so the rest of the engine depends on a single, stable surface. State changes
116
+ * from `@json-render/core`'s in-memory store are mirrored into an Angular signal
117
+ * (`state`) so templates and prop resolution stay reactive under zoneless CD.
118
+ */
119
+ declare class JrStateStore {
120
+ private readonly _spec;
121
+ /** The current spec, or `null` before one is set. */
122
+ readonly spec: Signal<Spec | null>;
123
+ private readonly _state;
124
+ /** The current runtime state model. */
125
+ readonly state: Signal<StateModel>;
126
+ private store;
127
+ private unsubscribe?;
128
+ /** Replace the spec and re-seed runtime state from `spec.state`. */
129
+ setSpec(spec: Spec): void;
130
+ /** Merge additional state on top of the current model. */
131
+ patchState(state: StateModel): void;
132
+ /** Look up an element by id in the current spec. */
133
+ getElement(id: string): UIElement | undefined;
134
+ /** Read a value from state by JSON Pointer path. */
135
+ getStatePath(path: string): unknown;
136
+ /** Write a value into state by JSON Pointer path (notifies subscribers). */
137
+ setStatePath(path: string, value: unknown): void;
138
+ /** Current full state snapshot (non-reactive read). */
139
+ snapshot(): StateModel;
140
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<JrStateStore, never>;
141
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<JrStateStore>;
142
+ }
143
+
144
+ /** DI token for a default {@link JrRegistry} provided via `provideJsonRender`. */
145
+ declare const JR_REGISTRY: InjectionToken<JrRegistry>;
146
+ /**
147
+ * Build a {@link JrRegistry} from a map of catalog `type` → Angular component class.
148
+ *
149
+ * ```ts
150
+ * const registry = defineRegistry({
151
+ * Stack: JrStack,
152
+ * Text: JrText,
153
+ * Button: JrButton,
154
+ * });
155
+ * ```
156
+ */
157
+ declare function defineRegistry(components: Record<string, Type<unknown>>): JrRegistry;
158
+ /** Merge multiple registries; later entries win on conflicting keys. */
159
+ declare function mergeRegistries(...registries: JrRegistry[]): JrRegistry;
160
+
161
+ /** A dispatcher routes emitted actions to their handlers. */
162
+ interface JrActionDispatcher {
163
+ dispatch(ctx: JrActionContext): void | Promise<void>;
164
+ }
165
+ /** DI token for the {@link JrActionDispatcher}. */
166
+ declare const JR_ACTION_DISPATCHER: InjectionToken<JrActionDispatcher>;
167
+ /** Options for {@link provideJsonRender}. */
168
+ interface ProvideJsonRenderOptions {
169
+ /** A default registry used by `JrRenderer` when no `[registry]` input is set. */
170
+ registry?: JrRegistry;
171
+ /** Map of action name → handler, invoked when a component emits that action. */
172
+ actions?: Record<string, JrActionHandler>;
173
+ }
174
+ /**
175
+ * Register json-render defaults for the application (or a route).
176
+ *
177
+ * ```ts
178
+ * export const appConfig: ApplicationConfig = {
179
+ * providers: [
180
+ * provideJsonRender({
181
+ * registry: primitivesRegistry,
182
+ * actions: { export_report: async () => { ... } },
183
+ * }),
184
+ * ],
185
+ * };
186
+ * ```
187
+ */
188
+ declare function provideJsonRender(options?: ProvideJsonRenderOptions): EnvironmentProviders;
189
+
190
+ export { JR_ACTION_DISPATCHER, JR_CONTEXT, JR_REGISTRY, JrRenderer, JrStateStore, defineRegistry, injectJrContext, mergeRegistries, provideJsonRender };
191
+ export type { JrActionContext, JrActionDispatcher, JrActionEvent, JrActionHandler, JrContext, JrRegistry, ProvideJsonRenderOptions };