Skip to content

Instantly share code, notes, and snippets.

@rynowak
Last active June 8, 2019 16:03
Show Gist options
  • Save rynowak/f2e6a4bfc3b685d7dce15d96942aa4b8 to your computer and use it in GitHub Desktop.
Save rynowak/f2e6a4bfc3b685d7dce15d96942aa4b8 to your computer and use it in GitHub Desktop.

Summary

Razor Components (.razor) have two primary kinds of metaprogramming

  1. Components (replaces an element with a C# class)
  2. Directive attributes (ref, onclick, bind-...)

Components are the main building block of the components programming model (duh). Components are always invoked as an element - primarily serve to create the hierarchical structure of the UI. Components can impact either other by providing cascading values, and choosing whether to render their child content or not. Components can be authored by anyone, and we expect everyone to write their own components as well as use components from libraries.

Directive attributes can decorate both components and DOM elements, and can attach arbitrary behavior. This can include intrinsic features of the render tree builder like ref, and event handlers (onclick) - or they can synthesize additional attributes like two-way binding (bind-...). Directive attributes can currently only be authored as part of the compiler.

Since directive attributes allow you customize the behavior of components and DOM elements, making them extensible adds a "word of power" to Razor as a language. While my immediate concern is how we design the syntax for invoking the built-in directives, we should also consider how these ideas would succeed or fail if users are able to author directives. These are features that we expect to appear everywhere in .razor files, and it will be extremely painful for users to change how they work post-release.

Requirements, Goals and Scoping

requirement: Specify the syntax of the built-in directives as part of ASP.NET Core 3.0.

requirement: Support all of the existing scenarios that built-in directives currently have and more.

scope: Built-in directives include ref, event handlers, binding, and soon splatting.

scope: Making directives extensible is not a requirement for this release.

goal: Syntax design includes high-quality tooling experiences.

goal: Syntax design is aesthetically pleasing.

goal: Seeing directives next to DOM attributes and component parameters should not feel too jarring or inconsistent.

goal: Make directives visually or syntactically distinct from normal DOM attributes.

goal: Provide suffient design-space for future directive designs without changes to syntax.

goal: Don't diverge from the metaphor of using attributes to attach behavior.

Case Studies

I'm comparing a few different approaches here so see what kinds of insights we can get from the syntactic choices. There are two main strategies to compare - giving the developer power through an expressive attribute key, and giving the developer power through an expressive attribute value. There's a third way, which is to make the actual attributes simple use more of them.

Case-Study: Vue

Vue provides a generic, extensible directive system that enables users to metaprogram DOM elements. Built-in directives are used for features like ref, event handlers, and binding (and more). It's important to note that while a Vue directive appears to set a single attribute, directives have the ability to metaprogram the element/component dom. Directives may set multiple DOM attributes, set a single one after transforming the value, or set no attribute at all.

Vue directives give a few design points to the implementor beyond a simple key=value.

An example:

<button v-on:submit.prevent="onSubmit"></button>

In the example above v-on:submit.prevent="onSubmit" can be decomposed as v-(name):(argument):(modifier)=(value). The directive has a name, an optional single argument, an optional list of modifiers (boolean flags) and an optional value.

In this example, the onSubmit javascript function is bound to the button's submit event, and the callback will call preventDefault() to stop event propagation. Read more about the kinds of things that v-on supports here

How does this align or compare with our goals:

  • v- syntax makes this visually and (practically) syntactically distinct from a normal HTML attribute.
  • Supporting an argument and modifiers allows for lots of flexible designs for directives.
  • The set of modifiers and completion of known values for attributes could be tooled.

My personal opinion on the aesthetics of Vue's attributes - I think that Vue's examples are readable and simple. More complex examples are going to feel cramped (multiple modifiers). However, since directives in Vue only allow a single argument, it's hard to imagine a case with much more visual noise than something like <span v-format.bold.highlight.underline>guide</span>

As an interesting note, Vue sticks to directives as valid HTML attributes names. For instance a directive may have a dynamic value for its argument (v-on:[eventName]="..."). eventName in this case is a javascript expression, but the expression may not contain spaces, nor an invalid attribute name characters like quotes. Vue also uses @ as a shorthand for v-on (@ is legal in an attribute name).

Case Study: WPF & Markup Extensions

I don't know much about the other Xaml flavours aside from WPF, I'm going to stick to what I know because there's some useful information there.

WPF/Xaml provide a concept called markup extensions, which allow metaprogramming of the value for a single attribute. The property/attribute system in WPF is complex, and I won't go into it in detail here. I'm going to analyze the syntax of WPF/Xaml's markup extensions and assume that it would be possible to build something as powerful as our built-in directives instead of being limited to setting a single property.

Since this is Xaml, the central metaphor is creating objects.

Example:

<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />  

A markup expression { ... } uses a markup extension (identified by DynamicResource or x:Static) to resolve a value. An instance of the mark up extension is created using ordinal and key-value parameters, and then the markup extension is invoked to produce the value. As seen here, markup extensions can be nested.

I've picked an example that deals with WPF's styling system, so I'm going to imagine something that's closer to our scenarios, and then look at this through the lens of HTML+Razor. One could imagine putting different syntax inside the attribute value - I'm showing this to consider the idea of using the attribute value.

Example:

<button onsubmit="@{EventHandler OnSubmit, PreventDefault = true}"></button>

Another:

<input type="text" value="@{Binder myValue, Event = Events.OnInput}"></button>

How does this align or compare with our goals:

  • @{ } syntax makes this visually and (practically) syntactically distinct from a normal HTML attribute value.
  • It does a good job when the attribute you want to set has a static and dynamic usage (bind-... or onclick)
  • It doesn't really offer a solution for something like ref that doesn't exist in HTML
  • It also implies that this kind of syntax might be allowed for any attribute or component property, I'm not sure if this is good or bad.
  • Supporting ordinal arguments and key-value-pairs allows for lots of flexible designs for directives.
  • The set of arguments, keys and completion of known values for attributes could be tooled.

I think the biggest criticism of something like this is that it's just not C#. Xaml tries to hide the syntax of a particular language because Xaml spans many. So Xaml needs features like markup extensions because you can't put literal code inside of an attribute value.

Well we can. What we rewrote this using regular C#?

<input type="text" value="@(EventCallback.Factory.CreateBinder(this, ...))"></button>

The problem is that users wouldn't want to write the real code that replaces .... It also includes implementation details like the fact that EventCallbackFactory needs to be involved instead of just being simple. For cases like bind-... the code required to make it work is already something we only trust the compiler to write.

Case Study: Angular Attribute Directives

Angular uses the term directive for a few different things, I'm specifically talking about what they call attribute directives here. However in terms of syntax used to invoke a directive, it looks like most of the different concepts angular provides behave similarly.

Angular's directives are more limited in expressiveness compared to Vue, Angular does more things with built-in syntax compared to Vue's directives.

Attribute directives in Angular specify a css-selector to attach themselves, and then they can optionally read other attributes from the same element. It's recommended (but not required to use a distinct attribute prefix).

Examples:

<p [appHighlight]="color" defaultColor="violet"></p>

Notice in this case that defaultColor is not prefixed.

<input #newHero (keyup.enter)="addHero(newHero.value)">

Notice in this case that the (keyup.enter) incantation is really similar to Vue v-on:keyup.enter. Angular's directives end up with simple keys and simple values for user-created directives. Built-in features that are part of the binding system (event bindings, style bindings) have a more Vue-like set of features in their keys.

How does this align or compare with our goals:

  • Angular's directives and the other attributes they read are not syntactically distinct.
  • Directives support reading arbitrary other attributes, but they have nothing like Vue's argument or modifiers.
  • Angular uses a built-in syntax for binding and events, and this is more powerful.
  • The set of known attributes can be tooled easily

Putting it together:

Based on the case study and the criteria above, some concrete things I'm going to propose.


Prefix our directive attributes with @ - @ is a legal character in an attribute name.

This feels very-Razorey, and as long as it isn't colored bright yellow it will fade into the background. This solves the problem where it's very easy to put something wrong in onclick and other events. This starts to feel important when directives overlap with and provide an alternate experience to the HTML built-ins.

Examples of our current built-ins:

<input @bind="foo" />
<MyTextInput @bind-Value="foo" />
<input @onclick="OnClick" />
<input @ref="bar" />

Note, we no longer need @ inside the attribute value for onclick because we don't need to distinguish between C# and markup based on the syntax of the value.

Another cool example:

<div @style="..."></div>

You could imagine scaling this to include more directives based around HTML primitives once it's possible to use the names everyone already knows. style is for normal css styling. @style is for when it gets real.


Stick to attribute names for passing additional arguments and flags to directives

I don't think there's much to say about trying to put more power at the users fingertips by introducing more syntax in the attribute value. Putting some kind of a syntactic format here would require us to write new parser logic for this, and it could stray from the ability for users to write equivalent code to what the compiler generates.

We already have arguments in our built-ins (bind-value-onclick + format-value) takes three arguments counting the format. We're introducing modifiers as well. We feel like the value these features provide is worth it and they shouldn't be removed. On the contrary, we should try to specify these in ways that lead to a good tooling experience. It's not clear if we need to support multiple arguments to a directive.

We don't have any concept yet of a dynamic argument. For instance Vue allows a syntax like v-on:[eventName] where the value of eventName is evaluated to decide how binding occurs. I don't really see us needing this for bind but it's worth considering what design approach we would take to build it.

The main concerns for a syntax like this:

  • We can't allows spaces in the attribute name (breaks our experience in most HTML parsers)
  • Do we allow mutliple arguments in a single attribute or require separate attributes
  • How do we delimit arguments (ordinal) from modifiers (unordered flags)

We could opt to surround things with a paired delimiter ((), [], {}), we could opt to use inline delimiters (., ,, :, -). The reason for extra delimiters is that they are a tremendous help to completion systems. The experience is much much better if the things we use as delimiters are also treated as commit characters by tooling. This means that you can type bin, select bind in the menu, type - and you now have bind- and are being shown completions for the next token.

One of our primary motivating cases (bind + format) also has the characteristic that we want to pass a string literal identifier value as an additional argument to the directive. This isn't something I could really find a precedent for other than in Xaml - Vue allows runtime evaluation of arguments, but the syntaxtic form must not contain special characters like ". I'm going to assume that we want to retain format-... as a separate attribute for the purposes of this discussion.

From experimentation and discussion within the team - things we like (good fit for Blazor):

  • A directive should be able to have flags (modifiers)
  • A directive should logically be able to have key-value arguments where the value is in C#

Things we don't like:

  • Expressing values inside an attribute name just feels bad
  • method-call syntax as an attribute name

Proposal

Here's a proposal that's opimized for readability and simplicity. These cases are meant to be unambiguous and scale to a variety of usages.

Directive attributes are an attribute prefixed with @ and can have an optional argument where the argument is an string literal identifier. For now we're limiting this to one argument, but we could allow multiple if there was ever a need. Directive attributes can also have key-value arguments which are an additional attribute with a key suffix.

Syntax for a directive attribute: @(name)-(argument)=(value) Syntax for a directive key-value attribute: @(name)-(argument):(key)=(value)

The reason for a secondary separator is for completion to use as a commit character, and also for clarity in how directives are grouped. It's important that we can tell the difference between a directive with an argument and a directive with a key-value argument. Since arguments are arbitrary identifiers it has to rely on syntax or else its ambiguous. Examples of this below.

I suggest for now that the presence of the directive attribute is required in order for the usage of the directive key-value attributes. This leads to preferring the use of the argument when designing directives. The implication is that for some directives the argument is optional, and for some it is required. When authoring directives we should strive to always make key-value attributes optional.

Attribute values follow the same rules as component parameters. They can either be specified as string-like, bool-like, or other. String-likes put you in the markup context by default. Bool-likes allow minimized form. Anything else puts you in the C# context by default.

Open question: If we like this idea, do we like keys as lowercase or camelcase?

Abstract examples

Some examples that attempt to describe the syntax and possible forms that are legal. Real examples from Blazor come later.


Directive attribute without a value:

<div @widget>...</div>

Directive attribute with a value:

<div @widget="kevin">...</div>

Directive attribute with an argument without a value:

<div @widget-x>...</div>

Directive attribute with an argument with a value:

<div @widget-x="kevin">...</div>

Directive attribute with a minimized key-value argument (modifier/flag):

<div @widget="kevin" @widget:awesome>...</div>

Directive attribute with a minimized key-value argument:

<div @widget="kevin" @widget:color="blue">...</div>

Directive attribute with multiple arguments:

<div @widget="kevin" @widget:color="blue" @widget:size="size">...</div>

Directive key-value argument without directive attribute (not allowed):

ERROR

<div @widget:size="size">...</div>

We could allow this if we wanted to, but I'm not sure we have a use case for it. If we wanted to allow it, it would be the same thing as:

<div @widget @widget:size="size">...</div>

It has to be defined this way because anything would case an imbiguity or inconsistent. As written we have to consider size to be the value provided for @widget:size. The only design space left is whether the @widget is provided or not. Since this is limited to booleans and non-obvious I propose we don't allow it. I'm not sure that making directives where the main function is a boolean is a good practice for directives.


Grouping of key-value arguments:

<div 
    @widget="kevin"
    @widget:awesome
    @widget-x="jimmy"
    @widget-x:size="huge"
    @widget-x:awesome="@isJimmyAwesome">...</div>

In this example, the directive @widget="kevin" is grouped with key-value arguments @widget:awesome. Directive @widget-x is grouped with @widget-x:size="huge" and @widget-x:awesome="@isJimmyAwesome". I don't expect that we'll see this kind of case that often, but I want to make the semantics clear. The value of giving different separators for arguments (-) and key-value arguments (:) is that these cases are consistent.

Blazor examples

Some motivating examples in the current state:

Event Handlers

Comparison with a component event handler is shown for comparison.

<button @onclick="OnClick"></button>
<MyComponent OnClick="OnClick" />
<button @onclick="@(() => text = "clicked")"></button>
<MyComponent OnClick="@(() => text = "clicked")" />

<!--
    Examples from our previous discussion. 
-->
<button @onclick="OnClick" @onclick:stoppropagation></button>
<input type="text" @onkeydown="KeyPressed" @onkeydown:preventdefault @onkeydown:repeatdelay="500" />

<!--
    Examples from our previous discussion with camelcase
-->
<button @onclick="OnClick" @onclick:stopPropagation></button>
<input type="text" @onkeydown="KeyPressed" @onkeydown:preventDefault @onkeydown:repeatDelay="500" />

Ref

<button @ref="button"></button>
<MyComponent @ref="OnClick" />

<!--
    Possible design for ref that doesn't define a field.
-->
<button @ref="button" @ref:assign></button>
<MyComponent @ref="OnClick" @ref:assign />

Bind

I propose that we simplify bind by requiring the use a key-value argument for specifying the event name. This makes these cases less terse but more obvious. We also get for free the ability to specify the event name using code.

<input type="text" @bind="message" />

<!--
    Note that this wasn't possible to write before without including the attribute name, you had to go from bind to bind-value-oninput.
-->
<input type="text" @bind="message" @bind:event="oninput" />

<!--
    I think this is much more clear that these things are related to each out. 
-->
<input type="text" @bind="message" @bind:format="..." />

<!--
    Today this would be written as `bind-custom-oninput`. 
-->
<input type="text" @bind-custom="message" @bind-custom:event="oninput" />

<!--
    Comparison with a component
-->
<input type="text" @bind="message" />
<MyComponent @bind-Value="message" />

<!--
    Hypothetical future thing
-->
<input type="text" @bind="latlong" @bind:converter="LatLong.Converter"/>

References

Vue.js directives

Vue.js custom directives

AngularJS attribute directives

HTML attribute syntax

WPF Markup Extensions

Previous examples

@SteveSandersonMS
Copy link

SteveSandersonMS commented Apr 22, 2019

This is really good. Thanks for writing it up so clearly! Overall I feel pretty happy about the result.

It took me a while to convince myself that the -* and :* sub-parts were really both needed and it wasn't too arbitrary. I feared that it was too specialized around current "bind" cases. However,

  1. If we look at Vue's syntax, which is probably thought of as the best of the alternatives compared here, it's certainly more arbitrary and restrictive than this.
  2. In practice, the -* sub-part rarely appears. The one common case is for component bindables as we already have today. It will rarely appear on HTML elements.
  3. There may be another way to think about the -* sub-part that clarifies why it's not arbitrary, and maybe gives it a clearer name (see "observation 1: multiple usage")

Observation 1: Multiple usage

One shortcoming I imagined was that there's no general way to enable multiple-usage of the same directive attribute (e.g., if there's no -* sub-part). Imagine you wanted to do this:

<button @onclick="MyMainHandler" @onclick="DoLoggingInBackground">...</button>

This can't be supported because, if there were also some key-value params, we wouldn't know which usage to attach them to. But then maybe the solution is this: the author of the @onclick directive attribute needs to decide whether multiple usage is meaningful or not. If it is and they want to support it, then they should allow the -* subpart, even if they are going to ignore its value. Now you have:

<button @onclick-primary="MyMainHandler" @onclick-another="DoLoggingInBackground">...</button>

... and any key-value params can be disambiguated. In this usage, the -* part is really a group name. The directive handler can choose to give semantic meaning to the group name if it wants (e.g., bind-selectedIndex), but otherwise (and by default) the directive attribute logic can ignore the group name and the system just uses it for grouping any key-value params so as to allow multiple usage.

As for whether we'd actually allow multiple event handlers (@onclick-foo, @onclick-bar), that's a separate question, but it's good to have a syntax that extends to it in case we do want that.

Of course, this way of thinking has implications for autocompletion and compilation errors depending on whether arbitrary group name strings should be allowed, or whether a given directive attribute enforces that only certain ones from a known set are allowed.

Observation 2: Key-value syntax for component params

When people get used to <button @onclick="MyHandler" @onclick:preventDefault="true" />, they will want to do the same thing with custom components, e.g., <SpecialButton OnClick="MyHandler" OnClick:preventDefault="true" />.

One easy way (easy for the runtime at least) we could support this would be something we already planned to do:

[Parameter(Name="OnClick:preventDefault")] bool OnClickPreventDefault { get; set; }

If we do this, it would be really cool if intellisense automatically grouped things based on : separators. Then you could have potentially dozens of Style:LineWidth, Style:Font, ... params, and it wouldn't clutter up the autocompletion list. You'd type sty or similar, press :, and then you'd see the completion options that start with Style:.

@SteveSandersonMS
Copy link

SteveSandersonMS commented Apr 22, 2019

More thoughts:

I suggest for now that the presence of the directive attribute is required in order for the usage of the directive key-value attributes.

I wouldn't be surprised if we ended up changing that. Cases like <input @validate:maxLength="30" /> seem completely plausible. Fine to start with this restriction if it simplifies things for us initially, but if it's actually more work to put in the error checking, perhaps we should consider not having that restriction.

Update: That's probably a bad example, because it would be better expressed as <input @validate-maxLength="30" @validate-maxLength:message="Too long" />. A better example would be <input @bind="MyProp" @validate:event="oninput" />.

This leads to preferring the use of the argument when designing directives

Not saying you're wrong, but the fact that -* parts are just names with no values (or equivalently, a single string subparam whose value can only be hardcoded) makes them pretty limited. I wouldn't be surprised if, in practice, we see a lot more usage of key-values as they are so much more flexible.

If we like this idea, do we like keys as lowercase or camelcase?

Camel case looks more satisfying to me (<a @onclick="DoSomething" @onclick:preventDefault>). I know it's a bit JavaScripty but it certainly aids legibility. Any reason you're not also considering PascalCase? <a @onclick="DoSomething" @onclick:PreventDefault>

<input type="text" @bind="message" @bind:event="oninput" />

I really like this. It's so much better to be able to modify the event without also having to specify -value, which usually didn't mean anything since it's not like anything else would work to do anything useful.

@Mike-E-angelo
Copy link

This is an excellent adjustment. FWIW the more PascalCase you introduce, the more Xaml-based developers you'll win over, IMO. It would be great to see the directives themselves with the ability to be PascalCased as well, e.g. @Bind:Event="OnInput". To be certain, ability and not required, that is, not to introduce any breaking changes. That way both @Bind:Event="OnInput" and @bind:event="oninput" are synonymous, and Xaml-based developers can go with Pascal-case, and web developers can go with lowercase.

Thank you for any consideration and for all your great work here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment