aspnetcore: Cannot pass through directive attributes, so components can't support @on{EVENT}:stopPropagation etc.

Is your feature request related to a problem? Please describe.

This feature relates to https://github.com/dotnet/aspnetcore/issues/18460 where I’m building a custom styled component and want to keep all features for a simple tag, eg:

<div class="@boxClass" @attributes="AdditionalAttributes">
    @ChildContent
</div>

@code  {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}

where, @boxClass is generated somewhere else. All attributes works except the @on{EVENT} in combination with @on{EVENT}:stopPropagation and/or @on{EVENT}:preventDefault.

Describe the solution you’d like

I’d expect that if I’m able to pass down the event handler it should be possible to pass the event custom attributes also since it’s not practical (could be for a workaround) to have two flags for each possible event and declare them into the component.

Additional context

None

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 6
  • Comments: 20 (13 by maintainers)

Most upvoted comments

Hi @SteveSandersonMS,

I came across this issue (again) and decided to look at code this time. For library developers who want to support arbitrary event listeners on their components, there’s just no way around attribute splatting, but that mechanism doesn’t support @onclick="Handler" @onclick:stopPropagation="Stop", because RZ10010 (“The component parameter 'onclick' is used two or more times for this component. Parameters must be unique (case-insensitive). The component parameter 'onclick' is generated by the '@onclick:stopPropagation' directive attribute.”) will prevent compiling that code.

The thing is: I don’t at all see why that’s the case! This is what I observed with the razor compiler:

  1. @{eventName}:stopPropagation="value" (and similarly preventDefault), will generate the code __builder.AddEventStopPropagationAttribute(seq, eventName, value);
  2. AddEventStopPropagationAttribute is implemented in WebRenderTreeBuilderExtensions with a one-liner: https://github.com/dotnet/aspnetcore/blob/a4154ac58b07bce7ddb086427c1bc8a13073babd/src/Components/Web/src/Web/WebRenderTreeBuilderExtensions.cs#L50
  3. Since AddEventStopPropagationAttribute is implemented as builder.AddAttribute(sequence, $"__internal_stopPropagation_{eventName}", value);, @{eventName}:stopPropagation="value" could be replaced by __internal_stopPropagation_{eventName}="@value" with the exact same effect as AddEventStopPropagationAttribute.
  4. Testing this with .NET 5 suggests that replacing the directive by the internal attribute works perfectly fine: the stopPropagation behavior is correctly applied.

This makes me wonder why RZ10010 is there in the first place. The implementation does not use the same attribute name twice at all (because one attribute is named onclick and the other __internal_stopPropagation_onclick).

Could it perhaps be that this is an error that was necessary in some old version of blazor, but now is just obsolete because implementation changed?

Can you please explain to me why this isn’t priotized, how is blazor useable when you can’t control events triggering upwards into the other components? It just results in a mess… parent components doing things when a child component get clicked…This ticket has been here since 2020, this should have been fixed a long time ago. I don’t see how blazor is useable without this.

You don’t have to increment the sequence numbers.

I apologize in advance, that I have not figured out the details, and unfortunately, I don’t have time to spend on this as I am heads down on Blazor united work. Take my suggestion as a rough sketch and play with it if you want, it might not pan out, but I think it can potentially be done.

Hi - did this get reviewed for ASP.NET 7? @stefanloerwald was working with us on Material.Blazor at the time of his comments 18 months back, and it would be good to have a resolution.

Parent.razor

<Child @bind-Value="@value" BindingProperties="new BindValueProperties("oninput",preventDefault:true, ...)" />

Child.razor

[Parameter] BindValueProperties BindingProperties { get; set; }
<input unused="@BindingProperties" />

Some extension method

public static void AddAttribute(this RenderTreeBuilder builder, int sequence, string name, BindValueProperties properties)
{
   // map the event manually
   // builder.AddAttribute(....)
}

Ok, I found a way using RenderFragment

// Box.tsx
@RenderParent()

@code
{
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }

    private RenderFragment RenderParent()
    {
        RenderFragment parent = builder =>
        {
            builder.OpenElement(1, "div");
            foreach (var (k, v) in AdditionalAttributes ?? new Dictionary<string, object>())
            {
                builder.AddAttribute(1, k, v);
            }

            foreach (var config in EventsConfig)
            {
                if (config.PreventDefault)
                {
                    builder.AddEventPreventDefaultAttribute(1, config.EventName, true);
                }
                if (config.StopPropagation)
                {
                    builder.AddEventStopPropagationAttribute(1, config.EventName, true);
                }
            }
            builder.AddContent(1, ChildContent);
            builder.CloseElement();
        };

        return parent;
    }
}

so the user can use like this

<Box @onclick="ClickHandler" EventsConfig="EventsConfig">
   Any content
</Box>

@code
{
    private EventConfig[] EventsConfig = new EventConfig[] {
        new EventConfig { EventName = "onclick", StopPropagation = true }
    };
}

And I agree about @key and @ref but they are standalone attributes while the @on{EVENT}:stopPropagation is not. Maybe there are some middle ground where we can capture these events directives the same way we capture unmatched parameters. But my way works fine for now.