Race condition in dmxBootstrap5Navigation with dmx-class:active during SPA navigation

Wappler 7.3.9
MacOS
NodeJS

Expected behavior

What do you think should happen?

When using SPA navigation with internal links, navigation elements that have dmx-class:active bindings should consistently apply the active class when their condition evaluates to true. The active state should be reliably reflected after each navigation event.

Actual behavior

What actually happens?

During SPA navigation, the active class is intermittently missing from navigation links even when the dmx-class:active condition is true. Sometimes the navigation link appears inactive despite being on the correct page. A hard page refresh always shows the correct active state.

How to reproduce

Step by step:

  1. Create a page with navigation links using both the internal attribute and dmx-class:active binding with a startsWith() condition to match a parent route and its sub-routes:

    <ul class="nav nav-pills flex-column">
        <li class="nav-item">
            <a class="nav-link" href="/dashboard" internal>Dashboard</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="/blog" internal
               dmx-class:active="browser1.location.pathname.startsWith('/blog')">
                Blog
            </a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="/profile" internal>Profile</a>
        </li>
    </ul>
    
  2. Create two pages:

    • /blog - a list or index page showing all blog posts
    • /blog/123 - a detail page showing a single blog post (sub-route of /blog)
  3. On the /blog page, add a link to the detail page:

    <a href="/blog/123" internal>View blog post</a>
    
  4. Navigate to /blog using the navigation link - observe the active class is correctly applied

  5. From /blog, click the link to navigate to /blog/123

  6. Observe that the active class is removed from the /blog navigation link, even though the dmx-class:active="browser1.location.pathname.startsWith('/blog')" condition should still be true (since /blog/123 starts with /blog)

  7. Refresh the page and observe the active class is correctly applied

Why this happens:

The issue occurs because when navigating to /blog/123, the dmxBootstrap5Navigation component's _stateHandler() checks if node.href == window.location.href. Since /blog !== /blog/123, it removes the active class. This conflicts with the custom dmx-class:active binding that uses startsWith() logic to keep parent routes active when viewing sub-routes.

Root cause:

The dmxBootstrap5Navigation component's _stateHandler() method (lines 26-46 in dmxBootstrap5Navigation.js) directly manages the active class:

_stateHandler () {
  const node = this.$node;
  const active = node.href == window.location.href ||
                 node.href == window.location.href.split("?")[0].split("#")[0];

  node.classList.toggle('active', active);
  // ...
}

This creates a race condition with AppConnect's dmx-class:active binding. Both systems try to control the same class during navigation events (popstate, pushstate, replacestate), leading to inconsistent timing and results.

Suggested fix:

Add a check to skip automatic active class management if the element has a dmx-class:active attribute:

_stateHandler () {
  const node = this.$node;

  const active = node.href == window.location.href ||
                 node.href == window.location.href.split("?")[0].split("#")[0];

   if (!node.hasAttribute('dmx-class:active')) {
      node.classList.toggle('active', active);
   }
  // ...
}

This would allow developers to use custom dmx-class:active logic (like startsWith() for matching sub-routes) without conflicts from the navigation component's automatic active state management, as this is the recommended approach by Patrick: