Skip to content
Merged
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
250 changes: 177 additions & 73 deletions text/0000-contextual-component-lookup.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,126 +4,230 @@

# Summary

Currently, you can only invoke components using a global name. The goal of this
RFC is to allow better composition of components by allowing components
to be referenced through property lookups on the current scope. For example,
The goal of this RFC is to allow for better component composition and the
usage of components for domain specific languages.

```hbs
{{this.localInput value=post.title}}
```
Ember components can be invoked three ways:

* `{{a-component`
* `{{component someBoundComponentName`
* `<a-component` (coming soon!)

In all these cases, attrs passed to the component must be set at the place of
invocation. Only the `{{component someBoundComponentName` syntax allows for the name
of the component invoked to be decided elsewhere.

All component names are resovled to components through one global resolution
path.

To improve composition, four changes are proposed:

where `localInput` is a property whose value is a component factory.
* The `(component` helper will be introduced to close over component attrs in
a yielding context.
* The `{{component` helper will accept an argument of the object created by
`(component` for invocation (as it invokes strings today).
* Property lookups with a value containing a dot will be considered for
rendering as components. `{{form.input}}` would be considered, for instance.
Helper invocations with a dot will also be treated like a component if the
key has a value of a component, for instance `{{form.input value=baz}}`.
* A `(hash` helper will be introduced.

# Motivation

The primary motivation is to support robust communication channels between
components, typically between a parent component and its children. The current
best paractice is for a child component, upon creation, to walk up the view
tree to find it's parent using `nearestOfType`.
When building a complex UI from several components, it can be difficult to
share data without breaking encapsulation. For example this template:

Sometimes `nearestOfType` is exactly what you want, but often times it is not.
Usuaully, you'd just prefer to have the component be instantiated with a
reference to its parent. Consider a case with nested components:
```hbs
{{#great-toolbar role=user.role}}
{{great-button role=user.role}}
{{/great-toolbar}}
```

Causes the user to pass the `role` data twice for what are obviously related
components. A component can yield itself down:

```hbs
{{#parent-component}}
{{#parent-component}}
{{child-component}}
{{/parent-component}}
{{/parent-component}}
{{! app/components/great-toolbar/template.hbs }}
{{yield this}}
```

In this case, `nearestOfType` can't be used to hook the child component up the
outer parent component. This is a similar problem that existed with context-
changing `{{each}}`, which block params solved.
```hbs
{{#great-toolbar role=user.role as |toolbar|}}
{{great-button toolbar=toolbar}}
{{/great-toolbar}}
```

## `form-for` Example
And `great-button` can have knowledge about properties on `great-toolbar`, but
this break the isolation of components. Additionally the calling syntax is not
much better, `toolbar` must still be passed to each downstream component.

This section describes an example of how you'd use this technique to
yield contextualized components in a form building component.
Often `nearestOfType` is used as a workaround for these limitations. This API
is poorly performing, and still results in the downstream child accessing the
parent component properties directly.

Let's say we want our users to write
Consequently there is a demand by several addons for improvement. Our goal
is a syntax similar to DSLs in Ruby:

```hbs
{{#form-for post as |f|}}
{{f.input 'title'}}
{{f.input 'body'}}
{{f.submit}}
{{/form-for}}
{{#great-toolbar role=user.role as |toolbar|}}
{{toolbar.button}}
{{toolbar.button orWith=additionalProperties}}
{{/great-toolbar
```

then our component template and class could look like
As laid out in this proposal, the `great-toolbar` implementation would look
like:

```hbs
{{! templates/components/form-for.js }}
{{! app/components/great-toolbar/template.hbs }}
{{yield (hash
button=(component 'great-button' role=user.role)
)}}
```

# Detailed design

{{yield controls}}
### The `(component` helper and `{{component` helper

Much like `(action` creates a closure, it is proposed that the `(component`
helper create something similar. For example with actions:

```hbs
{{#with (action "save" model) as |save|}}
<button {{action save}}>Save</button>
{{/with}}
```

and
The returned value of the `(action` nested helper (a function) closes over the
action being called (`actions.save` on the context and the `model` property).
The `{{action` helper can accept this resulting value and invoke the action
when the user clicks.

```js
// components/form-for.js
The `(component` helper will close over a component name. The
`{{component` helper will be modified to accept this resulting value and invoke
the component:

import Ember from 'ember';
import { curry } from 'factory-utils';
```hbs
{{#with (component "user-profile") as |uiPane|}}
{{component uiPane}}
{{/with}}
```

Additionally, a bound value may be passed to the `(component` helper. For
example `(component someComponentName)`.

export default Ember.Component.extend({
controls: Ember.computed(function() {
var inputFactory = this.container.lookupFactory('component:form-for-input');
var submitFactory = this.container.lookupFactory('component:form-for-submit');
Attrs for the final component can also be closed over. Used with yield, this
allows for the creation of components that have attrs from other scopes. For
example:

```hbs
{{! app/components/user-profile.hbs }}
{{yield (component "user-profile" user=user.name age=user.age)}}
```

return {
input: curry(inputFactory, { form: this }),
submit: curry(submitFactory, { form: this })
};
})
});
```hbs
{{#user-profile user=model as |profile|}}
{{component profile}}
{{/user-profile}}
```

where `curry` is a simple helper which wraps the factory and assigns the extra
properties to the instance.
Of course attrs can also be passed at invocation. They smash any conflicting
attrs that were closed over. For example `{{component profile age=lyingUser.age}}`

In the future we can use the injection API to avoid directly accessing the
container.
Passing the resulting value from `(component` into JavaScript is permitted,
however that object has no public properties or methods. Its only use would
be to set it on state and reference it in template somewhere.

# Detailed design
### Hash helper

Any component invocation with a dot separator `.` in the name of the component
will perform a lookup of this property on the scope. The value must be a factory
and the factory must create component instances, otherwise an error is thrown.
Unlike values, components are likely to have specific names that are semantically
relevent. When yielded to a new scope, allowing the user to change the name
of the component's variable would quickly lead to confusing addon documentation.
For example:

```hbs
{{#with (component "user-profile") as |dropDatabaseUI|}}
{{component dropDatabaseUI}}
{{/with}}
```

The affected syntaxes are:
The simplest way to enforce specific names is to make building hashes
of components (or anything) easy. For example:

```hbs
{{#with (hash profile=(component "user-profile")) as |userComponents|}}
{{component userComponents.profile}}
{{/with}}
```
{{this.component}}
{{this.component name=name}}
<this.component />
<this.component name="{{name}}" />

The `(hash` helper is a generic builder of objects, given hash arguments. It
would also be useful in the same manner for actions:

```hbs
{{#with (hash save=(action "save" model)) as |userActions|}}
<button {{action userActions.save}}>Save</button>
{{/with}}
```

# Drawbacks
### Component helper shorthand

In the HTML component case, it feels a little weird that the tag name does a
property lookup without being surrounded curlies. That said, I like the simplicity.
To complete building a viable DSL, `.` invocation for `{{` components will be
introduced. For example this `{{component` invocation:

An alternative syntax (that I'm not really a fan of) is
```hbs
{{#with (hash profile=(component "user-profile")) as |userComponents|}}
{{component userComponents.profile}}
{{/with}}
```

Could be converted to drop the explicit `component` helper call.

```hbs
<{{this.component}} name="{{name}}" />
{{#with (hash profile=(component "user-profile")) as |userComponents|}}
{{userComponents.profile}}
{{/with}}
```

A component can be invoked like this only when it was created by the
`(component` nested helper form. For example unlike with the `{{component`
helper, a string is not acceptable.

To be a valid invocation, one of two criteria must be met:

* The component can be called as a path. For example `{{form.input}}`.
* The component can be called as a helper. For example `{{form.input value=baz}}`

And of course a `.` must be present in the path.

# Drawbacks

This proposal encourages aggressive use of the `(` nested helper syntax.
Encouraging this has been slightly controversial.

No solution for angle components is presented here. The syntax for `.`
notation in angle components is coupled to a decision on the syntax for
bound, dynamic angle component invocation (a `{{component` helper for angle
components basically).

`(component 'some-component'` may be too verbose. It may make sense to simply
allow `(some-component`.

The requirement for the dot separator may also be seen as a drawback. The reason for
enforcing the dot separator is to avoid accidental fallback to global components.
Other proposals have leaned more heavy on extending factories in JavaScript
then passing an object created in that space. Some arguments against this:

This problem seems unavoidable as long as `{{foo}}` is ambiguous between a property
lookup and a helper call.
* Getting the container correct is tricky. Who sets it when?
* Properties on the classes would not be naturally bound, as they are in this proposal.
* As soon as you start setting properties, you likely want a `mut` helper,
`action` helper, etc, in JavaScript space.
* Keeping the component lookup in the template layer allows us to take advantage
of changes to lookup semantics later, such as local lookup in the pods
proposal.

# Alternatives

I don't know of any alternative strategies that support contextualized components.
All pain, no gain. Addons really want this.

# Unresolved questions

- Should this also be available for helpers?
There has been discussion of if a similar mechanism should be available for
helpers.