Skip to content

airframe-rx-html: onMount may not be able to find the rendered DOM element #3453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
xerial opened this issue Mar 15, 2024 · 0 comments · Fixed by #3947
Closed

airframe-rx-html: onMount may not be able to find the rendered DOM element #3453

xerial opened this issue Mar 15, 2024 · 0 comments · Fixed by #3947
Assignees
Labels

Comments

@xerial
Copy link
Member

xerial commented Mar 15, 2024

If an RxElement is rendered in a nested sequence, onMount can be called a bit earlier than rendered element actually appears in the browser.

An example:

class HoverableTextLabel(txt: RxElement, hoverMessage: String) extends RxElement:

  private val elementId = ULID.newULIDString

  override def onMount: Unit =
    RxDOM.getHTMLElementById(elementId).foreach { el =>
      try Dynamic.newInstance(Dynamic.global.bootstrap.Tooltip)(el)
      catch case e: Throwable => warn(e)
    }

  override def render: RxElement = span(
    id                   -> elementId,
    data("bs-toggle")    -> "tooltip",
    data("bs-placement") -> "top",
    data("bs-title")     -> hoverMessage,
    txt
  )

div(
  Seq[RxElement](
    HoverableTextLabel("hello", "mouseover message")
  )
)
@xerial xerial added the bug label Mar 15, 2024
@xerial xerial assigned Copilot and unassigned Copilot May 31, 2025
xerial added a commit that referenced this issue Jun 2, 2025
## Problem

When RxElements with ID attributes are rendered in nested sequences, the
`onMount` callback may be called before the DOM element is actually
available via `document.getElementById()`. This causes issues when
`onMount` code tries to find elements by their ID.

Example that demonstrates the issue:

```scala
class HoverableTextLabel(txt: RxElement, hoverMessage: String) extends RxElement:
  private val elementId = ULID.newULIDString

  override def onMount: Unit =
    RxDOM.getHTMLElementById(elementId).foreach { el =>
      // This may fail because element is not yet available by ID
      try Dynamic.newInstance(Dynamic.global.bootstrap.Tooltip)(el)
      catch case e: Throwable => warn(e)
    }

  override def render: RxElement = span(
    id -> elementId,
    data("bs-toggle") -> "tooltip",
    txt
  )

div(
  Seq[RxElement](
    HoverableTextLabel("hello", "mouseover message")
  )
)
```

## Root Cause

The previous implementation used a `MutationObserver` to detect when
elements were added to the DOM and then immediately called `onMount`.
However, there was a timing race condition where the element was added
to the DOM but not immediately available via `document.getElementById()`
due to browser batching of DOM updates.

## Solution

Modified `DOMRenderer` to ensure elements with IDs are fully available
before calling `onMount`:

1. **Mount element to DOM first** - The element is added to the DOM tree
2. **Check for ID attribute** - Determine if the element has an ID that
needs to be queryable
3. **Verify availability** - For elements with IDs, verify they're
available via `document.getElementById()`
4. **Retry if needed** - Use `setTimeout` with 0ms delay to retry if
element not immediately available
5. **Preserve existing behavior** - For elements without IDs, call
`onMount` immediately as before

## Testing

Added a comprehensive test case that reproduces the original issue and
verifies the fix works:

```scala
test("onMount should find element by ID in nested sequences") {
  class HoverableTextLabel(txt: RxElement, hoverMessage: String) extends RxElement {
    private val elementId = s"element-${System.nanoTime()}"

    override def onMount(n: Any): Unit = {
      RxDOM.getHTMLElementById(elementId) match {
        case Some(el) => foundElement := true  // Should succeed
        case None => // Should not happen with fix
      }
    }
    // ... render implementation
  }
  // Test passes with fix, was failing before
}
```

All existing `onMount` tests continue to pass, ensuring backward
compatibility.

Fixes #3453.

---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: xerial <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment