Make preventDefault and stopPropagation event flags work without handler on the page #62479
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
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 callspreventDefault
on the browser event. Similarly, Blazor stops iterating over parent elements if it encounters an element with thestopPropagation
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 ispassive: true
(as an UX optimization). Due to this, Blazor'spreventDefault
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 thecountByEventName[eventName]
value.We also register this listener with explicit
passive: false
while processing thepreventDefault
flag, to ensure that it works for all events. We can set this explicitly tofalse
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
viaDisposedEventHandlerIDs
. 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 thepreventDefault
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