Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local template blocks (as a precursor to block slot syntax) #199

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions text/0000-local-template-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
- Start Date: 2017-01-17
- RFC PR: (leave this empty)
- Ember Issue: (leave this empty)

# Summary

There is strong demand for the ability to pass in template blocks into higher components as a means to configure/override how/what they render to DOM, but unfortunately other proposals to that effect have failed to gain traction. This RFC proposes a more general-purpose syntax for defining locally-named template blocks that can be passed into higher order components as attrs.

# Motivation

The need/demand for "named yields"/"block slots" is well-established:

- https://github.com/emberjs/rfcs/pull/43
- https://github.com/emberjs/rfcs/pull/72
- https://github.com/emberjs/rfcs/pull/193
- https://github.com/ciena-blueplanet/ember-block-slots
- https://github.com/ryanto/ember-content-for
- https://github.com/knownasilya/ember-named-yields

In short, people want a way to pass in more than just the default/inverse template blocks, an unfortunate limitation to the flexibility/power of higher order components, and which generally speaking hurts the composability story of Ember components.

The problem with all of the above approaches (aside from the fact that they hack around the lack of such an officially-supported syntax/approach) is that they miss an important and more general use case, which is the ability to define a named local template block and pass it to multiple components, e.g.:

```hbs
{{#let-block sharedHeader as |person|}}
{{person.name}}
{{/let-block}}

{{complex-component
header=sharedHeader}}
{{complex-component
header=sharedHeader}}
{{complex-component
header=sharedHeader}}
```

None of the proposals I'm aware of support defining `sharedHeader` and passing it to multiple separate components; the most they can do is pass a named block to a higher order component and let the higher order component render it multiple times. It's a subtle but important limitation.

So, given this limitation, and that these proposals have languished, I suggest we ship a more basic / verbose but more fully-featured / flexible syntax today and build a nicer block-slot syntax based off of it in a separate RFC.

# Detailed design

I propose two new additions / primitives to the present-day templating API:

## 1. let-block

`let-block` is a Glimmer built-in that defines a local variable/constant within the current lexical scope that holds a reference to a block that can be rendered multiple times.

```hbs
{{#let-block fooBlock as |a b c|}}
Hello {{a}}, {{b}}, and {{c}}!
{{/let-block}}

{{x-bar headerBlock=fooBlock}}
{{x-wat headerBlock=fooBlock footerBlock=fooBlock}}
```

`let-block` declarations behave similar to block params in that a reference to `fooBlock` in the above template will always reference the block declared by `let-block` and never try and do a property lookup of `fooBlock` on the context.

`let-block` declarations are hoisted to the top of the current lexical scope, like classic JS function declarations; this means it's possible to reference a block _above_ where it is defined.
Copy link
Member

@GavinJoyce GavinJoyce Jan 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a use case for hoisting let-blocks?

this means it's possible to reference a block above where it is defined

This may be confusing as it seems to be inconsistent with JavaScript let hoisting and temporal dead zones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I go back and forth on this. If we don't hoist then we have to do one of the following:

  1. References to fooBlock before let-block fooBlock will refer to some outer scope, which seems bad and surprising
  2. We detect that there is a let-block fooBlock and throw a compile error if fooBlock is referenced above the let-block. Maybe this is fine?

Copy link
Contributor

@knownasilya knownasilya Jan 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just rename the helper? Like {{#define-block

Copy link
Member

@GavinJoyce GavinJoyce Jan 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can enforce let like semantics in the compiler this would simplify the conceptual model quite a bit I think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I much prefer the second option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ember-let, inline-let's declaration last until the nearest block/element terminates. This aligns nicely with how people commonly indent their Handlebars source.

Here's some examples: thefrontside/ember-let#12

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmun Doesn't the inline form of ember-let do Option 1, e.g. before the let, references to a or b will "pierce" the lexical scope?


```hbs
{{x-bar headerBlock=fooBlock}}
{{x-wat headerBlock=fooBlock footerBlock=fooBlock}}

{{! define fooBlock after it's referenced }}

{{#let-block fooBlock as |a b c|}}
Hello {{a}}, {{b}}, and {{c}}!
{{/let-block}}
```

## 2. {{yield to=referenceToBlock}}

`let-block` lets you declare blocks, but we still need a way to render them. I propose extending the the `yield` helper syntax:

```hbs
{{#let-block fooBlock as |a b c|}}
Hello {{a}}, {{b}}, and {{c}}!
{{/let-block}}

{{! render fooBlock here...}}
{{yield 'A' 'B' cValue to=fooBlock}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if you'd be able to render a block simply by placing it in the eval position of an expression:

{{fooBlock 'A' 'B' cValue}}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even {{my-comp customBlock=(fooBlock 'a' 'b')}} where the component provides only 'c'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cowboyd That syntax may or may not involve to huge of changes to Glimmer... i.e. it'd be hard to add that feature without possibly opening the Pandora's box of a generic Renderables/Invocables interface (which might sound nice but might prematurely bloat the scope of this RFC.

@knownasilya Hmm, you know, you've given me an idea... gonna follow up in top-level comment and mention you there.


{{! ...and again here }}
{{yield 'Ay' 'Bee' 'Cccc' to=fooBlock}}
```

This allows you to render a block whether it's in the same lexical scope, or you've somehow been passed a reference to such a block.

To mimic the behavior of `to="inverse"`, we should probably render nothing if the value passed to `to=` is falsy, but we should probably error if passed an object other than a value produced by `let-block`.

# How We Teach This

I think "named block" is simple and accurate; the only downside is that people familiar with the various RFCs might assume that "named block" refers to specific API for passing named blocks into higher order components, but generally speaking I think it's a new and obvious addition to vocabulary. Other RFCs proposing similar-ish things should probably call what they're providing "slot syntax".

We teach this as a continuation of contextual components and Ember's composability story. This RFC introduces syntax that addresses the heavy requirement that once you go beyond what can be expressed in main/inverse block syntax, you're required to create components/templates that live in separate files, which unnecessarily adds a lot of confusing indirection.

> Would the acceptance of this proposal mean the Ember guides must be re-organized or altered? Does it change how Ember is taught to new users at any level?

This feature could documented in the Guides within or alongside https://guides.emberjs.com/v2.10.0/components/block-params/

# Drawbacks

The biggest drawback is that this proposal is NOT the block slot syntax everyone's been waiting for, and even though it hit more use cases, when applied to the use cases most demanding of block slot syntax, you can wind up with a situation where you declare a bunch of local blocks in a row, and _then_ you pass it to the higher order component, e.g.

```hbs
{{#let-block header as |p|}}
I am header content {{p}}
{{/let-block}}
{{#let-block footer as |p|}}
I am footer content {{p}}
{{/let-block}}
{{#let-block body as |p|}}
I am body content {{p}}
{{/let-block}}
{{complex-component header=header footer=footer body=body}}
```

Most people (including myself) would prefer that in cases where a local block is used only once, it would be better to have a syntax where the blocks are nested in the call to render `complex-component`. The reason I'm OK with this drawback is that, as mentioned earlier, once you want to use a block in multiple places, a syntax specific to block-slots lets you down. It is my hope that another RFC builds off of this one to specify a block-slot syntax that desugars into `let-block` semantics.

The other drawback is that, unlike block params, which are syntactically guaranteed to appear at the top of the scope in which they're introduced, `let-block` syntax doesn't enforce such an order and relies on messy `var`-ish lexical hoisting.

# Alternatives

We could use the classic Handlebars `@data` sigil to name the blocks so that they stick out more.

Another alternative is to say "declaring a local named block and passing it in to multiple places doesn't have strong enough use-cases outside of what could be done if we nailed the block slot syntax". But I worry that those proposals may never land, and I think landing this RFC first would give those proposals a nice target to desugar to.

# Unresolved questions

Is the hoisting behavior a terrible idea? Is there some reason this couldn't work in glimmer?

Would it be possible to pass in a local template block as the `layout` of a component? In such a case, should we support the `yield` helper inside such a template block?