Permalink
48ed0c4 Aug 26, 2016
3 contributors

Users who have contributed to this file

@pvmsikrsna @mmun @efx
234 lines (174 sloc) 7.42 KB

Summary

The goal of this RFC is to allow for better component composition and the usage of components for domain specific languages.

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 resolved to components through one global resolution path.

To improve composition, four changes are proposed:

  • 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

When building a complex UI from several components, it can be difficult to share data without breaking encapsulation. For example this template:

{{#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:

{{! app/components/great-toolbar/template.hbs }}
{{yield this}}
{{#great-toolbar role=user.role as |toolbar|}}
  {{great-button toolbar=toolbar}}
{{/great-toolbar}}

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.

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.

Consequently there is a demand by several addons for improvement. Our goal is a syntax similar to DSLs in Ruby:

{{#great-toolbar role=user.role as |toolbar|}}
  {{toolbar.button}}
  {{toolbar.button orWith=additionalProperties}}
{{/great-toolbar}}

As laid out in this proposal, the great-toolbar implementation would look like:

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

Detailed design

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:

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

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.

The (component helper will close over a component name. The {{component helper will be modified to accept this resulting value and invoke the component:

{{#with (component "user-profile") as |uiPane|}}
  {{component uiPane}}
{{/with}}

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

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:

{{! app/components/user-profile.hbs }}
{{yield (component "user-profile" user=user.name age=user.age)}}
{{#user-profile user=model as |profile|}}
  {{component profile}}
{{/user-profile}}

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}}

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.

Hash helper

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:

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

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

{{#with (hash profile=(component "user-profile")) as |userComponents|}}
  {{component userComponents.profile}}
{{/with}}

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

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

Component helper shorthand

To complete building a viable DSL, . invocation for {{ components will be introduced. For example this {{component invocation:

{{#with (hash profile=(component "user-profile")) as |userComponents|}}
  {{component userComponents.profile}}
{{/with}}

Could be converted to drop the explicit component helper call.

{{#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}} or {{this.input}}
  • The component can be called as a helper. For example {{form.input value=baz}} or {{this.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.

Other proposals have leaned more heavy on extending factories in JavaScript then passing an object created in that space. Some arguments against this:

  • 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

All pain, no gain. Addons really want this.

Unresolved questions

There has been discussion of if a similar mechanism should be available for helpers.