From 138fa6ba0ba5e00a2225c3b109e9089feac32854 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 1 May 2018 17:12:03 +0100 Subject: [PATCH] Support custom events and non-bubbling standard events (#722) * Support non-bubbling events * Support responding to arbitrary events. E2E coverage of this and bubbling. * Rename E2E test files to avoid clash with other PR --- .../src/Rendering/EventDelegator.ts | 20 +++- .../BrowserRendererEventDispatcher.cs | 2 + .../Tests/EventBubblingTest.cs | 109 ++++++++++++++++++ .../EventBubblingComponent.cshtml | 34 ++++++ test/testapps/BasicTestApp/wwwroot/index.html | 1 + 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/EventBubblingTest.cs create mode 100644 test/testapps/BasicTestApp/EventBubblingComponent.cshtml diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/EventDelegator.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/EventDelegator.ts index fb24c2c5f..af101f506 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/EventDelegator.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/EventDelegator.ts @@ -1,5 +1,10 @@ import { EventForDotNet, UIEventArgs } from './EventForDotNet'; +const nonBubblingEvents = toLookup([ + 'abort', 'blur', 'change', 'error', 'focus', 'load', 'loadend', 'loadstart', 'mouseenter', 'mouseleave', + 'progress', 'reset', 'scroll', 'submit', 'unload', 'DOMNodeInsertedIntoDocument', 'DOMNodeRemovedFromDocument' +]); + export interface OnEventCallback { (event: Event, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet): void; } @@ -64,6 +69,7 @@ export class EventDelegator { // Scan up the element hierarchy, looking for any matching registered event handlers let candidateElement = evt.target as Element | null; let eventArgs: EventForDotNet | null = null; // Populate lazily + const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type); while (candidateElement) { if (candidateElement.hasOwnProperty(this.eventsCollectionKey)) { const handlerInfos = candidateElement[this.eventsCollectionKey]; @@ -78,7 +84,7 @@ export class EventDelegator { } } - candidateElement = candidateElement.parentElement; + candidateElement = eventIsNonBubbling ? null : candidateElement.parentElement; } } } @@ -105,7 +111,11 @@ class EventInfoStore { this.countByEventName[eventName]++; } else { this.countByEventName[eventName] = 1; - document.addEventListener(eventName, this.globalListener); + + // To make delegation work with non-bubbling events, register a 'capture' listener. + // We preserve the non-bubbling behavior by only dispatching such events to the targeted element. + const useCapture = nonBubblingEvents.hasOwnProperty(eventName); + document.addEventListener(eventName, this.globalListener, useCapture); } } @@ -154,3 +164,9 @@ interface EventHandlerInfo { componentId: number; eventHandlerId: number; } + +function toLookup(items: string[]): { [key: string]: boolean } { + const result = {}; + items.forEach(value => { result[value] = true; }); + return result; +} diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs index 5bc8668ca..7103e3484 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs @@ -38,6 +38,8 @@ private static UIEventArgs ParseEventArgsJson(string eventArgsType, string event return JsonUtil.Deserialize(eventArgsJson); case "change": return JsonUtil.Deserialize(eventArgsJson); + case "unknown": + return JsonUtil.Deserialize(eventArgsJson); default: throw new ArgumentException($"Unsupported value '{eventArgsType}'.", nameof(eventArgsType)); } diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/EventBubblingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/EventBubblingTest.cs new file mode 100644 index 000000000..5302a738d --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/EventBubblingTest.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BasicTestApp; +using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures; +using OpenQA.Selenium; +using System; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests +{ + public class EventBubblingTest : BasicTestAppTestBase + { + // Note that currently we only support custom events if they have bubble:true. + // That's because the event delegator doesn't know which custom events bubble and which don't, + // so it doesn't know whether to register a normal handler or a capturing one. If this becomes + // a problem, we could consider registering both types of handler and just bailing out from + // the one that doesn't match the 'bubbles' flag on the received event object. + + public EventBubblingTest( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + Navigate(ServerPathBase, noReload: true); + MountTestComponent(); + } + + [Fact] + public void BubblingStandardEvent_FiredOnElementWithHandler() + { + Browser.FindElement(By.Id("button-with-onclick")).Click(); + + // Triggers event on target and ancestors with handler in upwards direction + Assert.Equal( + new[] { "target onclick", "parent onclick" }, + GetLogLines()); + } + + [Fact] + public void BubblingStandardEvent_FiredOnElementWithoutHandler() + { + Browser.FindElement(By.Id("button-without-onclick")).Click(); + + // Triggers event on ancestors with handler in upwards direction + Assert.Equal( + new[] { "parent onclick" }, + GetLogLines()); + } + + [Fact] + public void BubblingCustomEvent_FiredOnElementWithHandler() + { + TriggerCustomBubblingEvent("element-with-onsneeze", "sneeze"); + + // Triggers event on target and ancestors with handler in upwards direction + Assert.Equal( + new[] { "target onsneeze", "parent onsneeze" }, + GetLogLines()); + } + + [Fact] + public void BubblingCustomEvent_FiredOnElementWithoutHandler() + { + TriggerCustomBubblingEvent("element-without-onsneeze", "sneeze"); + + // Triggers event on ancestors with handler in upwards direction + Assert.Equal( + new[] { "parent onsneeze" }, + GetLogLines()); + } + + [Fact] + public void NonBubblingEvent_FiredOnElementWithHandler() + { + Browser.FindElement(By.Id("input-with-onfocus")).Click(); + + // Triggers event only on target, not other ancestors with event handler + Assert.Equal(new[] { "target onfocus" }, GetLogLines()); + } + + [Fact] + public void NonBubblingEvent_FiredOnElementWithoutHandler() + { + Browser.FindElement(By.Id("input-without-onfocus")).Click(); + + // Triggers no event + Assert.Empty(GetLogLines()); + } + + private string[] GetLogLines() + => Browser.FindElement(By.TagName("textarea")) + .GetAttribute("value") + .Replace("\r\n", "\n") + .Split('\n', StringSplitOptions.RemoveEmptyEntries); + + private void TriggerCustomBubblingEvent(string elementId, string eventName) + { + var jsExecutor = (IJavaScriptExecutor)Browser; + jsExecutor.ExecuteScript( + $"document.getElementById('{elementId}').dispatchEvent(" + + $" new Event('{eventName}', {{ bubbles: true }})" + + $")"); + } + } +} diff --git a/test/testapps/BasicTestApp/EventBubblingComponent.cshtml b/test/testapps/BasicTestApp/EventBubblingComponent.cshtml new file mode 100644 index 000000000..4de6f38b2 --- /dev/null +++ b/test/testapps/BasicTestApp/EventBubblingComponent.cshtml @@ -0,0 +1,34 @@ +

Bubbling standard event

+ +
+ + +
+ +

Bubbling custom event

+ +
+
Element with onsneeze handler
+
Element without onsneeze handler
+
+ +

Non-bubbling standard event

+ + +
+

With onfocus:

+

Without onfocus:

+
+ +

Event log

+ + + +@functions { + string logValue = string.Empty; + + void LogEvent(string message) + { + logValue += message + Environment.NewLine; + } +} diff --git a/test/testapps/BasicTestApp/wwwroot/index.html b/test/testapps/BasicTestApp/wwwroot/index.html index 92376840c..d33536089 100644 --- a/test/testapps/BasicTestApp/wwwroot/index.html +++ b/test/testapps/BasicTestApp/wwwroot/index.html @@ -32,6 +32,7 @@ +