Concorde functional component — SDUI. sonic-sdui builds an interface from a JSON descriptor (server-driven UI). Pass the descriptor through props, or add fetch and an endpoint to load it like Fetch. It renders in the light DOM with displa Doc ID: core/components/functional/sdui/sdui. Keywords: Concorde, supersoniks, core/components/functional/sdui/sdui, sdui, SDUI, functional component, sonic-sdui, web component, props, fetch, display: contents, nodes, tagName, div, attributes, textContent. URL: https://concorde.supersoniks.org/core/components/functional/sdui.
SDUI
sonic-sdui builds an interface from a JSON descriptor (server-driven UI). Pass the descriptor through props, or add fetch and an endpoint to load it like Fetch. It renders in the light DOM with display: contents, so an empty instance does not affect layout.
Basic example
Each entry of nodes becomes an element: tagName (default div) · attributes · textContent · nested nodes.
<sonic-sdui
dataProvider="sdui-basic-example"
props='{
"nodes":[{
"tagName":"div",
"attributes":{ "class":"flex items-center gap-2" },
"nodes":[
{ "tagName":"strong", "textContent":"CN 001 · CDG → JFK" },
{ "tagName":"sonic-badge", "attributes":{ "type":"warning" }, "textContent":"Delayed" }
]
}]
}'
></sonic-sdui>
Fetching a descriptor
Fetching is off by default: add the boolean fetch attribute, then configure serviceURL / endPoint / dataProvider as on Fetch. Invalidating the publisher or changing the endpoint reloads the interface.
<sonic-sdui fetch dataProvider="sdui-fetch-example" endPoint="/src/core/components/functional/sdui/example.json"></sonic-sdui>
HTML content
markup builds the node from an HTML string, innerHTML appends HTML inside the element, and prefix / suffix wrap it. Prefer textContent for anything coming from user data.
<sonic-sdui
dataProvider="sdui-html-example"
props='{
"nodes":[
{ "markup":"Cruising at <sonic-badge>Mach 2.04</sonic-badge>" },
{ "tagName":"p", "prefix":"<hr>", "innerHTML":"Altitude: <b>60,000 ft</b>" }
]
}'
></sonic-sdui>
Library
A node with libraryKey uses a library entry as its model, then adds its own attributes and children. In the model, contentElementSelector sets where those children go. The descriptor library is merged over a default library of form fields (textfield · email · password · date · select · checkbox · radio · textarea · button · submit · fieldset…, see default-library.json). The library attribute replaces it with a JSON file loaded from a URL.
<sonic-sdui
dataProvider="sdui-library-example"
props='{
"library":{
"flightCard":{
"tagName":"sonic-card",
"attributes":{ "class":"block max-w-sm" },
"nodes":[{ "tagName":"div", "attributes":{ "class":"text-sm" } }],
"contentElementSelector":"div"
}
},
"nodes":[
{ "libraryKey":"flightCard", "nodes":[{ "tagName":"span", "textContent":"CN 001 · Gate C4" }] },
{ "libraryKey":"email", "attributes":{ "label":"Passenger email", "name":"email", "class":"block mt-4 max-w-sm" } }
]
}'
></sonic-sdui>
Child placement
A child with parentElementSelector is appended to the first element matching that selector inside its parent, instead of the parent itself. It has no effect on top-level nodes.
<sonic-sdui
dataProvider="sdui-parent-selector-example"
props='{
"nodes":[{
"nodes":[
{ "tagName":"strong", "textContent":"Departures" },
{ "tagName":"ul" },
{ "tagName":"li", "parentElementSelector":"ul", "textContent":"CN 001 · CDG → JFK" },
{ "tagName":"li", "parentElementSelector":"ul", "textContent":"CN 003 · LHR → JFK" }
]
}]
}'
></sonic-sdui>
Actions
A node with action: { name, context? } makes a click on its element dispatch an sdui-action event from sonic-sdui. detail is { name, context, sourceNodeId? }, where sourceNodeId is the node nodeId. The event bubbles and is composed, so a page shell can listen once for every node. Form posting stays with Submit.
<sonic-sdui
id="sdui-action-example"
dataProvider="sdui-action-example"
props='{
"nodes":[{
"tagName":"sonic-button",
"nodeId":"check-in",
"textContent":"Check in",
"action":{ "name":"check-in", "context":{ "flight":"CN 001" } }
}]
}'
></sonic-sdui>
document.querySelector("#sdui-action-example")?.addEventListener("sdui-action", (e) => {
const { name, context, sourceNodeId } = (e as CustomEvent).detail;
});
Safe profile
With profile="safe", the descriptor cannot inject HTML or scripts: js URLs are not loaded, and nodes using markup / innerHTML / prefix / suffix (directly or through their library model) are skipped with a console.warn. textContent, attributes and actions still work. Without the attribute, everything is rendered.
<sonic-sdui
profile="safe"
dataProvider="sdui-safe-example"
props='{
"nodes":[
{ "tagName":"p", "textContent":"Gate C4 · Boarding at 10:40" },
{ "innerHTML":"<b>Skipped: HTML is refused</b>" }
]
}'
></sonic-sdui>
To check a descriptor before rendering it, validateSafeDescriptor(descriptor) from @supersoniks/concorde/sdui-safe returns the violations as { path, field, message }[] without side effects.
Partial rendering
Declare nodes once in nodeById and reference them with childNodeIds. With the boolean partial, a missing id renders an empty placeholder (data-sdui-placeholder · aria-hidden · display: contents) so the node can arrive later. Without it, the missing child is skipped with a console.warn.
<sonic-sdui
partial
dataProvider="sdui-partial-example"
props='{
"nodes":[{ "tagName":"div", "childNodeIds":["flight", "gate"] }],
"nodeById":{
"flight":{ "tagName":"strong", "textContent":"CN 001 · CDG → JFK" }
}
}'
></sonic-sdui>
Incremental updates
upsertNode · removeNode · setData patch the rendered interface without a full rebuild, so sibling nodes keep their state (focus, input values). A node needs a nodeId to be updated. The first operation switches the component to incremental mode, where props changes are ignored until endIncremental(). To drive it from data, set opsDataProvider: each value assigned to that publisher (SDUIOp or SDUIOp[]) goes through applyOp.
<sonic-sdui
id="sdui-ops-example"
dataProvider="sdui-ops-example"
props='{
"nodes":[
{ "nodeId":"cn001", "tagName":"p", "textContent":"CN 001 · CDG → JFK · On time" },
{ "nodeId":"cn003", "tagName":"p", "textContent":"CN 003 · LHR → JFK · Boarding" }
]
}'
></sonic-sdui>
<sonic-button size="sm" onclick="document.getElementById('sdui-ops-example').upsertNode({ nodeId: 'cn001', tagName: 'p', textContent: 'CN 001 · CDG → JFK · Delayed 20 min' })">Delay CN 001</sonic-button>
<sonic-button size="sm" variant="outline" onclick="document.getElementById('sdui-ops-example').removeNode('cn003')">Remove CN 003</sonic-button>
Transformations
The transformation attribute points to a JSON file applied to the descriptor before rendering. Its library is merged into the descriptor library; each entry of transforms runs an action on the nodes matching patterns.
{
"transforms": [
{ "action": "remap", "patterns": [{ "libraryKey": "textfield" }], "ui": { "tagName": "sonic-input", "attributes": { "size": "lg" } } },
{ "action": "insert", "after": { "libraryKey": "email" }, "ui": { "tagName": "p", "textContent": "Boarding pass sent by email" } }
]
}
| Action | Effect |
|---|---|
remap |
Replaces each matched node with ui, keeping its attributes, content and children unless ui redefines them |
unwrap |
Replaces each matched node with its children |
wrap |
Groups the matched nodes into the ui node |
delete |
Removes the matched nodes |
insert |
Inserts ui relative to the node matched by before · after · in |
move |
Moves the matched nodes next to the node matched by before or after |
Wrapped descriptor
When the descriptor is one key of a larger payload, set sduiKey to that key. Set messagesKey to render the toast messages found at that key with sonic-toast-message-subscriber ({ content, status, type } items).
<sonic-sdui
sduiKey="ui"
dataProvider="sdui-key-example"
props='{
"flight":"CN 001",
"ui":{ "nodes":[{ "tagName":"p", "textContent":"Weather warning: turbulence expected over the Atlantic" }] }
}'
></sonic-sdui>
Component catalog
The package ships a JSON catalog of every custom element (tag; props with attribute / type / allowed values / default; slots and events), generated at build time for descriptor authors and tools. It has no runtime cost. Elements with thin annotations are listed in component-catalog.gaps.json.
import catalog from "@supersoniks/concorde/sdui/component-catalog.json";
Playground
Edit the descriptor and leave the field to render it.
<sonic-textarea
rows="8"
onChange="SonicPublisherManager.get('sdui-playground').set(JSON.parse(this.value))"
value='{"nodes":[{"tagName":"sonic-badge", "attributes":{"type":"success"}, "textContent":"CN 001 landed"}]}'
></sonic-textarea>
<sonic-sdui dataProvider="sdui-playground" props='{"nodes":[{"tagName":"sonic-badge", "attributes":{"type":"success"}, "textContent":"CN 001 landed"}]}'></sonic-sdui>
API reference
| Attribute | Values | Description |
|---|---|---|
dataProvider |
string | Publisher holding the descriptor. Required |
props |
SDUIDescriptor (JSON) |
Descriptor to render |
fetch |
boolean | Loads the descriptor from serviceURL + endPoint (or dataProvider). Other request attributes as on Fetch |
sduiKey |
string | Reads the descriptor from props[sduiKey] |
messagesKey |
string | Path in props of the toast messages to display |
profile |
safe |
Refuses js / markup / innerHTML / prefix / suffix. Unset by default |
partial |
boolean | Renders a placeholder for missing childNodeIds |
library |
URL | JSON library that replaces the descriptor library |
transformation |
URL | JSON transform description applied before rendering |
opsDataProvider |
string | Publisher whose assigned values are passed to applyOp |
| Descriptor key | Description |
|---|---|
nodes |
Root nodes |
nodeById |
Flat registry of nodes for childNodeIds |
library |
Node models by key, merged over the default library |
js / css |
Script and stylesheet URLs to load (js refused in safe profile) |
| Node field | Description |
|---|---|
tagName |
Element tag (default div) |
attributes |
Attribute name → value (objects are serialized to JSON) |
nodes |
Child nodes |
childNodeIds |
Child nodeIds resolved from nodeById or the tree |
textContent |
Plain text. Ignored when the node has children |
markup · innerHTML · prefix · suffix |
HTML strings (refused in safe profile) |
libraryKey |
Library entry used as the model |
contentElementSelector |
In a library model: where children are appended |
parentElementSelector |
Appends the node to a matching element of its parent |
nodeId |
Logical id for actions, childNodeIds and incremental updates (rendered as data-sdui-node-id) |
action |
{ name, context? } dispatched as sdui-action on click |
| Method | Description |
|---|---|
upsertNode(node, parentId?, index?) |
Adds or replaces a node by nodeId |
removeNode(nodeId) |
Removes a node |
setData(path, value) |
Sets a publisher path (form.email), absolute or relative to dataProvider |
applyOp(op) |
Runs an SDUIOp: upsertNode · removeNode · setData |
endIncremental() |
Leaves incremental mode; the next props change rebuilds everything |
| Event | Detail |
|---|---|
sdui-action |
{ name, context, sourceNodeId? } |
No slots.