Skip to content

Commit 36e596e

Browse files
wickedevclaude
andcommitted
feat: add onDeadEndClick callback for buttons without navigation targets
Add callback that fires when a button or link without a navigation action (Goto, Back, Forward) is clicked. The callback receives: - sceneId: current scene ID - elementId: clicked button/link ID - elementText: displayed text - elementType: #button or #link Also adds helper functions isNavigationAction and hasNavigationAction, and comprehensive unit tests for the new functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e110a1d commit 36e596e

File tree

2 files changed

+373
-12
lines changed

2 files changed

+373
-12
lines changed

src/renderer/Renderer.res

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,24 @@ module DomBindings = {
5656
*/
5757
type onSceneChangeCallback = (option<string>, string) => unit
5858

59+
/**
60+
* Dead end click info type.
61+
* Contains information about the clicked element that has no navigation target.
62+
*/
63+
type deadEndClickInfo = {
64+
sceneId: string,
65+
elementId: string,
66+
elementText: string,
67+
elementType: [#button | #link],
68+
}
69+
70+
/**
71+
* Dead end click callback type.
72+
* Called when a button or link without navigation target is clicked.
73+
* @param info Information about the clicked element and current scene
74+
*/
75+
type onDeadEndClickCallback = deadEndClickInfo => unit
76+
5977
/**
6078
* Configuration for the rendering process.
6179
*/
@@ -65,6 +83,7 @@ type renderOptions = {
6583
injectStyles: bool,
6684
containerClass: option<string>,
6785
onSceneChange: option<onSceneChangeCallback>,
86+
onDeadEndClick: option<onDeadEndClickCallback>,
6887
device: option<deviceType>,
6988
}
7089

@@ -77,6 +96,7 @@ let defaultOptions: renderOptions = {
7796
injectStyles: true,
7897
containerClass: None,
7998
onSceneChange: None,
99+
onDeadEndClick: None,
80100
device: None,
81101
}
82102

@@ -230,17 +250,44 @@ let deviceTypeToClass = (device: deviceType): string => {
230250
*/
231251
type actionHandler = interactionAction => unit
232252

253+
/**
254+
* Dead end handler function type - called when an element without navigation is clicked.
255+
* Receives element ID, text, and type.
256+
*/
257+
type deadEndHandler = (string, string, [#button | #link]) => unit
258+
259+
/**
260+
* Check if an action is a navigation action (Goto, Back, Forward).
261+
*/
262+
let isNavigationAction = (action: interactionAction): bool => {
263+
switch action {
264+
| Goto(_) | Back | Forward => true
265+
| Validate(_) | Call(_) => false
266+
}
267+
}
268+
269+
/**
270+
* Check if actions array has any navigation actions.
271+
*/
272+
let hasNavigationAction = (actions: array<interactionAction>): bool => {
273+
actions->Array.some(isNavigationAction)
274+
}
275+
233276
// ============================================================================
234277
// Element Rendering
235278
// ============================================================================
236279

237-
let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): option<DomBindings.element> => {
280+
let rec renderElement = (
281+
elem: element,
282+
~onAction: option<actionHandler>=?,
283+
~onDeadEnd: option<deadEndHandler>=?,
284+
): option<DomBindings.element> => {
238285
// Handle input-only boxes by rendering children directly in a wrapper
239286
if isInputOnlyBox(elem) {
240287
let inputs = getInputsFromBox(elem)
241288
// If only one input, render it directly
242289
switch inputs->Array.get(0) {
243-
| Some(input) => renderElement(input, ~onAction?)
290+
| Some(input) => renderElement(input, ~onAction?, ~onDeadEnd?)
244291
| None => None
245292
}
246293
} else {
@@ -258,7 +305,7 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
258305
}
259306

260307
children->Array.forEach(child => {
261-
switch renderElement(child, ~onAction?) {
308+
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
262309
| Some(el) => div->DomBindings.appendChild(el)
263310
| None => ()
264311
}
@@ -274,8 +321,11 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
274321
btn->DomBindings.setTextContent(text)
275322
applyAlignment(btn, align)
276323

277-
// Attach click handler for actions
278-
if actions->Array.length > 0 {
324+
// Check if button has navigation actions
325+
let hasNavigation = hasNavigationAction(actions)
326+
327+
if hasNavigation {
328+
// Attach click handler for navigation actions
279329
switch onAction {
280330
| Some(handler) => {
281331
btn->DomBindings.addEventListener("click", _event => {
@@ -288,6 +338,16 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
288338
}
289339
| None => ()
290340
}
341+
} else {
342+
// No navigation - call dead end handler
343+
switch onDeadEnd {
344+
| Some(handler) => {
345+
btn->DomBindings.addEventListener("click", _event => {
346+
handler(id, text, #button)
347+
})
348+
}
349+
| None => ()
350+
}
291351
}
292352

293353
Some(btn)
@@ -312,8 +372,11 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
312372
link->DomBindings.setTextContent(text)
313373
applyAlignment(link, align)
314374

315-
// Attach click handler for actions
316-
if actions->Array.length > 0 {
375+
// Check if link has navigation actions
376+
let hasNavigation = hasNavigationAction(actions)
377+
378+
if hasNavigation {
379+
// Attach click handler for navigation actions
317380
switch onAction {
318381
| Some(handler) => {
319382
link->DomBindings.addEventListener("click", event => {
@@ -327,6 +390,17 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
327390
}
328391
| None => ()
329392
}
393+
} else {
394+
// No navigation - call dead end handler
395+
switch onDeadEnd {
396+
| Some(handler) => {
397+
link->DomBindings.addEventListener("click", event => {
398+
DomBindings.preventDefault(event)
399+
handler(id, text, #link)
400+
})
401+
}
402+
| None => ()
403+
}
330404
}
331405

332406
Some(link)
@@ -376,7 +450,7 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
376450
applyAlignment(row, align)
377451

378452
children->Array.forEach(child => {
379-
switch renderElement(child, ~onAction?) {
453+
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
380454
| Some(el) => row->DomBindings.appendChild(el)
381455
| None => ()
382456
}
@@ -397,7 +471,7 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
397471
contentEl->DomBindings.setClassName("wf-section-content")
398472

399473
children->Array.forEach(child => {
400-
switch renderElement(child, ~onAction?) {
474+
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
401475
| Some(el) => contentEl->DomBindings.appendChild(el)
402476
| None => ()
403477
}
@@ -411,13 +485,17 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
411485
}
412486
}
413487

414-
let renderScene = (scene: scene, ~onAction: option<actionHandler>=?): DomBindings.element => {
488+
let renderScene = (
489+
scene: scene,
490+
~onAction: option<actionHandler>=?,
491+
~onDeadEnd: option<deadEndHandler>=?,
492+
): DomBindings.element => {
415493
let sceneEl = DomBindings.document->DomBindings.createElement("div")
416494
sceneEl->DomBindings.setClassName("wf-scene")
417495
sceneEl->DomBindings.dataset->DomBindings.setDataAttr("scene", scene.id)
418496

419497
scene.elements->Array.forEach(elem => {
420-
switch renderElement(elem, ~onAction?) {
498+
switch renderElement(elem, ~onAction?, ~onDeadEnd?) {
421499
| Some(el) => sceneEl->DomBindings.appendChild(el)
422500
| None => ()
423501
}
@@ -670,7 +748,21 @@ let render = (ast: ast, options: option<renderOptions>): renderResult => {
670748
let sceneMap = Map.make()
671749

672750
ast.scenes->Array.forEach(scene => {
673-
let sceneEl = renderScene(scene, ~onAction=handleAction)
751+
// Create a scene-specific dead end handler that includes the scene ID
752+
let handleDeadEnd = switch opts.onDeadEndClick {
753+
| Some(callback) =>
754+
Some((elementId: string, elementText: string, elementType: [#button | #link]) => {
755+
callback({
756+
sceneId: scene.id,
757+
elementId,
758+
elementText,
759+
elementType,
760+
})
761+
})
762+
| None => None
763+
}
764+
765+
let sceneEl = renderScene(scene, ~onAction=handleAction, ~onDeadEnd=?handleDeadEnd)
674766
app->DomBindings.appendChild(sceneEl)
675767
sceneMap->Map.set(scene.id, sceneEl)
676768
})

0 commit comments

Comments
 (0)