Skip to content

Latest commit

 

History

History
272 lines (208 loc) · 8.12 KB

0000-let.md

File metadata and controls

272 lines (208 loc) · 8.12 KB
  • Start Date: 2017-01-14
  • RFC PR: (leave this empty)
  • Ember Issue: (leave this empty)

Summary

Introduce a let keyword to bind values to names within a handlebars template.

Block form:

{{#let "Milk" "Cereal" as |one two|}}
  Breakfast:
  1. {{one}}
  2. {{two}}
{{/let}}

or inline:

{{let one="Milk" two="Cereal"}}

Breakfast:
1. {{one}}
2. {{two}}

Motivation

The primary method for supplying named values to a template is via properties on its backing scope (either a controller or a component). These properties are specified in JavaScript which, while as a mechanism is very well understood by the community, also comes with several drawbacks.

The first is that it spreads the requisite knowledge to understanding a template across two places. In other words, it is impossible to understand what exactly it does without simultaneously holding both the .hbs and .js files together and continuously merging them with your mind. The lack of this overhead is one of the commonly cited strengths of JSX over Handlebars.

Another is that computed properties are error-prone because of their requirement to explicitly enumerate dependencies. If the computation of a property has incorrectly specified dependent keys it will break in mysterious ways that usually involve a sudden and hard-to-reproduce lack of reactivity to changing inputs.

In order to avoid these pitfalls, developers have begun moving more and more computation into templates, and the rise of popular of addons like ember-truth-helpers, ember-math-helpers and ember-composable-helpers are strong evidence of this pressure.

For example:

<button disabled={{or isPending isError}}>Submit</button>

This solves the problem of proximity by placing the derivation of a value right next to where it's used. There's no question about how the disabled attribute is computed. It also solves the dependency problem because the dependencies of an expression are implicity enumerated merely through the act of expressing it.

While these are advantages that a large portion of the community finds empowering, they do not come without drawbacks.

If an expression becomes sufficiently complex, then it actually begins to reduce clarity rather than improve it; especially when you end up using it more than once.

Consider:

Welcome back {{concat (capitalize person.firstName) ' ' (capitalize person.lastName)}}

Account Details:
First Name: {{capitalize person.firstName}}
last Name: {{capitalize person.lastName}}

The moment you need to re-use values, or compose them from constituent values that are themselves re-used, you need to unroll the whole thing and move towards composition via computed properties.

The let helper relieves this pressure by allowing you to bind the value of computation to a name so that it can be used to derive subsequent values. It preserves the advantages of proximity and dependency management, while retaining the re-use and composability of computed properties.

With let, the above example can be re-written as:

{{let firstName=(capitalize person.firstName) lastName=(capitalize person.lastName)}}

Welcome back {{concat firstName ' ' lastName}}

Account Details:
First Name: {{firstName}}
last Name: {{lastName}}

By allowing you to bind a computation to a name, the let helper lets you define computed properties right within your template.

Detailed design

The let keyword is implemented as a block helper that takes its positional parameters and yields them on to passed block. In this way, it is very similar to the with keyword (in fact, the original implementation was cribbed from there). The only difference is that let will always render its block. Whereas with will not render if it is passed an empty list, null, undefined, false, or an empty string. This counterintuitive behavior leads to nasty suprises and difficult to workaround situations.

{{#with (array) as |list|}}
  {{!does not render}}
  You'll never see me!
{{/with}}

On the other hand, let binds all values equitably.

{{#let (array) as |list|}}
  Hello there!
{{/let}}

An inline form is also supported via a syntax transform, so

{{let a=1 b=2}}
a + b = {{sum a b}}

expands to

{{#let 1 2 as |a b|}}
  a + b = {{sum a b}}
{{/let}}

Like let in JavaScript and most functional languages, bindings can be "temporarily" overriden within a scope, and then "restored" once they pass out of scope:

{{let a=1 b=2}}
a + b = {{sum a b}} <!-- 3 -->
<div>
  {{let a=5}}
  a + b = {{sum a b}} <!-- 7 -->
</div>
a + b = {{sum a b}} <!-- 3 -->

Since this expands to (approximately):

{{#let 1 2 as |a b|}}
  a + b = {{sum a b}} <!-- 3 -->
  <div>
    {{#let 5 as |a|}}
      a + b = {{sum a b}} <!-- 7 -->
    {{/let}}
  </div>
  a + b = {{sum a b}} <!-- 3 -->
{{/let}}

For a reference implementation. See https://github.com/thefrontside/ember-let

How We Teach This

let is an extraordinarily common concept for programmers of all stripes, and so while it is new to Ember templates, it still enjoys the familiarity only 60 years of ubiquity can bring. It makes sense to lean on that shared understanding to introduce it to the Ember community. It's about binding a name to value full stop.

At the same time, Embereños are familiar with computed properties, and in many cases you could use a let expression instead of a computed property. In other words, let performs a very similar function to a computed property: it assigns a referencable name to a value that is purely derived from other values.

Anywhere in the guides that uses with would be a candidate for using let instead, but it would also be helpful to have a section on "when to use computed properties" and "when to use let" since let is not a silver bullet. An audit of all the examples contained in the guides with an eye to how they might be improved via the use of let or confirmed as much better using CPs would serve to mark and understand these use cases.

Drawbacks

This knocks down some barriers that today keep people from declaring more computation in their templates. While there are stronger guarantees of purity with computations performed in handlebars, it does introduce a fundamental question that developers will need to ask themselves when specifying each computation: "should I put this in a template or should I put it in my controller / component?" That overhead as deveolpers attempt to figure out where that line is drawn could be cumbersome.

Another potential drawback is that computations declared in Handlebars cannot be shared back to JavaScript except via an action. We might find computations duplicated in templates that really could be avoided by having a strong shared model with computed properties.

Alternatives

Doing nothing is one approach. with is almost let, and does handle many simple cases.

{{#with (hash one="One" two="Two" three="Three") as |h|}}
  <p>I've been to {{h.one}} beach, {{h.two}} coffee shops and {{h.three}} bars.
{{/with}}

See the Detailed design section for the caveats of using with

Unresolved questions

As proposed, let bindings happen in parallel, which is to say, binding expressions within a let expression are invisible to each other:

{{let a=1 b=(sum a 5)}} <!-- `a` is not visible yet!!! -->

Many languages have a let* form which binds values in sequence which can be nice if you have lots of dependent derived values:

{{let* a=1 b=(sum a 5)}} <!-- `b`'s binding expression sees `a` -->
b: {{b}} <!-- 6 -->

Many languages also have a function similar to Ember's with called if-let which only binds and evaluates its body if its test is truthy. Something to consider is deprecating with in favor of if-let to align Ember with wider programming practice and re-enforce what with is actually doing.

{{#if-let someValue as |value|}}
  Hallo!
{{/if-let}}

<button onclick={{action (mut someValue) true}}>
  Now you see me
</button>

<button onclick={{action (mut someValue) false}}>
  Now you don't
</button>