Skip to content

Latest commit

 

History

History
393 lines (293 loc) · 14.9 KB

THEORY.md

File metadata and controls

393 lines (293 loc) · 14.9 KB

Ember's This Fallback Behavior

Prior to Ember 4.0, Ember's template syntax included a feature called "This Fallback."

This meant that Ember would interpret a bare variable name like hello as this.hello under certain circumstances.

this fallback
The feature described in this document, which causes Ember to interpret variableName as this.variableName under certain circumstances. This feature was removed in Ember 4.0.
variable reference
A valid Handlebars variable name (such as hello, varName, or hello-world)
local variable reference
A variable reference that refers to a Handlebars local variable binding (created using as |varName|).
this refererence
A variable reference that refers to the current this context of the template.
Named Argument Reference ("at-name")
A variable reference that refers to a named argument using @{identifier} syntax.
Ambiguous variable reference
A variable reference that is not a local variable reference, a this reference, or a named argument reference.
Property Path
A dot-separated path that begins with a variable reference. This variable reference segment is a candidate for this fallback behavior under the circumstances described in this document.

Important

Only ambiguous variable references are candidates for this fallback behavior.

This excludes:

  1. local variable reference
  2. this reference
  3. named argument reference

Lexical Scope Intuition

The lexical scope of a classic Handlebars template has:

  1. Local variables, bound with as |var-name| and referenced via path expressions (var-name or var-name.property...). These variables are in the local namespace.
  2. Global variable references, in one of the following namespaces:
    1. html
    2. component
    3. helper
    4. modifier
    5. value
    6. ambiguous::content
    7. ambiguous::attr-value
    8. keyword (such as yield, if, etc.)

Important

The namespaces are determined by the parser and are purely syntactic. For the most part, they map onto straight-forward user intuitions about what the syntax means. The ambiguous namespaces are an exception: they are ambiguous from a human understanding perspective, and this fallback behjavior makes them worse.

Global variables in the value namespaces have this fallback behavior (in Ember 3.28), as do global variables in the ambiguous namespaces.

No other namespaces have this fallback behavior.

Global variable references in the value and ambiguous namespaces have this fallback behavior in Ember 3.28, but it was removed in Ember 4.0.

Note

Variable references with this fallback behavior are annotated with ^^^.

<SomeComponent @arg={{some-var}} />
{{!                   ^^^^^^^^}}

{{#let (hash component=@some.component value=12) as |t|}}
  <t.component @arg1={{some-var}} @arg2={{(some-helper t.value some-var)}} 
{{!                    ^^^^^^^^                                ^^^^^^^^}}
{{/let}}

<div title={{maybe-helper}} role={{get-role some-var}}>
{{!          ^^^^^^^^^^^^                   ^^^^^^^^}}
  {{yield}}
  {{very-ambiguous}}
{{! ^^^^^^^^^^^^^^}}
</div>

{{#let (fields-for @model) as |f|}}
  <f.input @field="user" />
  {{!-- since f _is_ in scope, f is a normal local variable and
        there is no _this fallback_ behavior --}}
{{/let}}

<some.Component />
{{! since some is not in local scope, this is a ❗️syntax error❗️}}

These examples are equivalent to this template, with namespaces made explicit (and prefixed with %):

Note

These resolution rules exist in the parser, in largely this form. The elaborated syntax was created for this explanation and doesn't exist elsewhere (yet?).

In this example, variable references with this fallback behavior are annotated with ^^^ and their associated namespace is annotated with ~~~.

<%component@SomeComponent @arg={%value@some-var} />

{{%keyword@let (%keyword@hash component=@some.component value=12) as |%local@t|}}
  <%local@t.component
    @arg1={{%value@some-var}}
  {{!       ~~~~~~~^^^^^^^^ some-var has _this fallback_ behavior}}
    @arg2={{(%helper@some-helper %local@t.value %value@some-value)}}
  {{!--                                         ~~~~~~~^^^^^^^^^^
                                                some-value has _this fallback_ behavior
  --}}  
  />
{{/%keyword@let}}

<html@div
  title={{%ambiguous::attr-value@maybe-helper}}
  {{!--   ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ 
          maybe-helper has _this fallback_ behavior, but only if maybe-helper does not
          resolve to a helper
  --}}
  role={{%helper@get-role %value@some-var}}
  {{!                     ~~~~~~~^^^^^^^^ some-var has _this fallback_ behavior}}
>
    {{%keyword@yield}}
    {{%ambiguous::content@very-ambiguous}}
{{!-- ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^ 
      very-ambiguous has _this fallback_ behavior, but only if very-ambiguous does
      not resolve to a component or helper
--}}
</html@div>

{{#%keyword@let (%helper@fields-for @model) as |%local:f|}}
  <%local:f.input @field="user" />
{{/let}}

High-Level Intuition

The basic intuition of this fallback behavior is:

When a piece of syntax that could be a path expression is used as a value, and the variable at the front of the path is not in local Handlebars scope, it falls back to this.var-name if necessary.

This fallback behavior happens far less frequently than you might expect, and when it happens, the consequences are fairly constrained.

This is because:

  1. It doesn't apply to unambiguous syntax like <ComponentName>, {{modifier-name}} or (helper-name with args).
  2. When it applies to arguments, this fallback behavior is the only option after ruling out local variables.
  3. When it applies to ambiguous syntax, it only applies after determining that there is no component or helper with that name.

This means that it applies to only these scenarios, and only if var-name is not in local scope:

  1. In content position ({{var-name}}), if there is no component or helper named var-name, and only if there are zero arguments.
  2. In attribute value position (attr={{var-name}}), if there is no helper named var-name, and only if there are zero arguments.
  3. In any argument position ((some-helper this or.this or=even.this)). But since this only applies if the variable reference is not in scope, it's possible to determine, completely statically, when the rule applies. TL;DR it only applies if the variable is not in local scope, and the only possible fix is to prefix this..

Syntax Positions

The behavior of this fallback is different depending on the syntactic position. This section describes the relevant differences.

Angle-Bracket Callee (<ComponentName />)

Namespace component
This fallback behavior Never

An HTML tag name that syntactically meets one of these criteria is a Glimmer Property Path.

  1. begins with a capital letter
  2. is a path that begins with local variable reference
  3. is a path with at least two segments (i.e. contains a dot)

Important

None of these variable references are candidates for this fallback behavior.

{{!-- 
  ⛔️ None of these examples are candidates for this fallback behavior ⛔️
}}

{{! the path is `ComponentName`}}
<ComponentName />

{{! the path is `f.input` and the variable reference is `f`}}
<f.input />

{{! the path is @arg}}
<@arg />

{{! the path is @arg.property and the variable reference is `@arg`}}
<@arg.property />

{{#let @arg.property as |comp|}}
  {{! the path is comp}}
  <comp />

  {{! the path is comp.child and the variable reference is `comp`}}
  <comp.child>
{{/let}}

Modifier Callee (<ComponentName {{autofocus}} />)

Namespace modifier
This fallback behavior Never

The callee of a modifier invocation is always a Glimmer Property Path.

Important

None of these variable references are candidates for this fallback behavior.

It doesn't matter whether the modifier has any arguments.

{{!-- 
  ⛔️ None of these examples are candidates for this fallback behavior ⛔️
}}

{{! the path is `modifier-name`}}
<div {{modifier-name}} />

{{! the path is `name.modifier` and the variable reference is `name`}}
<div {{name.modifier}} />

{{! the path is @arg}}
<@arg />

{{! the path is @arg.property and the variable reference is `@arg`}}
<@arg.property />

{{#let @arg.property as |comp|}}
  {{! the path is comp}}
  <comp />

  {{! the path is comp.child and the variable reference is `comp`}}
  <comp.child>
{{/let}}

Helper Callee

Namespace helper
This fallback behavior Never

When a syntax is unambiguously a helper invocation, its callee is always a Glimmer Property Path.

A syntax is unambiguously a helper invocation if it meets one of the following criteria:

  1. It is the callee in a subexpression (i.e. (helper-name) or (helper-name arg), regardless of whether the subexpression has any arguments.
  2. It is the callee in {{curly syntax}} and it has at least one positional or named argument. This includes attribute values.

Tip

The intuition is: "it looks like a call expression and isn't an [ambiguous invocation][#ambiguous-invocation].

Ambiguous Content

Namespace ambiguous:content
This fallback behavior If the variable name is not a global helper or component

Ambiguous content:

  1. appears in content position
  2. consistents of only a single ambiguous variable reference (the ambiguous name), and has no arguments of any kind.
  3. is not an Ember content keyword such as {{yield}}.

If the ambiguous name is var-name, this syntax ({{var-name}} in content position) could mean any of the following:

  1. Invoke a helper named var-name
  2. Invoke a component named var-name
  3. (this fallback) Look up this.var-name (removed in Ember 4.0)

Ambiguous Attribute Value

Namespace ambiguous:attr
This fallback behavior If the variable name is not a global helper

An ambiguous attribute value:

  1. appears in attribute value position
  2. consistents of only a single ambiguous variable reference (the ambiguous name), and has no arguments of any kind.
  3. is not an Ember helper keyword (such as if).

If the ambiguous name is var-name, this syntax (attr={{var-name}} in content position) could mean:

  1. Invoke a helper named var-name
  2. (this fallback) Look up this.var-name (removed in Ember 4.0)

[!NOTE] In Ember 4.0, this syntax is unambiguous.

Argument Position

Namespace value
This fallback behavior Always

Tip

Intuitively, an argument position is an argument passed to any kind of invocation.

The argument position syntax is any of these:

  1. Positional arguments in all curly syntaxes (components, helpers, modifiers, content, etc.) and subexpressions.
  2. Argument values passed as named arguments in all curly syntaxes and subexpressions.
  3. Argument values passed as named arguments via angle-bracket component invocations.

1. Positional Arguments in Curly Syntaxes and Subexpressions

{{any-curly positional bare or.path}}
{{!         ^^^^^^^^^^ ^^^^ ^^~~~~~}}

{{any-curly (some-helper bare or.path (also nested or.path))}}
{{!                      ^^^^ ^^~~~~~       ^^^^^^ ^^~~~~  ~}}

{{! examples of any-curly }}

{{component-name positional bare or.path}}
{{!              ^^^^^^^^^^ ^^^^ ^^~~~~~}}
{{component-name (some-helper positional bare or.path (also nested or.path))}}
{{!                           ^^^^^^^^^^ ^^^^ ^^~~~~~       ^^^^^^ ^^~~~~~ }}

<div {{modifier-name positional bare or.path}} />
{{!                  ^^^^^^^^^^ ^^^^ ^^~~~~~    }}
<div {{modifier-name (some-helper positional bare or.path (also nested or.path))}} />

<Component class={{some-cx positional bare or.path}} />
{{!                        ^^^^^^^^^^ ^^^^ ^^~~~~~}}
<Component class={{some-cx (some-helper positional bare or.path (also nested or.path))}} />
{{!                                     ^^^^^^^^^^ ^^^^ ^^~~~~~       ^^^^^^ ^^~~~~~ }}

[!INFO]

The variable reference is annotated via ^^^ and the rest of the path expression is annotated via ~~~.

2. Named Arguments in Curly Syntaxes

{{any-curly a=value b=or.path c=(any-helper a=nested b=or.path) }}
{{!           ^^^^^   ^^~~~~~                 ^^^^^^   ^^~~~~~  }}

{{! examples of any-curly }}

{{component-name a=value b=or.path}}
{{!                ^^^^^   ^^~~~~~}}
{{component-name (some-helper a=value b=or.path) c=(any-helper a=nested b=or.path)}}
{{!                             ^^^^^   ^^~~~~~                  ^^^^^^   ^^~~~~~ }}

<div {{modifier-name a=value b=or.path}} />
{{!                    ^^^^^   ^^~~~~~    }}
<div {{modifier-name (some-helper a=value b=or.path) c=(any-helper a=nested b=or.path)}} />
{{!                                 ^^^^^   ^^~~~~~                  ^^^^^^   ^^~~~~~ }}

<Component class={{some-cx a=value b=or.path}} />
{{!                          ^^^^^   ^^~~~~~   }}
<Component class={{some-cx (some-helper a=value b=or.path) c=(any-helper a=nested b=or.path)}} />
{{!                                       ^^^^^   ^^~~~~~                  ^^^^^^   ^^~~~~~    }}

3. Named Arguments in Angle-Bracket Syntaxes

<Component @a={{value}} @b={{or.path}} />
{{!             ^^^^^        ^^~~~~~   }}

<Component @a={{value}} @b={{or.path}} />
{{!             ^^^^^        ^^~~~~~   }}

<Component @named={{(any-helper a=nested b=or.path)}} />
{{!                               ^^^^^    ^^~~~~~    }}