-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}} | ||
{{^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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
<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 if both <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> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 {{#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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an alternative syntax.
There was a problem hiding this comment.
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 :-/
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, i would like to unify these two things.
There was a problem hiding this comment.
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.