Skip to content

Make preventDefault and stopPropagation event flags work without handler on the page #62479

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

Merged
merged 5 commits into from
Jul 2, 2025

Conversation

oroztocil
Copy link
Member

@oroztocil oroztocil commented Jun 26, 2025

Investigation

Browser event handling in Blazor is "virtualized". If there is at least one C# event handler for some event on the page, the client-side Blazor code registers a global JS listener. When the event is processed by the listener, Blazor iterates over targeted DOM elements and when it finds a registered handler, it invokes the appropriate .NET code via interop. The important detail here is that if there is no C# handler for the event on the page, this event gets ingored by Blazor.

At the same time, support for @on{eventName}:preventDefault and @on{eventName}:stopPropagation attributes is implemented by setting up "flags" on elements. A flag value is set when the renderer handles a special internal attibute on the element (e.g. __internal_preventDefault_{eventName}).

The flags then get checked when the element is iterated over by the global listener for the event. If an element has the preventDefault flag set, Blazor calls preventDefault on the browser event. Similarly, Blazor stops iterating over parent elements if it encounters an element with the stopPropagation flag set.

However, if no handler is registered for the event on the page, the global listener does not get set up and the flags are never checked.

Furthemore, since the global listener is registered without specifying the passive property, it is either active or passive depending on the type of the event. For certain events, namely wheel and touch events, the default is passive: true (as an UX optimization). Due to this, Blazor's preventDefault flag does nothing for these events.

Solution

When we set the flag to true for an element, we ensure that the global listener for the browser event is added by calling addGlobalListener(eventName) while applying the internal attribute. This either registers a new listener (if there was none), or increments the countByEventName[eventName] value.

We also register this listener with explicit passive: false while processing the preventDefault flag, to ensure that it works for all events. We can set this explicitly to false for all events because other event types have this as default anyway.

Cleaning up unused global listeners

What complicates things is that, ideally, we would want to remove the global listener when there are no actual handlers for that event AND no event flags. That is, we would like to decrement countByEventName[eventName] when an event flag is no longer applied - in all situations, including the removal of the parent element.

The fact that an event handler got removed is signalled to the client-side directly in the RenderBatch via DisposedEventHandlerIDs. There is no equivalent for event flag attributes as those are not tracked by the .NET side. (They are directly emitted on the elements as the internal attributes and forgotten.) That means that if the entire element with the event flag (or some parent section of the DOM) gets removed, the client-side gets no notification that the removed frames included the internal attribute.

Update:

Not cleaning up the global listener for an event is a minor issue that happens only in a specific scenario, i.e. the page contains the preventDefault flag and all of the elements with a handler or event flag for that event are later removed. Therefore, we decided to not handle such situation in order to not over-complicate the implementation. In cases where the preventDefault flag is removed due to the value of its condition changing, the listener gets properly cleaned up. (And users can therefore ensure this by disabling the flag before removing the element from the page, if they really care about the listener potentially being left behind.)

Fixes #18449
Fixes #24932

@github-actions github-actions bot added the area-blazor Includes: Blazor, Razor Components label Jun 26, 2025
@oroztocil oroztocil force-pushed the oroztocil/18449-prevent-default-without-handler branch from 27b3a1e to 0589eab Compare June 26, 2025 14:27
@oroztocil oroztocil requested a review from javiercn June 26, 2025 15:02
@oroztocil oroztocil force-pushed the oroztocil/18449-prevent-default-without-handler branch from 7458223 to 150152c Compare July 1, 2025 17:33
@oroztocil
Copy link
Member Author

oroztocil commented Jul 1, 2025

@javiercn Following our discussion about simplicity and "special casing", I decided to go with the client-side only solution. These are my reasons:

  • This solution does not increase special casing - it uses the existing special casing in the form of the setPreventDefault and setStopPropagation methods on the EventDelegator.
  • The changes are limited to the client-side, no .NET changes.
  • No unneeded interop calls.

I also added setting the listener for events with preventDefault flag as passive: false (this changes behavior only for events that would otherwise ignore the flag, the rest already had this by default), and I added E2E tests.

@oroztocil oroztocil marked this pull request as ready for review July 1, 2025 17:59
@oroztocil oroztocil requested a review from a team as a code owner July 1, 2025 17:59
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Solid work @oroztocil!

@oroztocil oroztocil merged commit 4594be6 into main Jul 2, 2025
29 checks passed
@oroztocil oroztocil deleted the oroztocil/18449-prevent-default-without-handler branch July 2, 2025 15:49
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0-preview7 milestone Jul 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Blazor webassembly: @onwheel:preventDefault has no effect @onevent:preventDefault does not trigger when event is not already registered
4 participants