Repository navigation
Releases: shteynu/ngx-json-render
Release list
v0.8.0
Two new entry points: build an MCP App in Angular from npm, and load a catalog on a server without Angular.
ngx-json-render/mcp is the Angular side of an MCP App, a tool result that Claude, ChatGPT or VS Code renders as an inline view. injectJsonRenderApp() connects to the host and keeps the spec the model passed to the tool in a signal. Until now it lived only in this repository's example.
readonly mcp = injectJsonRenderApp({ name: 'my-app', version: '1.0.0' });
// <json-render [spec]="mcp.spec()" [loading]="mcp.loading()" [registry]="registry" />The spec renders while the model is still writing the tool call. mcp.sendMessage(text, data) posts a user message to the chat, so a button can continue the conversation. mcp.callServerTool(name, args) replaces the spec with another tool's result. See Inside an MCP App host.
ngx-json-render/schema holds the spec grammar with nothing but @json-render/core behind it. A catalog.ts that imports schema from it can be loaded by a plain Node server, tsx or an edge function, with no import '@angular/compiler' workaround.
// catalog.ts
import { schema } from 'ngx-json-render/schema';What changes for your app
- Nothing breaks.
ngx-json-renderstill exports the sameschemaobject, so existing catalogs keep working. @modelcontextprotocol/ext-appsand@modelcontextprotocol/sdkare new optional peers. Install them only if you importngx-json-render/mcp;ng adddoes not add them.
Upgrading
npm i ngx-json-render@0.8.0 ngx-json-render-material@0.3.10Install both: the catalog's peer range moves to ^0.8.0, and npm resolves a mismatched pair by quietly picking an older renderer.
Full Changelog: v0.7.8...v0.8.0
v0.7.8
A spec that arrives again, or a form full of bound fields, now costs only what actually changed.
Two cases still made the renderer redo work across the whole tree. A spec handed over as all-new objects (a whole-spec part, a host that fetches the spec again, buildSpecFromParts replaying a message) made every element resolve its props, and every container re-render. And every element with a $bindState or $bindItem prop resolved and re-rendered on every state write, whatever path the write touched. On a tree of 1,001 elements, an identical spec now resolves nothing instead of 1,000 elements, and one keystroke in a 500-field form re-renders one field instead of 500.
What changes for your app
- Spec elements compare by content. An element whose content did not change keeps its resolved props and its template does not re-run, however the spec object was built. Streamed patches already shared unchanged elements and behave as before.
spec.stateno longer overwrites the user's edits when the same spec arrives again. In uncontrolled mode, only leaves whose content changed are written to the store. Before, arrays compared by reference, so re-sending an unchanged spec wrote every array back and discarded edits made inside it, such as text typed into a field bound with$bindItem. A spec whose state really changed still writes the new value.- Two-way bound elements follow only their own paths. They resolve when a path they read is written, like any other element. A write that touches the bound path (the path itself, a container above it, or a path inside it) still always reaches the component, so the README's input pattern keeps putting the DOM right when the value returns to what it was.
Upgrading
npm i ngx-json-render@0.7.8 — no API changes, and ngx-json-render-material@^0.3.8 works with it unchanged. If an app relied on re-sending the same spec to reset its state, write that state explicitly instead, for example with a setState action.
Full Changelog: v0.7.7...v0.7.8
material-v0.3.9
IconButton shows its label as a tooltip. An icon-only button already used label as its accessible name, but sighted mouse users couldn't see it, so a generated toolbar of icons was guesswork. The label now also appears as a Material tooltip on hover and focus. The accessible name is unchanged, and screen readers still read the label once: the CDK adds no aria-describedby when the tooltip repeats the aria-label. The catalog's description of IconButton now mentions the tooltip.
Thanks to @vns-agm for this one (#4), the catalog's first outside contribution. A disabled IconButton doesn't show the tooltip yet. That is #5, open as a good first issue.
Upgrading
npm i ngx-json-render-material@^0.3.9No code changes are needed. It works with ngx-json-render 0.7.x.
Full Changelog: material-v0.3.8...material-v0.3.9
material-v0.3.10
The catalog now loads on a server without Angular, and a disabled IconButton shows its tooltip.
ngx-json-render-material/catalog: the catalog without the components. A Node server can describe the vocabulary to a model withmaterialCatalog.prompt()and check what comes back withmaterialCatalog.validate(spec), without loading Angular. The MCP App example's server now imports it this way.- A disabled
IconButtonkeeps its tooltip. It now renders with Material'sdisabledInteractive, so it stays hoverable and focusable, shows the disabled styling and getsaria-disabled="true". A click on it emits nothing.
Thanks to @knownshah for the IconButton fix (#6), the catalog's second outside contribution. Two more good first issues are open: #7 does the same for the plain Button, and #8 is a small check in the MCP App server.
import { materialCatalog } from 'ngx-json-render-material/catalog';Upgrading
npm i ngx-json-render@0.8.0 ngx-json-render-material@0.3.10Requires ngx-json-render@^0.8.0, so install the pair together. No code changes are needed. Full notes: v0.8.0.
Full Changelog: material-v0.3.9...material-v0.3.10
material-v0.3.8
Two layout fixes found by running the catalog inside ChatGPT. Both changed what a generated UI looked like without anything in the spec being wrong.
Textkeeps line breaks. A\nincontentnow starts a new line (white-space: pre-line); before, the browser folded it into a space, so a poem, an address or a short list arrived as one run-on line. Runs of spaces still collapse as before. The catalog's description ofTextnow tells the model that\nstarts a new line.Cardwithout actions has no empty footer. Theactionsrow is rendered only when the element'sactionsslot has children. Material gives that row a minimum height, so every card without buttons ended in a blank strip about 50 px tall.
Upgrading
npm i ngx-json-render-material@^0.3.8No code changes are needed. Two things to check: Text content that contains \n now renders on several lines, and a Card with no actions slot no longer has a mat-card-actions element, which matters only if your styles or tests look for it. It works with ngx-json-render 0.7.x.
Full Changelog: material-v0.3.7...material-v0.3.8
v0.7.7
Type a component's props from its catalog entry. ngx-json-render now re-exports core's InferComponentProps, InferCatalogComponents and InferActionParams, so the Zod schema in the catalog is the one place a component's props are declared.
import type { InferComponentProps } from 'ngx-json-render';
import type { catalog } from './catalog';
export class Card {
readonly ctx = injectRenderContext<InferComponentProps<typeof catalog, 'Card'>>();
}A field with a Zod .default() comes out required in the inferred type, but the renderer applies no defaults: the component still gets undefined when the spec leaves the field out. Wrap the type in Partial<…> or handle the missing value.
Upgrading
npm i ngx-json-render@^0.7.7Types only, no runtime change. ngx-json-render-material@0.3.7 works with it.
Full Changelog: v0.7.6...v0.7.7
v0.7.6
State keys that contain / or ~ now stay one key. JSON Pointer writes such a key as ~1 or ~0, and two places in the renderer did not: a key like "a/b" was split into a nested { "a": { "b": … } }, and validation could not find a field whose path used the escape.
What changes for your app
- Seeding keeps the key you wrote. State from the
stateinput orspec.stateis written into the store key by key. A key containing/or~used to land at the wrong place, so{ "links": { "a/b": "x" } }became{ "links": { "a": { "b": "x" } } }, and{ "$state": "/links/a~1b" }read nothing. It now stays"a/b". createStoreSetStatewrites to the same key. It had the same split when it diffed a whole-state update into path writes.- Validation finds escaped paths. A field bound to
/links/a~1bis now checked against the value at"a/b", the same way the state store reads it. Before, it was always checked againstundefined, sorequiredfailed on a filled field.
State whose keys contain neither character behaves exactly as before.
Upgrading
npm i ngx-json-render@^0.7.6No code changes are needed, and ngx-json-render-material@0.3.6 works with it as it is.
Full Changelog: v0.7.5...v0.7.6
v0.7.5
Two fixes where an action or a watcher silently never happened. Neither changes the API; both change what your app sees in a case that used to fail quietly.
What changes for your app
- A second
confirmno longer strands the first. When an action with aconfirmwas dispatched while another confirmation was still open (from awatch, or a programmaticinjectActions().execute()), it replaced the open one, and the firstexecute()never settled: noonSuccess, noonError, no settled action observer. Now only one confirmation is open at a time, and a new one cancels the waiting one, so itsexecute()rejects with the usual cancellation (isActionCancelled(error)is true). A late answer to the replaced dialog no longer closes its successor. watchfires for writes made on an external store. In controlled mode (thestoreinput),watchonly heard about writes made through the renderer. A write from your own state management re-rendered the UI but triggered no watcher. On every store notification the renderer now compares the new snapshot with the last one, by reference, and reports each changed path, containers included (/useras well as/user/name). Writes made through the renderer are still reported once, not twice.
Upgrading
npm i ngx-json-render@^0.7.5No code changes are needed, and ngx-json-render-material@0.3.6 works with it as it is. If you use an external store, check that it replaces objects on write rather than editing them in place: a store that mutates its snapshot still re-renders, but its changes cannot be seen by the comparison and trigger no watch.
Full Changelog: v0.7.4...v0.7.5
material-v0.3.7
Component props are typed from the catalog's Zod schemas. The 25 Material components used hand-written prop interfaces that had to be kept in step with the schemas by hand; they now read their types from the schemas, so the two can no longer drift.
The new MaterialProps<'Card'> export is that type for one component: its schema's props with every field optional, because the renderer hands props over as the spec wrote them and checks them only when validate is on. Use it to type a replacement component:
import { type MaterialProps, materialComponents } from 'ngx-json-render-material';
export class BrandedCard {
readonly ctx = injectRenderContext<MaterialProps<'Card'>>();
}
const registry = defineRegistry(materialCatalog, {
components: { ...materialComponents, Card: BrandedCard },
actions: {},
});Upgrading
npm i ngx-json-render-material@^0.3.7No code changes are needed; the components render exactly as before. It works with ngx-json-render 0.7.x.
Full Changelog: material-v0.3.6...material-v0.3.7