forked from rust-lang/rfcs
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rust-lang#311 from emberjs/AngleBracketInvocation
Introduce `<AngleBracketInvocationSyntax />`
- Loading branch information
Showing
1 changed file
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
- Start Date: 2018-03-09 | ||
- RFC PR: (leave this empty) | ||
- Ember Issue: (leave this empty) | ||
|
||
# Angle Bracket Invocation | ||
|
||
## Summary | ||
|
||
This RFC introduces an alternative syntax to invoke components in templates. | ||
|
||
Examples using the classic invocation syntax: | ||
|
||
```hbs | ||
{{site-header user=this.user class=(if this.user.isAdmin "admin")}} | ||
{{#super-select selected=this.user.country as |s|}} | ||
{{#each this.availableCountries as |country|}} | ||
{{#s.option value=country}}{{country.name}}{{/s.option}} | ||
{{/each}} | ||
{{/super-select}} | ||
``` | ||
|
||
Examples using the angle bracket invocation syntax: | ||
|
||
```hbs | ||
<SiteHeader @user={{this.user}} class={{if this.user.isAdmin "admin"}} /> | ||
<SuperSelect @selected={{this.user.country}} as |Option|> | ||
{{#each this.availableCountries as |country|}} | ||
<Option @value={{country}}>{{country.name}}</Option> | ||
{{/each}} | ||
</SuperSelect> | ||
``` | ||
|
||
## Motivation | ||
|
||
The original [angle bracket components](https://github.com/emberjs/rfcs/pull/60) | ||
RFC focused on capitalizing on the opportunity of switching to the new syntax | ||
as an opt-in to the "new-world" components programming model. | ||
|
||
Since then, we have switched to a more iterative approach, favoring smaller | ||
RFCs focusing on one area of improvment at a time. Collectively, these RFCs | ||
have largely accomplished the goals in the original RFC without the angle | ||
bracket opt-in. | ||
|
||
Still, separate from other programming model improvements, there is still a | ||
strong desire from the Ember community for the previously proposed angle | ||
bracket invocation syntax. | ||
|
||
The main advantage of the angle bracket syntax is clarity. Because component | ||
invocation are often encapsulating important pieces of UI, a dedicated syntax | ||
would help visually distinguish them from other handlebars constructs, such as | ||
control flow and dynamic values. This can be seen in the example shown above – | ||
the angle bracket syntax made it very easy to see the component invocations as | ||
well as the `{{#each}}` loop, especially with syntax highlight: | ||
|
||
```hbs | ||
<SuperSelect @selected={{this.user.country}} as |Option|> | ||
{{#each this.availableCountries as |country|}} | ||
<Option @value={{country}}>{{country.name}}</Option> | ||
{{/each}} | ||
</SuperSelect> | ||
``` | ||
|
||
This RFC proposes that we adopt the angle bracket invocation syntax to Ember as | ||
an alternative to the classic ("curlies") invocation syntax. | ||
|
||
Unlike the original RFC, the angle bracket invocation syntax proposed here is | ||
purely syntatical and does not affect the semantics. The invocation style is | ||
largely transparent to the invokee and can be used to invoke both classic | ||
components as well as [custom components](https://github.com/emberjs/rfcs/pull/213). | ||
|
||
Since the original angle bracket RFC, we have worked on a few experimental | ||
implementation of the feature, both and in Ember and Glimmer. These experiments | ||
allowed us to attempt using the feature in real apps, and we have learned some | ||
valuable insights throught these usage. | ||
|
||
The original RFC proposed using the `<foo-bar ...>` syntax, which is the same | ||
syntax used by web components (custom elements). While Ember components and web | ||
components share a few similarities, in practice, we find that there are enough | ||
differences that causes the overload to be quite confusing for developers. | ||
|
||
In addition, the code needed to render Ember components is quite different | ||
from what is needed to render web components. If they share the same syntax, | ||
the Glimmer template compiler will not be able to differentiate between the two | ||
at build time, thus requiring a lot of extra runtime code to support the | ||
"fallback" scenario. | ||
|
||
In conclusion, the ideal syntax should be similar to HTML syntax so it doesn't | ||
feel out of place, but different enough that developers and the compiler can | ||
easier tell that they are not just regular HTML elements at a glance. | ||
|
||
## Detailed design | ||
|
||
### Tag Name | ||
|
||
The first part of the angle bracket invocation syntax is the tag name. While | ||
web components use the "dash rule" to distinguish from regular HTML elements, | ||
we propose to use capital letters to distinguish Ember components from regular | ||
HTML elements and web components. | ||
|
||
The invocation `<FooBar />` is equivalent to `{{foo-bar}}`. The tag name will | ||
be normalized using the `dasherize` function, which is the same rules used by | ||
existing use cases, such as service injections. This allows existing components | ||
to be invoked by the new syntax. | ||
|
||
Another benefit of the capital letter rule is that we can now support component | ||
names with a single word, such as `<Button>`, `<Modal>` and `<Tab>`. | ||
|
||
> Note: Some day, we may want to explore a file system migration to remove the | ||
> need for the normalization rule (i.e. also use capital case in filenames). | ||
> However, that is out-of-scope for this RFC, as it would require taking into | ||
> consideration existing code (like services), transition paths and codemods. | ||
### Arguments | ||
|
||
The next part of the invocation is passing arguments to the invoked component. | ||
We propose to use the `@` syntax for this purpose. For example, the invocation | ||
`<FooBar @foo=... @bar=... />` is equivilant to `{{foo-bar foo=... bar=...}}`. | ||
This matches the [named arguments syntax](https://github.com/emberjs/rfcs/pull/276) | ||
in the component template. | ||
|
||
If the argument value is a constant string, it can appear verbatim after the | ||
equal sign, i.e. `<FooBar @foo="some constant string" />`. Other values should | ||
be enclosed in curlies, i.e. `<FooBar @foo={{123}} @bar={{this.bar}} />`. | ||
Helpers can also be used, as in `<FooBar @foo={{capitalize this.bar}} />`. | ||
|
||
#### Reserved Names | ||
|
||
`@args`, `@arguments` and anything that does not start with a lowercase letter | ||
(such as `@Foo`, `@0`, `@!` etc) are reserved names and cannot be used. These | ||
restrictions may be relaxed in the future. | ||
|
||
#### Positional Arguments | ||
|
||
Positional arguments (`{{foo-bar "first" "second"}}`) are not supported. | ||
|
||
### HTML Attributes | ||
|
||
HTML attributes can be passed to the component using the regular HTML syntax. | ||
For example, `<FooBar class="btn btn-large" role="button" />`. HTML attributes | ||
can be interleaved with named arguments (it does not make any difference). This | ||
is a new feature that is not available in the classic invocation style. | ||
|
||
These attributes can be accessed from the component template with the new | ||
`...attributes` syntax, which is available only in element positions, e.g. | ||
`<div ...attributes />`. Using `...attributes` in any other positions, e.g. | ||
`<div>{{...attributes}}</div>`, would be a syntax error. It can also be used on | ||
multiple elements in the same template. If attributes are passed but the | ||
component template does not contain `...attributes` (i.e. the invoker passed | ||
some attributes, but the invokee does not take them), it will be a development | ||
mode error. | ||
|
||
It could be thought of that the attributes in the invocation side is stored in | ||
an internal block, and `...attributes` is the syntax for yielding to this | ||
internal block. Since the `yield` keyword is not available in element position, | ||
a dedicated syntax is needed. | ||
|
||
Classic components (`Ember.Component`) will implicitly have an `...attributes` | ||
added to the end of the wrapper element (if `tagName` is not an empty string), | ||
after any attributes added by the component itself (using `attributeBindings`, | ||
`classNames` etc). This means that attributes provided by the caller will | ||
override (replace) those added by the component (except for `class`, which is | ||
merged). | ||
|
||
### Block | ||
|
||
A block can be passed to the invokee using the angle bracket invocation syntax. | ||
For example, the invocation `<FooBar>some content</FooBar>` is equivalent to | ||
`{{#foo-bar}}some content{{/foo-bar}}`. As with the classic invocation style, | ||
this block will be accessible using the `{{yield}}` keyword, or the `@main` | ||
named argument per the [named blocks RFC](https://github.com/emberjs/rfcs/blob/master/text/0226-named-blocks.md). | ||
|
||
Block params are supported as well, i.e. `<FooBar as |foo bar|>...</FooBar>`. | ||
|
||
There is no dedicated syntax for passing an "else" block directly. If needed, | ||
that can be passed using the named blocks syntax. | ||
|
||
### Closing Tag | ||
|
||
The last piece of the angle bracket invocation syntax is the closing tag, which | ||
is mandatory. The closing tag should match the tag name portion of the opening | ||
tag exactly. If no block is passed, the self-closing tag syntax `<FooBar />` | ||
can also be used (in which case `{{has-block}}` will be false). | ||
|
||
### Dynamic Invocations | ||
|
||
In additional to the static invocation described above (where the tag name is a | ||
statically known component name), it is also possible to use the angle bracket | ||
invocation syntax for dynamic invocations. | ||
|
||
The most common use case is for invoking "contextual components", as shown in | ||
the first example: | ||
|
||
```hbs | ||
<SuperSelect @selected={{this.user.country}} as |Option|> | ||
{{#each this.availableCountries as |country|}} | ||
<Option @value={{country}}>{{country.name}}</Option> | ||
{{/each}} | ||
</SuperSelect> | ||
``` | ||
|
||
Because `Option` is the name of a local variable (block param), the `<Option>` | ||
invocation will invoke the yielded value instead of looking for a component | ||
named "option". | ||
|
||
Similar to curly invocations, most valid Handlebars path expressions are | ||
invokable in this manner: | ||
|
||
```hbs | ||
{{!-- LOCAL VARIABLES --}} | ||
{{#form-for model=user as |f|}} | ||
{{f.fieldset}} | ||
{{f.input name="username" type="text"}} | ||
{{f.input name="password" type="password" }} | ||
{{/f.fieldset}} | ||
{{!-- is equivilant to --}} | ||
<f.fieldset> | ||
<f.input @name="username" @type="text" /> | ||
<f.input @name="password" @type="text" /> | ||
</f.fieldset> | ||
{{/form-for}} | ||
{{!-- NAMED BLOCKS OR CURRIED COMPONENTS --}} | ||
{{@content}} | ||
{{!-- is equivilant to --}} | ||
<@content /> | ||
{{!-- THIS LOOKUP --}} | ||
{{#this.container}} | ||
{{this.child}} | ||
{{/this.container}} | ||
<this.container> | ||
<this.child /> | ||
</this.container> | ||
``` | ||
|
||
> Note: The [named blocks RFC](https://github.com/emberjs/rfcs/blob/master/text/0226-named-blocks.md) | ||
> proposed to use the `<@foo>...</@foo>` syntax on the invocation side to mean | ||
> providing a block named `@foo`, which creates a conflict with this proposal. | ||
> [RFC #317](https://github.com/emberjs/rfcs/pull/317) propose to change the | ||
> block-passing syntax to `<@foo=>...</@foo>` to avoid this conflict. | ||
Notably, based on the rules laied out above, the following is perfectly legal: | ||
|
||
```hbs | ||
{{!-- DON'T DO THIS --}} | ||
{{#let (component "my-div") as |div|}} | ||
{{!-- here, <div /> referes to the local variable, not the HTML tag! --}} | ||
<div id="my-div" class="lol" /> | ||
{{/let}} | ||
``` | ||
|
||
From a programming language's perspective, the semantics here is quite clear. A | ||
local variable is allowed to override ("shadow") another varible on the outer | ||
scope (the "global" scope, in this case), similar to what is possible in | ||
JavaScript: | ||
|
||
```js | ||
let console = { | ||
log() { | ||
alert("I win!"); | ||
} | ||
}; | ||
|
||
console.log("Hello!"); // shows alert dialog instead of logging to the console | ||
``` | ||
|
||
While this is semantically unambigious, it is obviously very confusing to the | ||
human reader, and we don't recommend anyone actually doing this. | ||
|
||
A previous version of this RFC recommended statically disallowing these cases. | ||
However, after giving it more thoughts, we realized it should not be the | ||
programming language's job to dictate what are considered "good" programming | ||
patterns. By statically disallowing arbitrary expressions, it actually makes it | ||
more difficult to learn and understand the underlying programming model. | ||
|
||
Instead, we recommend [including a template linter](https://github.com/ember-cli/rfcs/pull/114) | ||
in the default stack and defer to the linter to make such recommendations. At | ||
minimum, we recommend liniting against invoking local variables with lowercase | ||
names without a path segment, regarless of whether the name actually collide | ||
with a known HTML tag – human readers of an Ember template should be able to | ||
safely assume lowercase tags refer to HTML. | ||
|
||
Eventually, we might want to provide stronger guidance with via the linter. For | ||
example, we may want to recommend capitalizing invokable local variables, as in | ||
`<F.Input />`. We will let the community experiment and coalesce around these | ||
conventions before recommending them by default. | ||
|
||
Finally, there are two exceptions to the general rule where certain technically | ||
valid Handlebars path expressions are not supported for dynamic invocations: | ||
|
||
* Implicit `this` lookups (a.k.a. "property fallback" in [RFC #308](https://github.com/emberjs/rfcs/pull/308) | ||
* Slash lookups | ||
|
||
First, while `{{foo}}` or `{{Foo}}` can normally refer to `{{this.foo}}` or | ||
`{{this.Foo}}` normally, allowing this implicitly lookup will mean _any_ tag | ||
in the template (i.e. `<foo />` or `<Foo />`) can possibly refer to a property | ||
on the current `this` context. | ||
|
||
This ambiguity is highly undesirable for both human readers and the compiler, | ||
therefore implicitly `this` lookup is not allowed in angle bracket invocations. | ||
This explicit form, `<this.foo />` and `<this.Foo />` is required. | ||
|
||
This requirement aligns well with [RFC #308](https://github.com/emberjs/rfcs/pull/308) | ||
and the current curly invocation semantics, due to the ["dot rule"](https://github.com/emberjs/rfcs/blob/master/text/0064-contextual-component-lookup.md#component-helper-shorthand) | ||
that requires a dot in the path. Note that this is actually more restrictive | ||
than the proposed angle bracket invocation semantics, since it is not possible | ||
to invoke a local variable without a dot: | ||
|
||
```hbs | ||
{{#super-select selected={{this.user.country}} as |option|> | ||
{{#each this.availableCountries as |country|}} | ||
{{!-- this is not legal today, since `option` does not contain a dot --}} | ||
{{#option value=country}}{{country.name}}{{/option}} | ||
{{/each}} | ||
{{/super-select}} | ||
``` | ||
|
||
We propose to relax that rule to match the proposed angle bracket invocation | ||
semantics (i.e. allowing local variables without a dot, as well as `@names`, | ||
but disallowing implicit `this` lookup). | ||
|
||
Second, while Handlebars technically allows `{{foo/bar}}` as an equivilant | ||
alternative to the `{{foo.bar}}` path lookup (and therefore `foo/bar` is | ||
technically a valid Handlebars path expression), it will not be supported in | ||
angle bracket invocation. This is both because the `/` conflicts with the HTML | ||
closing tag syntax, and the fact that Ember overrides that syntax with a | ||
different semantic. | ||
|
||
In today's semantics, `{{foo/bar}}` does not try to lookup `this.foo.bar` and | ||
invoke it as a component. Instead, it is used as a filesystem scoping syntax. | ||
Since this feature will be rendered unnecessary with [Module Unification](https://github.com/emberjs/rfcs/blob/master/text/0143-module-unification.md), | ||
we recommend apps using "slash components" to migrate to alternatives provided | ||
by Module Unification (or, alternatively, keep using curly invocations for this | ||
purpose). | ||
|
||
## How we teach this | ||
|
||
Over time, we will switch to teaching angle bracket invocation as the primary | ||
invocation style for components. The HTML-like syntax should make them feel | ||
more familiar for new developers. | ||
|
||
Classic invocation is here to stay – the ability to accept positional arguments | ||
and "else" blocks makes them ideal for control-flow like components such as | ||
`{{liquid-if}}`. | ||
|
||
## Drawbacks | ||
|
||
Because angle bracket invocation is designed for the future in mind, allowing | ||
angle bracket invocations on classic components might introduce some temporary | ||
incoherence (such as the interaction between the attributes passing feature and | ||
the "inner HTML" semantics). However, in our opinion, the upside of allowing | ||
incremental migration outweighs the cons. | ||
|
||
## Alternatives | ||
|
||
We could just stick with the classic invocation syntax. |