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.21k stars 9.95k forks source link

Syntax for writing a dictionary of attributes onto an element #5071

Closed chanan closed 5 years ago

chanan commented 6 years ago

This issue is being opened by request of @SteveSandersonMS Please see the full issue details https://github.com/aspnet/Blazor/issues/735 in the Blazor Repo.

The summary is that will be nice to be able to pass arbitrary attributes from a custom component down to an HTML element. For example:

<MyComponent id="myid" />

in the component render it as:

<button id="myid" />

without having to code every single attribute possible. Not mention allowing attributes such data-* to be passed down.

SteveSandersonMS commented 6 years ago

Edit: @rynowak hijacking this post

Summary

We want to have a feature for splatting arbitrary attributes into an element or component.

This will look like:

    <div @attributes="myAttributes" another="something">...</div>

Status/Items

Preview 6 update: The compiler support (splat operator) didn't make it for VS16.2-preview2. So it's possible to use this feature from components written in C#. We'll add the language feature in preview 7.

Syntax

Other attributes would be allowed to appear before or after the splat:

    <div foo= "bar" @attributes="myAttributes" another="something">...</div>

Multiple splats are allowed:

    <div foo= "bar" @attributes="myAttributes" another="something" @attributes="myOtherAttributes">...</div>

Attributes and expressions will be evaluated from left to right (as they are today).

Semantics

We should define this feature in terms of an IEnumerable<KeyValuePair<<string, object>>. This means that types like the following would all work:

If desired we can also make this work for arbitrary objects by mapping their properties into key-value pairs like we do in ASP.NET Core routing.

As in other languages we should rely on ordering to determine precedence. I propose we follow what typescript does, and allow later attributes to take precedence.

Examples:

@{
    var values = new Dictionary<string, string>()
    {
        { "a", "123" },
        { "b", "456" },
    }
}

<!-- results in <div b="456" a="ABC"></div> -->
<div @attributes=values  a="ABC"></div> 

<!-- results in <div a="123" b="456"></div> -->
<div a="ABC" @attributes=values></div> 

Implementation concerns

We have a choice of how to implement this semantic. Given the following code:

<div a="ABC" @attributes=values></div> 

This will map to something like:

builder.OpenElement(0, "div");
builder.AddAttribute(1, "a" "ABC");
builder.AddMultipleAttributes(2, values);
builder.CloseElement(0, "div");

note: I propose that we do the obvious thing with sequence numbers... We map a single splat to a single render tree builder call and a single sequence number

We have to decide how to do the de-duplication of the duplicate attribute (in this case a). This could be handled by the render tree builder (a) shows up once in the render tree or it could be handled by the renderer (a) shows up twice in the render tree, and the renderer is smart enough to do the right thing.

I'm looking for thoughts and input on which is the better approach.

rynowak commented 5 years ago

@SteveSandersonMS - added some design notes, would like your feedback on this.

SteveSandersonMS commented 5 years ago

This basically all looks great to me and matches what I think we've discussed before.

the only syntactic conflict is with directive attributes

One further syntactic possibility is to copy JS/TS and be explicit that we're merging contents by prefixing with ... as JS does. So the closest Razorish equivalent to the JS/TS syntax would be:

<somelem normal-attribute="something" @...myAttributes />

You'd then get a compile error if you just tried to put @myAttributes inside an element, except if that's the name of a directive attribute.

I'm not 100% determined that we should do this, but am interested in what you think.

chucker commented 5 years ago

I'm not sure JS-esque @... i quite the right 'idiomatic' syntax for C#, but I do favor such explicitness, given recent issues like https://github.com/aspnet/AspNetCore/issues/9786 which arose because the Razor syntax isn't particularly explicit.

rynowak commented 5 years ago

One further syntactic possibility is to copy JS/TS and be explicit that we're merging contents by prefixing with ... as JS does. So the closest Razorish equivalent to the JS/TS syntax would be: <somelem normal-attribute="something" @...myAttributes />

This would be possible to do, but could be tricky. I think this would require parser changes since we do shenanigans with ..

Originally I was thinking about something like <someelem normal-attribute="something" @splat="myAttributes" /> or <someelem normal-attribute="something" @attrs="myAttributes" />

rynowak commented 5 years ago

There's a bit of a tricky thing to figure out here, because the behaviour of HTML is different from what's described here.

If you write the following:

<div class="test1" class="test2">Peasy Cheese</div>

The result (in the DOM) is <div class="test1">Peasy Cheese</div>. That's right, static HTML does the opposite of what we've specced out here.

If you try the above example in Blazor right now, the result will be the same as static HTML because of the markup block transformation.

However if you do something to defeat the markup block transformation, you'll get the result we described here:

<div class="test1" class="@("test2")">Peasy Cheese</div>

Result (in the DOM) is <div class="test2">Peasy Cheese</div>.

Components also do the same thing right now (last wins).


So before I get too far with this item, I want answer the question of whether we want to do the opposite of what HTML does. I think it's reasonable for our case since we're not actually generating HTML text, we're manipulating the DOM directly.

This is relevant to describe because it's useful to think about splatting as equivalent to writing out all of the attributes by hand.

Options here:

  1. Last attribute wins a. Similar to how most programming languages think about splatting b. Current behavior for AddAttribute + browser rendering (elements and components) c. NOT current behavior for Markup Blocks d. NOT current behavior for prerendering - since prerendering generates static HTML
  2. First attribute wins a. Canonical HTML behavior b. NOT current behavior for AddAttribute + browser rendering (elements and components) c. Current behavior for Markup Blocks d. Current behavior for prerendering - since prerendering generates static HTML

JSX has last attribute wins semantics. So I lean towards that - I think it's the most intuitive option and it's already been chosen by a popular tech.

SteveSandersonMS commented 5 years ago

So before I get too far with this item, I want answer the question of whether we want to do the opposite of what HTML does. I think it's reasonable for our case since we're not actually generating HTML text, we're manipulating the DOM directly.

Absolutely. I didn't know about the HTML behavior for duplicate attributes (and have been writing HTML for a while...), and have never heard of anyone making use of that behavior deliberately. I see no reason to favour that weirdness over the intuitive convention that's been established in useful cases with JSX etc.

Originally I was thinking about something like <someelem normal-attribute="something" @splat="myAttributes" /> or <someelem normal-attribute="something" @attrs="myAttributes" />

Sure, having it be an explicit named directive attribute is fine.

I can't yet think of a name I find wholly satisfying. @splat is too colloquial for my liking - many developers may have no idea why it's called this and just consider it bizarre. Regarding @attrs, I'd probably prefer @attributes to be more formal, though it's a bit vexing that it's so clearly HTML terminology when we're trying to be more agnostic.

Hmm... I guess @attributes is the least likely to clash with unrelated future concepts and probably the easiest to remember.

rynowak commented 5 years ago

Do you explicitly prefer the directive attribute to <div @myAttributes>? We already started work on the parsing for directive attributes assuming that we were going to use that form (just an expression).

If we do end up with a directive attribute for splatting this will simplify Ajay's work, but we should pick the design we think is best.

SteveSandersonMS commented 5 years ago

I certainly prefer <div @myAttributes> except for the potential to clash with future directive attributes. I also prefer <div @...myAttributes> as a best-of-both-worlds solution, except if the implementation involves too much extra expense (and I think you're saying it does).

That said, I don't feel bad about making it a named directive attribute. The only issue is that we haven't settled on a good name for it yet.

On that point, one thing we don't seem to have discussed yet is whether this can also apply to components. We'd certainly get away without it in the short term, but eventually, people are going to have <BootstrapButton> or whatever, and will want to wrap it in <MyBootstrapButton> and pass through arbitrary params to the underlying BootstrapButton. So if this feature needs to extend to splatting component parameters, either now or in the future, then the name attributes seems extra awkward.

So perhaps on balance, among the suggestions for a named directive attribute, I lean towards either:

The latter of these is clearer, so probably that. I know this contradicts what I said earlier about preferring @attributes over @attribs, but I like the correspondence between @params and C#'s params arguments rather than having @parameters.

@danroth27 What do you think people will understand best?

danroth27 commented 5 years ago

I actually like @splat="myStuff" 😃. It's the same terminology used by Powershell (for better or worse).

galvesribeiro commented 5 years ago

I think @...myAttributes would be better as it works "similar" to what the deconstruction is on plain ES6...

chucker commented 5 years ago

@splat is too colloquial for my liking - many developers may have no idea why it's called this and just consider it bizarre.

And:

I like the correspondence between @params and C#'s params arguments

Yup.

“Splat” seems weird to me (I guess those familiar with the name will feel right at home, though?). “Combine” explains the function better, but the similarity in purpose to C# params arrays makes that one a great contender.

Liander commented 5 years ago

@SteveSandersonMS Would it make sense to also allow assignment of a proxy-object accompanying the dictionary of attributes, and have that object implement the render-behavior? The default implementation would have the duplicate order preference you discussed.

One can then inherit and produce custom render behavior for custom attributes. That was the essence of the idea mentioned here: #5600

Liander commented 5 years ago

Just wanted to make my previous question somewhat less cryptic with examples of potential use. It is mainly different factories with fluent API with different but not that complicated render manipulation:

<button @Bootstrap.Primary().Large().Block().Active() />
<ul @Bootstrap.ListGroup() />
<form @FormExtensions.Model(@myFormData, @myLanguageResource, @myValidator) />
<MyComponent @Oocss.Structure.Size(..).Padding(..).Margins(..) @Oocss.Skin.Color(..).Background(..).Border(..) />
<MyComponent @VisibilityExtensions.Hide(@isSupported) />

Maybe it is not falling into your liking, but I felt I needed to mention it.

Anyway, I noticed I am a bit late for any design discussion, but wanted to show the resemblance with this splat feature and let you explore if this kind-of render middleware is a good fit for it or not.

rynowak commented 5 years ago

Would it make sense to also allow assignment of a proxy-object accompanying the dictionary of attributes, and have that object implement the render-behavior? The default implementation would have the duplicate order preference you discussed.

I don't think a feature is needed for this. If a component defines a CaptureUnmatchedAttributes parameter, you can assign it directly.

rynowak commented 5 years ago

Also, to your examples you could do exactly what you're asking for once we have the language feature by returning an IEnumerable<KeyValuePair<string, object>>. So, I guess that's a yes 👍

Liander commented 5 years ago

Also, to your examples you could do exactly what you're asking for once we have the language feature by returning an IEnumerable<KeyValuePair<string, object>>. So, I guess that's a yes 👍

Thanks, Yes, for some of the examples that are only providing another API in a typed manner then one would only need to handle duplicates (at most) and it resembles with this splat handling.

For the list example I was hoping that one could have it generating other presets to child list-items to set 'list-group-item' automatically, and similar for the 'form' example, it could target settings of label, placeholder etc. To override the default render behaviour would mainly be to make those settings available to children. Making a component is always always an alternative but there is a risk of getting myriads of things like MyMaterialDesignForm, MyBootstrapForm, etc.

When it is only about passing along settings to chidren it could be an alternative to have the ability of passing along defaults selectively to children (by target element ID or by type in these examples) instead of making a component for handling it. Can that be done somehow with CaptureUnmatchedAttributes? To be used by child elements selectively instead of making components?

rynowak commented 5 years ago

When it is only about passing along settings to chidren it could be an alternative to have the ability of passing along defaults selectively to children (by target element ID or by type in these examples) instead of making a component for handling it.

For now this kind of scheme would have to be built-in the the component you're calling. We have no immediate plans (3.0) to build metaprogramming or macro-like extensbility.

Liander commented 5 years ago

For now this kind of scheme would have to be built-in the the component you're calling. We have no immediate plans (3.0) to build metaprogramming or macro-like extensbility.

Yes, a macro might behave similarly but then the elements need to be predefined. Building on this splat feature, please note that you should already be able to achieve this by having one "splat factory" register values to a service that another "splat factory" retrieves values from in a child element giving some filter key. Manually injecting those factory calls on all child elements would be impractical and ugly though. I wanted to point out the similarities to see if those calls could be handled somehow by the framework instead. Anyway, thanks for thinking about it.

SteveSandersonMS commented 5 years ago

@rynowak Did this issue stall on us not concluding the syntax/name? Or do you consider the plan to be decided?

rynowak commented 5 years ago

Did this issue stall on us not concluding the syntax/name? Or do you consider the plan to be decided?

Correct. I'm proposing we use @attributes

SteveSandersonMS commented 5 years ago

Even for components, even ones that might not have anything to do with HTML rendering?

I feel a bit odd about this, but maybe we justify it by saying you're meant to think of it as a syntactical macro - you're asking the framework to pretend a bag of stuff was actually a bunch of attributes in your anglebrackets-source-code. It is strange though:

<PersonEditor Person="@data.Person" @attributes=@ExtraParams />

@functions {
    [Parameter(CaptureExtraValues = true)] public IDictionary<string, object> ExtraParams { get; set; }
}
rynowak commented 5 years ago

Even for components, even ones that might not have anything to do with HTML rendering?

I really don't want to pick a different name for applying extra attributes to components. That sounds like something we'll explain to users eternally.

SteveSandersonMS commented 5 years ago

Yep, having two names would be pretty sucky and I don't want that either. But the problems with @attributes are enough to make me think @splat or @combine or @values would be more satisfying in the long run.

danroth27 commented 5 years ago

I think @attributes is fine. The markup syntax is still elements and attributes, even if some of those elements are components that use attributes to specify parameter values. That's is how I describe the syntax for using components for folks that are new to Blazor: You use an HTML-like syntax where the element matches the component type name and the attributes specify parameter values.

I also like @spat because I think it sounds fun, but after asking around a bit it seems not many folks agree with me :smile:.

rynowak commented 5 years ago

Who didn't agree. Cool people or .... them?

SteveSandersonMS commented 5 years ago

OK. Philosophically, @attributes is different to how I've thought about the meaning of what appears in .razor files. It's a statement about Razor syntax, rather than being a usage of Razor syntax. For it to make sense, you have to think of it as being a macro or another kind of metaprogramming.

In the end none of the other options are ideal either, so I'll stop being perfectionist about this. I can accept @attributes as being like a macro even though it gets resolved at runtime.

Liander commented 5 years ago

In a mixed environment of C# and HTML I think it is a bit unfortunate to use the name attributes. Isn't something like @apply more descriptive? (Maybe it's confusing using a verb.)

stavroskasidis commented 5 years ago

Why not use this syntax to avoid conflicting with the directive attributes

<div a="ABC" @(values)></div>

I don't think it gets clearer than that IMO.

rynowak commented 5 years ago

@stavroskasidis we considered this and we don't want to make a distinction in behavior between two constructs that are very similar in other contexts.

Ex:

<div>@Message</div>
<div>@(Message)</div>

<input @bind>
<input @(bind)>

In examples 1 and 2 they are semantically equivalent (evaluate the expression).

In examples 3 and 4 they are different. 3 is a directive attribute, and 4 is an expression.

When you but these examples side-by-side it starts to get vexing. We don't expect the usage of this feature to be extremely common, and using an attribute for it is already pretty concise. We have the ability to make something more concise later if it's needed.

stavroskasidis commented 5 years ago

Ok, that makes sense. Thank you for answering.

abhisheksiddhu commented 5 years ago

In preview6 @attributes is also used at the top level in component to apply attributes on components like authorize, I fear many users might get confused between the two.

danroth27 commented 5 years ago

@abhisheksiddhu Applying an attribute to a component class is actually done with @attribute (singular). Even so, I agree there is some potential for confusion. Tooling should help out here.