|
| 1 | +# Semantic DOM Debugging |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +You are looking at a rendered Intent screen in the browser and want to know which semantic node each DOM element corresponds to. The DOM class names and IDs are internal renderer details, not product semantics. |
| 6 | + |
| 7 | +## Solution |
| 8 | + |
| 9 | +Pass `showSemanticIds: true` to `renderDom()` or `renderRouter()`. The renderer adds `data-intent-*` attributes to key DOM elements, mapping them to the same semantic IDs produced by `inspectScreen()`. |
| 10 | + |
| 11 | +## Data attributes produced |
| 12 | + |
| 13 | +| Attribute | On element | Value example | |
| 14 | +|-----------|-----------|--------------| |
| 15 | +| `data-intent-screen` | `<main>` | `screen:invite-member` | |
| 16 | +| `data-intent-ask` | `<label>`, `<input>` | `ask:email` | |
| 17 | +| `data-intent-action` | `<button>` | `action:invite-member` | |
| 18 | + |
| 19 | +The attribute values are the same `semanticId` strings documented in the [Inspect Screen and Diagnostics Guide](Inspect-Screen.md). |
| 20 | + |
| 21 | +## Usage |
| 22 | + |
| 23 | +### With `renderDom()` |
| 24 | + |
| 25 | +```ts |
| 26 | +import { renderDom } from "@intent-framework/dom" |
| 27 | + |
| 28 | +const cleanup = renderDom(MyScreen, { |
| 29 | + target: document.getElementById("root")!, |
| 30 | + showSemanticIds: true, |
| 31 | +}) |
| 32 | +``` |
| 33 | + |
| 34 | +### With `renderRouter()` |
| 35 | + |
| 36 | +```ts |
| 37 | +import { renderRouter } from "@intent-framework/dom" |
| 38 | + |
| 39 | +const handle = renderRouter(myRouter, { |
| 40 | + target: document.getElementById("root")!, |
| 41 | + showSemanticIds: true, |
| 42 | +}) |
| 43 | +``` |
| 44 | + |
| 45 | +### Opt-in only |
| 46 | + |
| 47 | +By default, no `data-intent-*` attributes are emitted. Your production output is unaffected. Enable the flag only during development or in debug builds. |
| 48 | + |
| 49 | +## Debugging workflow |
| 50 | + |
| 51 | +### 1. Enable semantic IDs |
| 52 | + |
| 53 | +```ts |
| 54 | +renderDom(MyScreen, { |
| 55 | + target: document.getElementById("root")!, |
| 56 | + showSemanticIds: import.meta.env.DEV, // only in dev |
| 57 | +}) |
| 58 | +``` |
| 59 | + |
| 60 | +### 2. Inspect in browser DevTools |
| 61 | + |
| 62 | +Open the Elements panel. Select any rendered label, input, or button. The `data-intent-ask` or `data-intent-action` attribute tells you which semantic node it belongs to. |
| 63 | + |
| 64 | +Example — inspecting a rendered email input: |
| 65 | + |
| 66 | +```html |
| 67 | +<main data-intent-screen="screen:invite-member"> |
| 68 | + <form method="POST" novalidate> |
| 69 | + <div class="ask-group"> |
| 70 | + <label for="ask_email" data-intent-ask="ask:email">Email</label> |
| 71 | + <input id="ask_email" name="ask_email" type="email" required autocomplete="email" data-intent-ask="ask:email"> |
| 72 | + </div> |
| 73 | + <button id="act_invite_member" type="button" data-intent-action="action:invite-member">Invite member</button> |
| 74 | + <output id="feedback-output" aria-live="polite"></output> |
| 75 | + </form> |
| 76 | +</main> |
| 77 | +``` |
| 78 | + |
| 79 | +### 3. Cross-reference with `inspectScreen()` |
| 80 | + |
| 81 | +Open the console and run: |
| 82 | + |
| 83 | +```js |
| 84 | +import { inspectScreen } from "@intent-framework/core" |
| 85 | +console.log(inspectScreen(MyScreen)) |
| 86 | +``` |
| 87 | + |
| 88 | +The `semanticId` values in the console output match the `data-intent-*` attribute values in the Elements panel. |
| 89 | + |
| 90 | +### 4. Use in tests |
| 91 | + |
| 92 | +Query elements by their semantic attributes in browser tests: |
| 93 | + |
| 94 | +```ts |
| 95 | +const emailInput = document.querySelector('[data-intent-ask="ask:email"]') |
| 96 | +const inviteButton = document.querySelector('[data-intent-action="action:invite-member"]') |
| 97 | +const screen = document.querySelector('[data-intent-screen="screen:invite-member"]') |
| 98 | +``` |
| 99 | + |
| 100 | +This is more resilient than querying by generated class names or internal IDs, and expresses the test's intent at the semantic level. |
| 101 | + |
| 102 | +## How the mapping works |
| 103 | + |
| 104 | +When `showSemanticIds: true` is set, `renderDom()` calls `inspectScreen()` internally to resolve the semantic IDs for every ask and action node. It then applies them as data attributes during DOM construction. |
| 105 | + |
| 106 | +The mapping is: |
| 107 | + |
| 108 | +1. `renderDom()` calls `inspectScreen(screenDef)`. |
| 109 | +2. The returned `InspectedScreen` object contains `asks[].semanticId` and `acts[].semanticId` values. |
| 110 | +3. These values are matched to DOM elements by the internal node `id` field (e.g., `ask_email`, `act_invite_member`). |
| 111 | +4. The semantic ID is set as the corresponding `data-intent-*` attribute. |
| 112 | + |
| 113 | +This is the same `inspectScreen()` call you would make manually — no extra computation, just reuse of the existing graph snapshot. |
| 114 | + |
| 115 | +### Screen-level attribute |
| 116 | + |
| 117 | +The `<main>` element receives `data-intent-screen` with the screen's own `semanticId`: |
| 118 | + |
| 119 | +```ts |
| 120 | +main.setAttribute("data-intent-screen", inspected.semanticId) |
| 121 | +``` |
| 122 | + |
| 123 | +The screen `semanticId` follows the same normalization rules: `screen("InviteMember")` → `screen:invite-member`. |
| 124 | + |
| 125 | +## What is not included |
| 126 | + |
| 127 | +- **Floating blocked-reason paragraphs** (`<p id="act_*-reason">`) — these are runtime feedback, not semantic nodes. They do not receive data attributes. |
| 128 | +- **Enter-key hints** (`<p id="*-enter-hint">`) — UI affordance, not a semantic node. |
| 129 | +- **Feedback output** (`<output>`) — runtime status, not a semantic node. |
| 130 | +- **Flow elements** — flows are graph concepts without a DOM representation. |
| 131 | +- **Surface elements** — the surface's DOM id is set from the surface's internal id, but no `data-intent-surface` attribute is added. The `<main>` element id doubles as the surface id. |
| 132 | + |
| 133 | +## Example end-to-end |
| 134 | + |
| 135 | +Given this screen: |
| 136 | + |
| 137 | +```ts |
| 138 | +const InviteScreen = screen("InviteMember", $ => { |
| 139 | + const email = $.state.text("email") |
| 140 | + const emailAsk = $.ask("Email", email).required() |
| 141 | + const invite = $.act("Invite member").primary().when(emailAsk.valid) |
| 142 | + $.surface("main").contains(emailAsk, invite) |
| 143 | +}) |
| 144 | +``` |
| 145 | + |
| 146 | +Rendering with `showSemanticIds: true` produces DOM elements with these attributes: |
| 147 | + |
| 148 | +| DOM element | Attribute | Value | |
| 149 | +|------------|-----------|-------| |
| 150 | +| `<main>` | `data-intent-screen` | `screen:invite-member` | |
| 151 | +| `<label>` | `data-intent-ask` | `ask:email` | |
| 152 | +| `<input>` | `data-intent-ask` | `ask:email` | |
| 153 | +| `<button>` | `data-intent-action` | `action:invite-member` | |
| 154 | + |
| 155 | +Calling `inspectScreen(InviteScreen)` returns matching `semanticId` values: |
| 156 | + |
| 157 | +```json |
| 158 | +{ |
| 159 | + "name": "InviteMember", |
| 160 | + "semanticId": "screen:invite-member", |
| 161 | + "asks": [ |
| 162 | + { |
| 163 | + "id": "ask_email", |
| 164 | + "semanticId": "ask:email", |
| 165 | + "label": "Email" |
| 166 | + } |
| 167 | + ], |
| 168 | + "acts": [ |
| 169 | + { |
| 170 | + "id": "act_invite_member", |
| 171 | + "semanticId": "action:invite-member", |
| 172 | + "label": "Invite member" |
| 173 | + } |
| 174 | + ] |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +## See also |
| 179 | + |
| 180 | +- [Inspect Screen and Diagnostics Guide](Inspect-Screen.md) — full `inspectScreen()` reference, semantic ID rules, and diagnostics reference. |
| 181 | +- [Quickstart](Quickstart.md) — step-by-step guide with `renderDom()`. |
| 182 | +- [Root README](../README.md) — project overview. |
0 commit comments