ngx-json-render 0.4.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-json-render",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Angular renderer for json-render — stream AI-generated JSON specs into real Angular components with signals.",
5
5
  "keywords": [
6
6
  "angular",
@@ -1,7 +1,7 @@
1
1
  import { Spec, StateStore, ActionHandler, ComputedFunction, ValidationFunction, DirectiveDefinition } from '@json-render/core';
2
- import { Type, Provider } from '@angular/core';
2
+ import { Provider, Type } from '@angular/core';
3
3
  import { ComponentFixture } from '@angular/core/testing';
4
- import { ComponentRegistry, StateModel, RegistryEntry, JsonRenderer, JsonRenderStateService, JsonRenderValidationService, StateChange } from 'ngx-json-render';
4
+ import { RenderContext, ComponentRegistry, StateModel, RegistryEntry, JsonRenderer, JsonRenderStateService, JsonRenderValidationService, StateChange } from 'ngx-json-render';
5
5
 
6
6
  /**
7
7
  * The lines of one recorded response body, in the order the server sent them,
@@ -85,6 +85,111 @@ declare function usageLine(usage: {
85
85
  totalTokens?: number;
86
86
  }): string;
87
87
 
88
+ /** The DOM half of a harness. */
89
+ interface DomQueries {
90
+ /** Trimmed text of the first match, or of everything when given nothing. */
91
+ text(selector?: string): string;
92
+ /** Trimmed text of every match. */
93
+ texts(selector: string): string[];
94
+ /** The first match. Throws when there is none. */
95
+ find<T extends HTMLElement>(selector: string): T;
96
+ /** Every match, possibly none. */
97
+ findAll<T extends HTMLElement>(selector: string): T[];
98
+ /** Click an element (or the first match of a selector) and settle. */
99
+ click(target: string | Element): Promise<void>;
100
+ /** Set an input's value, fire `input`, and settle. */
101
+ fill(target: string | Element, value: string): Promise<void>;
102
+ }
103
+
104
+ /** One `setBound` call that reached a bound prop, in order. */
105
+ interface BoundWrite {
106
+ prop: string;
107
+ value: unknown;
108
+ }
109
+ /** Options for {@link renderComponent}. Everything is optional. */
110
+ interface RenderComponentOptions<P extends object> {
111
+ /** The resolved props the component sees. Defaults to `{}`. */
112
+ props?: P;
113
+ /**
114
+ * Which props are two-way bound, as `prop → state path` — what
115
+ * `$bindState` / `$bindItem` would have resolved to. A `setBound` on a prop
116
+ * listed here is recorded and written back into `props`; on any other prop
117
+ * it is a no-op, exactly as under the renderer.
118
+ */
119
+ bindings?: Record<string, string>;
120
+ /**
121
+ * The events the spec would bind in `on`. Only affects what `ctx.on(event)`
122
+ * reports as `bound` — `emitted` records every event regardless.
123
+ */
124
+ on?: readonly string[];
125
+ /** The loading flag the component sees. */
126
+ loading?: boolean;
127
+ /** The spec key, for components that `injectElementKey()`. */
128
+ key?: string;
129
+ /** The element's catalog type name. */
130
+ type?: string;
131
+ /** Extra TestBed providers for anything else the component injects. */
132
+ providers?: Provider[];
133
+ }
134
+ /**
135
+ * A catalog component mounted on its own, and what it said to the renderer
136
+ * it thinks it is inside.
137
+ */
138
+ interface ComponentHarness<T, P extends object> extends DomQueries {
139
+ /** The fixture of the host the component is mounted in. */
140
+ readonly fixture: ComponentFixture<unknown>;
141
+ /** The host element, the root of the rendered DOM. */
142
+ readonly element: HTMLElement;
143
+ /** The component instance. */
144
+ readonly component: T;
145
+ /** The render context the component injected. */
146
+ readonly context: RenderContext<P>;
147
+ /** Every event the component emitted, bound or not, in order. */
148
+ readonly emitted: readonly string[];
149
+ /** Every `setBound` that reached a bound prop, in order. */
150
+ readonly writes: readonly BoundWrite[];
151
+ /** The props as the component currently sees them. */
152
+ props(): P;
153
+ /** Replace the props and settle. */
154
+ setProps(props: P): Promise<void>;
155
+ /** Merge into the props and settle. */
156
+ patchProps(patch: Partial<P>): Promise<void>;
157
+ /** Flip the loading flag and settle. */
158
+ setLoading(loading: boolean): Promise<void>;
159
+ /** Let effects, promises and change detection finish. */
160
+ settle(): Promise<void>;
161
+ /** Destroy the fixture. Rarely needed — TestBed resets between tests. */
162
+ destroy(): void;
163
+ }
164
+ /**
165
+ * Mount a catalog component with no renderer and no spec: hand it props, and
166
+ * read back the events it emitted and the values it wrote to bound props.
167
+ *
168
+ * This is the test of a component as a presentational one — what it draws
169
+ * for these props, and what it says when used. What the spec then does with
170
+ * that is the renderer's business, and `renderSpec` is where to test it.
171
+ *
172
+ * `props` is not inferred from the first value, so a later `patchProps` can
173
+ * set a prop the first render left out. Pass `P` to have the props typed.
174
+ *
175
+ * A component that renders `<jr-children>`, registers field validation or
176
+ * dispatches actions directly needs the renderer's services and fails with a
177
+ * message pointing at `renderSpec`.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const button = await renderComponent(MyButton, { props: { label: 'Save' } });
182
+ *
183
+ * expect(button.text('button')).toBe('Save');
184
+ * await button.click('button');
185
+ * expect(button.emitted).toEqual(['press']);
186
+ *
187
+ * await button.patchProps({ disabled: true });
188
+ * expect(button.find<HTMLButtonElement>('button').disabled).toBe(true);
189
+ * ```
190
+ */
191
+ declare function renderComponent<T, P extends object = Record<string, unknown>>(component: Type<T>, options?: RenderComponentOptions<NoInfer<P>>): Promise<ComponentHarness<T, P>>;
192
+
88
193
  /** One action the spec asked for, in the order it was dispatched. */
89
194
  interface DispatchedAction {
90
195
  name: string;
@@ -123,7 +228,7 @@ interface RenderSpecOptions {
123
228
  * `fixture` and `element` are there for everything this does not cover — the
124
229
  * harness is a shortcut, not a wall.
125
230
  */
126
- interface SpecHarness {
231
+ interface SpecHarness extends DomQueries {
127
232
  /** The fixture of the host that owns the `<json-render>`. */
128
233
  readonly fixture: ComponentFixture<unknown>;
129
234
  /** The host element, the root of the rendered DOM. */
@@ -150,18 +255,6 @@ interface SpecHarness {
150
255
  write(path: string, value: unknown): Promise<void>;
151
256
  /** The whole state model. */
152
257
  state(): StateModel;
153
- /** Trimmed text of the first match, or of everything when given nothing. */
154
- text(selector?: string): string;
155
- /** Trimmed text of every match. */
156
- texts(selector: string): string[];
157
- /** The first match. Throws when there is none. */
158
- find<T extends HTMLElement>(selector: string): T;
159
- /** Every match, possibly none. */
160
- findAll<T extends HTMLElement>(selector: string): T[];
161
- /** Click an element (or the first match of a selector) and settle. */
162
- click(target: string | Element): Promise<void>;
163
- /** Set an input's value, fire `input`, and settle. */
164
- fill(target: string | Element, value: string): Promise<void>;
165
258
  /** Destroy the fixture. Rarely needed — TestBed resets between tests. */
166
259
  destroy(): void;
167
260
  }
@@ -193,5 +286,5 @@ interface SpecHarness {
193
286
  */
194
287
  declare function renderSpec(spec: Spec | null, options: RenderSpecOptions): Promise<SpecHarness>;
195
288
 
196
- export { recordedTransport, renderSpec, specStream, usageLine };
197
- export type { DispatchedAction, RecordedFailure, RecordedTransportOptions, Recording, Recordings, RenderSpecOptions, SpecHarness };
289
+ export { recordedTransport, renderComponent, renderSpec, specStream, usageLine };
290
+ export type { BoundWrite, ComponentHarness, DispatchedAction, RecordedFailure, RecordedTransportOptions, Recording, Recordings, RenderComponentOptions, RenderSpecOptions, SpecHarness };
@@ -604,7 +604,22 @@ declare class JrElement {
604
604
  /** Prop/visibility resolution context (state + repeat scope + extensions). */
605
605
  private readonly resolutionCtx;
606
606
  protected readonly visible: Signal<boolean>;
607
- /** The element with all prop expressions resolved. */
607
+ /**
608
+ * The element with all prop expressions resolved.
609
+ *
610
+ * Resolving builds a new props object on every state write, whatever the
611
+ * write touched. Handing that on as a change would re-run every catalog
612
+ * template in the tree for one keystroke, so a result that holds the same
613
+ * values as the last one counts as unchanged and keeps the old object.
614
+ *
615
+ * Two-way bound elements are the exception, and keep treating every
616
+ * resolution as a change. Their component writes the user's input into the
617
+ * DOM before it reaches state, so the DOM can differ from a prop whose
618
+ * value never did: typing and a `clearStatePath` in the same turn take
619
+ * the prop from '' through 'milk' back to '' with nobody reading 'milk'.
620
+ * The README tells catalog authors to sync such inputs with an effect on
621
+ * props, and that effect only gets to put the DOM right if it runs.
622
+ */
608
623
  readonly resolvedElement: Signal<UIElement<string, Record<string, unknown>> | undefined>;
609
624
  /** Two-way binding paths ($bindState / $bindItem) by prop name. */
610
625
  readonly bindings: Signal<Record<string, string> | undefined>;
@@ -643,6 +658,12 @@ declare class JrElement {
643
658
  */
644
659
  private fireEvent;
645
660
  private emitEvent;
661
+ /**
662
+ * Run an `on` or `watch` binding's actions in order. Params are resolved
663
+ * per action, just before it runs, so a `$state` read in a later action of
664
+ * the chain sees what an earlier one wrote.
665
+ */
666
+ private dispatch;
646
667
  private eventHandle;
647
668
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<JrElement, never>;
648
669
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<JrElement, "jr-element", never, { "elementKey": { "alias": "elementKey"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;