dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.19k stars 9.93k forks source link

Enable control over event propagation and default actions #5545

Closed SteveSandersonMS closed 4 years ago

SteveSandersonMS commented 6 years ago

There are lots of scenarios where this is needed. See descriptions in aspnet/blazor#715.

Since we don't want to rely on synchronous APIs, the solution will probably be a variation on event registration syntax, e.g.,

    <button @onclick="MyHandler" @onclick:stopPropagation="true" />

We still need to design what terminology to use (e.g., "propagation" vs "bubbling") and what features to include (e.g., whether there is also control over "preventDefault").

danieldegtyarev commented 6 years ago

IMO both are independant and required:

onclick-stop-propagation onclick-prevent-default

Usage

<button onclick=@MyHandler 
        onclick-stop-propagation 
        onclick-prevent-default />
Andrzej-W commented 6 years ago

Please read this comment: https://github.com/aspnet/Blazor/issues/715#issuecomment-403171755 to see an example where we need to make a decision at runtime.

danielmeza commented 6 years ago

What about use Attributes on the target method [StopPropagation] [PreventDefault] ?

jaredthirsk commented 6 years ago

Instead of:

<button onclick.prevent.stop=@MyHandler 
        onclick-stop-propagation 
        onclick-prevent-default />

Alternative vue-inspired syntax:

<button onclick.prevent.stop=@MyHandler />
aredfox commented 6 years ago

+1 this is really needed indeed, thinking of individual cells on tabels, other use cases etc.. otherwise the framework would be deemed very limited I'm afraid.

osintsevvladimir commented 6 years ago

Yes, for me this feature is also very important.

footysteve commented 6 years ago

I haven't been using Blazor very long, it may be something that's been covered and isn't possible.

I think these

button onclick.prevent.stop=@MyHandler

button onclick.prevent.stop=@MyHandler onclick-stop-propagation onclick-prevent-default

are both undesirable formats. My preference would be a parameter list, something like this

<button onclick=(@handler, prevent-default, no-propagation) />

less clutter, no changes to well known events, doesn't look like xaml .

Andrzej-W commented 6 years ago

Blazor 0.6 is coming with new wonderful features and I think it is time to schedule this issue for Blazor 0.7. It is a showstopper for all component creators. Please read #1293, #1063, #988, #951, #902, #803, #715. Please note, that sometimes we have to decide at runtime what to do. Simple example is here: https://github.com/aspnet/Blazor/issues/715#issuecomment-403171755 and some suggestion how to solve this problem at the bottom of this comment: https://github.com/aspnet/Blazor/issues/715#issuecomment-401950907

Andrzej-W commented 6 years ago

I have just updated my comment here https://github.com/aspnet/Blazor/issues/715#issuecomment-403171755 with an example which (I believe) can be implemented in Blazor using async API.

osintsevvladimir commented 5 years ago

Add this to the 0.7 release please.

danroth27 commented 5 years ago

We'll take a look at getting this into 0.7.0.

SQL-MisterMagoo commented 5 years ago

Hi, I have tried this out for myself on a clone of the 0.6.0 release, and one small change in EventDelegator.ts has given me the ability to use preventDefault() on any event on any element by specifying on the element.

For my testing, I just went with a simple attribute "bl-preventdefault" which contains a list of events you want to enable preventDefault on.

<div draggable="true" dropzone="move"
     ondragstart="@OnDragStart"
     ondragend="@OnDragEnd"
     ondragenter="@OnDragEnter"
     ondragleave="@OnDragLeave"
         ondragover="@OnDragOver"
     ondrop="@OnDrop"
         bl-preventdefault="dragenter,dragleave,dragover,drop"
     class="@Class"
     style="@(Styles())">
    @ChildContent(Data)
</div>

What do you think of using a simple fix like this for now?

Andrzej-W commented 5 years ago

@SteveSandersonMS , @danroth27 some time ago this was scheduled for Blazor 0.7. Any chance to reconsider for 0.8? Blazor is a great product but to succeed it needs ecosystem with ready to use components and I'm not talking about something simple like enhanced combobox or date picker. There are projects where we need powerful components from leading component vendors, for example report designer, spreadsheet, etc. Those vendors have to have full control over events and strictly speaking we need it also even for some very simple components.

gulbanana commented 5 years ago

as a workaround, i'm currently using js interop to register event handlers instead of the native event binding. dynamic input handling is really necessary for complex components, even if it doesn't fit nicely into the async/worker-thread model. even simple buttons and fields need a plethora of weird special cases when you're handling touch input, different browers, history state etc.

SteveSandersonMS commented 5 years ago

OK, I've been reading through everything that's ever been written about this, and am formulating it into a proposed design.

Scenarios

Constraint: Synchrony

The big limitation, which we've discussed many times, is that for this to work within the Razor Components programming model, we need to know in advance whether you want to cancel bubbling or preventDefault.

We can't have some .NET API for controlling this after the user has triggered the event, because code running on the server receives it later, and the browser APIs are synchronous. Nor can we have some system like suggested here where we block bubbling by default, but then re-raise the event asynchronously if the .NET code says we should - that doesn't work because many browser events can't be simulated from script (e.g., typing), nor are events treated the same if they are triggered asynchronously (e.g., attempting to open a popup). I know people want a way to do this, but there just isn't a way, so we have to come up with a nice design around it.

Design

Controlling propagation of bubbling events

Bubbling events propagate by default, so all we have to do is create a way to say if you don't want a certain event to bubble up from a certain element.

    <div onclick-stop-propagation="true">...</div> <!-- Or any other event -->

Note that we have to phrase it positively. We can't have it as "onsomeevent-bubble=false", because in Razor, a false bool attribute is shorthand for not emitting the attribute at all.

The use of onevent-stop-propagation is completely independent of whether you also have an onevent handler (C# or JS) on that same element, so this is very flexible.

If you need to use event-time logic to decide whether to bubble (e.g., whether the user is holding "shift" or not), your scenario is advanced and you can implement it with JS. Example:

    <div onclick="conditionallyStopPropagation(event)">...</div>

    // Separately, in a `.js` file:
    function conditionallyStopPropagation(event) {
        if (!event.shiftKey) {
            event.stopPropagation();
        }
    }

It's not unreasonable to use JS for advanced scenarios like this, and given the synchrony limitation, is the only way you're going to be able to put in totally arbitrary logic that run synchronously.

Implementation note: Our EventDelegator.ts implements its own bubbling mechanism to simulate regular bubbling while also using event delegation. The new feature we add is going to need to prevent bubbling of both the native event and the simulated event.

Control over default actions

Currently, our behavior is that we automatically preventDefault for submit events that have a .NET handler, but don't for any other type of event (whether or not they have a .NET handler).

We can make it possible to prevent default in a similar way to preventing event propagation:

    <input type="checkbox" onclick="@DoSomething" onclick-prevent-default="true" />

Again, onX-prevent-default=true can be used independently of whether you also have an onX event on that element. Internally it will register its own event handler that calls event.preventDefault().

As with above, it's only possible to prevent default based on information that exists prior to the event. You can't use .NET logic to prevent default conditionally after the event has started, because of the synchrony limitation. If you really do need conditional logic, this is an advanced scenario and you'll solve it via JS interop.

Example:

   <input bind="@SomeString" onkeydown="blockDigits(event)" />

    // Separately, in a `.js` file:
    function blockDigits(event) {
        if (event.keyCode >= 48 && event.keyCode <= 57) {
            event.preventDefault();
        }
    }

Again, it's not unreasonable to use JS for advanced and uncommon scenarios like this.

Non-scenarios

Controlling event capture: In JS, it's possible to register event listeners that fire during the "capture" phase of event handling, instead of during the "bubble" phase. However with Razor Components we don't expose that distinction - we use a mixture of capturing and bubbling handlers, depending on the event type, to produce the desired behaviors. If, one day, we have a way of changing your listener to be capturing, then we would also want something like onclick-stop-capture-propagation. But that's not a scenario we have today.

Avoiding preventDefault for submit when there's a .NET handler It's just wrong to ever want to do that. It's just not a scenario.

rynowak commented 5 years ago

This seems to be just what I expected 👍 concise and orthonogonal to other features.

One small nitpick - it should also be possible to write these examples with a minimized attribute, and maybe that's the preferred style.

    <div onclick-stop-propagation>...</div>

It's obvious that there has to be some runtime component to this, do you imagine that there's a compiler/tooling angle as well?

SteveSandersonMS commented 5 years ago

it should also be possible to write these examples with a minimized attribute, and maybe that's the preferred style

Totally agree. I was only writing it with ="true" because I wanted to emphasise that you can specify a bool value if you want to, as people might want it to vary dynamically (e.g., onclick-stop-propagation=@shouldStopPropagation). But if it's given as a minimized attribute, that's interpreted as "true". Also I agree it's the natural and preferred style.

do you imagine that there's a compiler/tooling angle as well?

We can add tooling for intellisense, but I was expecting us not to do so. It's sufficiently advanced that people are only going to be doing this because they're following an example or read about it in docs - the value of discovery through intellisense is lower than for most features.

So how about in the first cut we do it as a runtime feature only, and leave open the option to add a built-in tag helper to provide intellisense later if we want? It will make this much cheaper to implement, and you know how much we need to do in the P5 timeframe.

rynowak commented 5 years ago

OK, agreed on all points.

gulbanana commented 5 years ago

Will these attributes contribute to diffing when StateHasChanged()? I'd like to be able to render either <foo onlick-stop-propagation> or <foo> based on some parameter, for asynchronous dynamism.

SteveSandersonMS commented 5 years ago

@gulbanana Yes, they will be normal attributes whose values can change. That's what I meant by this above:

you can specify a bool value if you want to, as people might want it to vary dynamically (e.g., onclick-stop-propagation=@shouldStopPropagation)

uazo commented 5 years ago

another way could be a syntax like:

`

`

and also

@for (var i = 0; i <= 2; i++)
{
    <button onclick="@(x => x.StopPropagation(() => i % 2 == 0 )
                             .Then(() => IncrementCount2(x)))">@i</button>
}

with a class like this

namespace WebApplication1.App
{
    public class JSLambda<T> : UIEventArgs where T: UIEventArgs
    {
        Action _action;

        public bool RequestStopPropagation { get; set; }

        public JSLambda<T> StopPropagation()
        {
            RequestStopPropagation = true;
            return this;
        }

        public JSLambda<T> StopPropagation(Func<bool> func)
        {
            RequestStopPropagation = func();
            return this;
        }

        public JSLambda<T> Then(Action action)
        {
            _action = action;
            return this;
        }
    }

    [EventHandler("js-click", typeof(JSLambda<UIMouseEventArgs>))]
    public static class EventHandlers
    {
    }
}

with the only drawback is that StopPropagation (Func <bool> func) would always be called in the rendering phase and not during the real click, but in this way you could be able to control the bubbling from the server side, and possibly also client side if JSLambda could turn c # into javascript (or a series of instructions to be interpreted in javascript), all in c # without dirtying the html.

just to make it clear that it would probably be possible, here a project that transforms c # lambda into javascript

https://github.com/gearz-lab/lambda2js

sbuchok commented 5 years ago

If you are able to do:

onclick-stop-propagation=@shouldStopPropagation

why can't @shouldStopPropagation be an annonymous function? And if that is the case, why can't we just have a property on UIKeyboardEventArgs (or whatever args)? Honestly, using attributes seems wrong and only covers a small portion of what is needed. Personally, I'd prefer to have this left out completely if it's not going to be implemented nicely and rely on interop rather than muddying the water.

I have always found that stopPropagation is something that is either set, or it isn't (typically, but not always). However, preventDefault I use ypically with conditions. Prevent key strokes, button presses based on other inputs ... Very seldom do I blindly override preventDefault for all paths in a control.

I really don't know the technical limitations, but if adding a parameter to the UIKeyboardEventArgs is possible (which I'm 99.9% sure you can't or it would have been done already) or using an annonymous function to be passed to event-stop-propagation instead of a boolean variable, this to me would be prefered.

I don't mean any of this to sound rude at all. I am primarily a front end developer, so not being able to have my conditions in the function that is handling the event seems wrong and limiting. Or maybe I just need to change how I am used to doing things.

SteveSandersonMS commented 5 years ago

@sbuchok It's because of the asynchrony. I know it's not immediately obvious, but it is explained above in this thread (i.e., why making post-event decisions about preventDefault etc will have to be done in JS code, not .NET).

sbuchok commented 5 years ago

@SteveSandersonMS Stupid question, can we have both synchronous and asynchronous events? In the synchronous events, allow for preventDefault and stopPropagation. Chances are, this is not a minor change, so I'm guessing it won't go in, but throwing it out there.

SteveSandersonMS commented 5 years ago

No, it's async only.

sbuchok commented 5 years ago

@SteveSandersonMS DOH! Ok, having preventDefault for a textbox though doesn't make sense without conditions as you won't be able to type in the textbox.

I can understand there being an issue with async for stopPropagation, however, preventDefault I think is different (this is without reading understanding any of the code you guys have written). They don't have to be implemented the same way.

BTW, I'm loving working with Blazor/RazorComponents (I originally was using Blazor and switched to Razor Components) so far, with this exception ;)

I've primarily worked as a front end developer writing JS so having it work similarly is obviously ideal ... for me. Good luck and thanks.

miroslavp commented 5 years ago

Does that mean that one day when we are able to access the browser APIs directly from webassembly and we no longer need to write js, we will still rely on js interop for the event propagation and preventDefault? I thought js interop is a temporary solution until webassembly gets mature enough and we get rid of all the js.

Andrzej-W commented 5 years ago

I fully agree with @sbuchok that we usually want to handle preventDefault and often stopPropagation conditionally. I understand that it is a problem in Blazor on the server, but I believe that people will prefer client side Blazor in the future (when it will be fully supported). For those of us it would be nice to have fully functional framework without artificial limitations.

SteveSandersonMS commented 5 years ago

@mkArtakMSFT @danroth27 Realistically I'm not going to be in a position to do this during preview 6.

It's been waiting for the new directive attributes feature so it could build on that, but since that isn't in yet so I couldn't start, and I'm unavailable next week, and I will still need the last bits of this week for auth, it just won't fit.

We can either punt the whole thing, or try to identify someone else to implement it. I hope my design description above would give enough info for someone else to implement it, though the final APIs would look a bit different since they need to align with the new directive attributes once done.

Let's talk about this during team sync today.

sbuchok commented 5 years ago

I personally would prefer to see this feature gone and force us to use JS Interop rather than bring in something that only works for a small set of desired scenarios.

When I first started using Blazor, I thought that it was going to be able to do everything that JS did. However, I'm guessing it won't and probably was never meant to. If that's the case, maybe forcing us to use JS Interop makes more sense rather than muddying the water.

That's my 2... 5 cents ;)

I also think I'm bias. I love JS. But I'm currently working on something (for myself) where I'm trying to do it 100% in Blazor without any JS. So far, this has been my biggest stumbling block, which really speaks to how much is already available in Blazor.

Thanks again

JinShil commented 5 years ago

It's not unreasonable to use JS for advanced scenarios like this, and given the synchrony limitation, is the only way you're going to be able to put in totally arbitrary logic that run synchronously.

That makes me sad 😢

legistek commented 5 years ago

Has there been any news or progress on this? It's blocking me on a number of fronts. Is there any actual workaround currently besides handling the event in JS and invoking the C# handler manually?

stsrki commented 5 years ago

If we have this syntax to stop event propagation will we have full control of stopping events in code-behind based on some condition?

<button @onclick="MyHandler" @onclick:stopPropagation="true" />

Maybe better solution is to have behaviour similar to events in WinForms where we can control parameters on event args. Like this:

public class MouseEventArgs : EventArgs
{
    // other properties

    public bool StopPropagation { get; set; }

    public bool PreventDefault { get; set; }
}

void OnClick( MouseEventArgs eventArgs )
{
    if ( someCondition )
    {
        eventArgs.PreventDefault = true;
        return;
    }

    // do something else
}
manigandham commented 5 years ago

@stsrki That's not possible because of the async event handling. The decision has to be upfront. Read Steve's comment above: https://github.com/aspnet/AspNetCore/issues/5545#issuecomment-469748054

stsrki commented 5 years ago

@manigandham Thanks, I missed that comment. Steve said there is no way to preventDefault once the event has happened. It seems we're going to be stuck with JavaScript for a long time.

KameshRajendran commented 4 years ago

@danieldegtyarev Could you please let me know about this issue's estimated arrival time?

danroth27 commented 4 years ago

@kameshrajandran .NET Core 3.1, which is scheduled for Nov/Dec of this year.

SteveSandersonMS commented 4 years ago

The runtime part of this is now done and was merged in https://github.com/aspnet/AspNetCore/pull/14509

The tooling part is going to happen in 3.1.0-preview2, so assigning to @ajaybhargavb for the remaining work.

SteveSandersonMS commented 4 years ago

In fact we have a different issue tracking the tooling work for this (https://github.com/aspnet/AspNetCore/issues/14517), so closing this one.

arivoir commented 4 years ago

Was this finally added to the released Blazor 3.0?

danroth27 commented 4 years ago

Unfortunately it didn't make it into 3.0, but it has now been implemented for 3.1. We will have a preview of the functionality it a couple of weeks.

MaverickMartyn commented 4 years ago

Awesome. :) I just started fiddling with Blazor and ran into this requirement for drag and drop functionality.

mrpmorris commented 4 years ago

It's unfortunate JS won't let us decide asynchronously. I tried cancelling and then emulating mouse events but some browsers just wouldn't allow it.

A tricky problem indeed.

legistek commented 4 years ago

WPF has the same issue. You have to mark RoutedEventArgs as Handled before your event handler does anything async.

legistek commented 4 years ago

I wonder if the Blazor team has considered handling bubbling through the Blazor framework itself and bypassing Javascript event bubbling? Or would that just be too overwhelming for every DOM element to trigger an event in C# just for the possibility of bubbling it up to a parent?