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

Named Template Blocks #47

Closed
wants to merge 4 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
126 changes: 126 additions & 0 deletions active/0003-named-template-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
- Start Date: 2015/04/12
- RFC PR:
- ember-cli Issue:

# Summary

Introduce a new Handlebars syntax that would denote beginning and end of a template block.

# Motivation

In Ember 2.0, we're moving towards components becoming primary mechanisms for encapsulating functionality with templates being the mechanism that wires these components together. It would be helpful to allow developers the ability to customize certain portion of component's template from it's block template without having to extend a component or overwrite it's layout.

Currently, we have the ability to do this but it's limited to `{{else}}` helper.

For example, we can customize what `{{each}}` helper presents when the array passed to it is empty.

```
{{#each model as |item|}}
{{item.name}}
{{else}}
No items are available.
{{/each}}
```

`{{if}}` helper has the same behaviour.

We are currently lacking the ability to specify a custom template block that we can consume inside of the component or a helper. This RFC proposes that we introduce the ability to specify custom named template blocks that can be used to customize the default behaviour of a component or a helper.

A table component is a good example of a component that could benefit from this API. Let's consider Addepar's [Ember Table Component](https://github.com/Addepar/ember-table). *Ember Table* has 3 distinct areas that a user might want to cusomize while using the component, namely header, body & footer.

Currently, *Ember Table*'s component layout looks like this

```
{{#if controller.hasHeader}}
{{view Ember.Table.HeaderTableContainer}}
{{/if}}
{{view Ember.Table.BodyTableContainer}}
{{#if controller.hasFooter}}
{{view Ember.Table.FooterTableContainer}}
{{/if}}
{{view Ember.Table.ScrollContainer}}
{{view Ember.Table.ColumnSortableIndicator}}
```

To customize any of these areas, the developer has to extend the component, change specify a new layout and make changes inside of this new layout. If we had the ability to specify custom named template blocks then we could allow the developer to customize one of these areas from the template where the component is being used.

Here is what that might look like,

```
{{#ember-table}}
Copy link
Member

Choose a reason for hiding this comment

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

an alternative syntax.

<ember-table as |t|>
  <t.header as |name|>
    {{name}}
  </t.header>
  <t.cell as |value|>
    {{value}}
  </t.cell>
</ember-table>

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Stef's suggestion looks very similar to the proposed nested helper syntax :-/

Choose a reason for hiding this comment

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

I also noticed that this RFC and #43 aim to resolve the same issue from different aspects. Is that what you refer to, @mixonic ?

Copy link
Author

Choose a reason for hiding this comment

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

@stefanpenner one of the goals of the proposal is to allow the user to overwrite a portion of the default layout. Does what you're suggesting allow for this?

Copy link
Member

Choose a reason for hiding this comment

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

Stef's suggestion looks very similar to the proposed nested helper syntax :-/

yes, i would like to unify these two things.

Choose a reason for hiding this comment

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

@stefanpenner this is a different use case and a different RFC, yielded helpers are about the block placing the content. Named templates are about the parent controlling the rendering via its layout. We need to keep these things separate.

{{^header as |name|}}
{{name}}
{{^cell as |value|}}
{{value}}
{{^}}
Nothing to show.
{{/ember-table}}
```

Specifying a named block inside of the component's block template would override default implementation of that block inside of the component's layout.

# Detailed design

## Expand ^ syntax

Currently, Handlebars implements `^` which means `else`. Infact, `else` is aliased to `^`. We would expand this syntax to allow named portions of template.

Here is an example of what a template with this syntax might look like.

```
{{#table-component items as |item|}}
// default block
{{^header}}
// header content
{{^footer}}
// footer content
{{^}}
// empty view
{{/table-component}}
```

`{{^name}}` serve as dividers of the block template.

## Refactor `{{else}}` helper

One possible implementation would be to make `{{^}}` implementation and allow the name of the helper to be specified.

Handlebars has several built in blocks that are availble on helper's `options` argument, namely `options.fn` & `options.inverse`. These would be changed to `options.blocks.default` & `options.blocks.inverse` respectively. Every other named template block would be available on `options.blocks` hash. The above example would have `options.blocks.header` & `options.blocks.footer` in addition to it's default blocks.

The component hook will append named blocks onto the template as it does currently with default block.

## Block are available in the layout

The component must be able to determine programmatically if it should consume it's default block or use the passed in named block. If we consider the above example, then `table-component`'s layout might look something like this.

```
{{#if blocks.header}}
{{yield-to 'header' headerContent}}
{{else}}
<thead>
{{#each headerContent as |name|}}
<th>{{name}}</th>
{{/each}}
</thead>
{{/if}}
```

`blocks` keyword becomes a reserved keyword and will throw a warning when the component defines a *blocks* property. We already have a reserved `hasBlock` keyword in the template, so this will not be a far stretch.

## Named blocks have block params

Named blocks will have block params and will receive values with a new `{{yield-to}}` helper. `{{yield-to}}` will take name of a block as a first parameter and yield properties as `{{yield}}` does. This will allow the component to expose template friendly values to be used in the named blocks.

```{{yield-to 'header' headerContent}}``` will yield `headerContent` to `header` block.

# Drawbacks

Why should we *not* do this?

# Alternatives
Copy link
Member

Choose a reason for hiding this comment

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

yield-to has no idea what to do if there are multiple named template blocks, imagine the following.

<my-form as |f|>

  <f.input> </f.input>
  <f.input> </f.input>
  <f.input> </f.input>

  <f.save> Save</f.save>
</my-component>

or a table example:

<!-- DSL usage -->
<my-table rows=data as |t|>
  <t.column do |c|>
    <c.header>Name</c.header>
    <c.value> {{name}} </c.value>
  </t.column>

   <t.column do |c|>
    <c.header>Phone</c.header>
    <c.value> {{format-phone phone}} </c.value>
  </t.column>

   <t.column do |c|>
    <c.header>Age</c.header>
    <c.value> {{in-years age}} </c.value>
  </t.column>
</my-table>
<!-- my-table.hbs (implementation) -->
<table>
  <thead>
    <tr>
    {{#each columns as |column|}}
      <td {{action 'toggleSort' column.name}} class="{{column-name}}">{{column.name}}</td>
    {{/#each}}
   </tr>
  </thead>

  <tbody>
    {{#each rows as |row|}}
      <tr class="row-{{row.id}}>
        {{#each columns as |column|}}
          <td {{action "click" row }}>
            {{some-helper column.path.value}}
          </td>
        {{/each}}
     </tr>
    {{#each}}
  </tbody>
</table>

with ^ we also lose lexical scoping.

if both my-tag and my-other-tag have the same named content block, they can not be interleaved.

<my-tag>
  <my-other-tag>
    {{^header}}
    {{^header}}
  </my-other-tag>
</my-tag>

with block params they can.

<my-tag as |t|>
  <my-other-tag as |o|>
    <t.header />
    <o.header />
  </my-other-tag>
</my-tag>

Copy link
Author

Choose a reason for hiding this comment

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

yield-to has no idea what to do if there are multiple named template blocks, imagine the following.

yield-to would not have to deal with multiple scopes because it's scope is limited to the layout. Here is an example,

in app/components/ember-table.hbs

{{#if blocks.header}}
  {{yield-to 'header' header}}
{{else}}
  {{#each header as |column|}}
    {{#if blocks.header-column}}
      {{yield-to 'header-column' column}}
    {{else}}
      {{column.name}}
    {{/if}}
  {{/each}}
{{/if}}
{{#if blocks.rows}}
  {{yield-to 'rows' rows}}
{{else}}
  {{#each rows as |row|}}
    {{if blocks.row}}
         {{yield-to 'row' row}}
      {{else}}
         {{#each row as |column|}}
           {{#if blocks.column}}
             {{yield-to 'column' column}}
           {{else}}
             {{column.name}}
           {{/if}}
        {{/each}}
    {{/if}}
  {{/each}}
{{/if}}

When using the component, the user can specify a custom header-column and column blocks.

{{#ember-table}}
  {{^header-column as |column|}}
    <button {{action 'sort' column.sortKey}}>{{column.name}}</button>
  {{^column as |column|}}
    {{column.value}}
{{/ember-table}}

Even if these components were nested, the named blocks only effect the block that they're dividing, not parent blocks.

{{#ember-table}}
  {{^column as |column|}}
   {{#ember-table content=column.value}}
     {{^column as |column|}}
       {{column.value}}
   {{/ember-table}}
{{/ember-table}}


What other designs have been considered? What is the impact of not doing this?

# Unresolved questions

* Should this be added to Handlebars or should we fork Handlebars?