Skip to content

Commit 7c43fb4

Browse files
committed
Merge pull request rust-lang#64 from emberjs/contextual-component-lookup
Contextual components RFC
2 parents 12c0c29 + 8201898 commit 7c43fb4

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
- Start Date: 2015-06-12
2+
- RFC PR: https://github.com/emberjs/rfcs/pull/64
3+
- Ember Issue: (leave this empty)
4+
5+
# Summary
6+
7+
The goal of this RFC is to allow for better component composition and the
8+
usage of components for domain specific languages.
9+
10+
Ember components can be invoked three ways:
11+
12+
* `{{a-component`
13+
* `{{component someBoundComponentName`
14+
* `<a-component` (coming soon!)
15+
16+
In all these cases, attrs passed to the component must be set at the place of
17+
invocation. Only the `{{component someBoundComponentName` syntax allows for the name
18+
of the component invoked to be decided elsewhere.
19+
20+
All component names are resovled to components through one global resolution
21+
path.
22+
23+
To improve composition, four changes are proposed:
24+
25+
* The `(component` helper will be introduced to close over component attrs in
26+
a yielding context.
27+
* The `{{component` helper will accept an argument of the object created by
28+
`(component` for invocation (as it invokes strings today).
29+
* Property lookups with a value containing a dot will be considered for
30+
rendering as components. `{{form.input}}` would be considered, for instance.
31+
Helper invocations with a dot will also be treated like a component if the
32+
key has a value of a component, for instance `{{form.input value=baz}}`.
33+
* A `(hash` helper will be introduced.
34+
35+
# Motivation
36+
37+
When building a complex UI from several components, it can be difficult to
38+
share data without breaking encapsulation. For example this template:
39+
40+
```hbs
41+
{{#great-toolbar role=user.role}}
42+
{{great-button role=user.role}}
43+
{{/great-toolbar}}
44+
```
45+
46+
Causes the user to pass the `role` data twice for what are obviously related
47+
components. A component can yield itself down:
48+
49+
```hbs
50+
{{! app/components/great-toolbar/template.hbs }}
51+
{{yield this}}
52+
```
53+
54+
```hbs
55+
{{#great-toolbar role=user.role as |toolbar|}}
56+
{{great-button toolbar=toolbar}}
57+
{{/great-toolbar}}
58+
```
59+
60+
And `great-button` can have knowledge about properties on `great-toolbar`, but
61+
this break the isolation of components. Additionally the calling syntax is not
62+
much better, `toolbar` must still be passed to each downstream component.
63+
64+
Often `nearestOfType` is used as a workaround for these limitations. This API
65+
is poorly performing, and still results in the downstream child accessing the
66+
parent component properties directly.
67+
68+
Consequently there is a demand by several addons for improvement. Our goal
69+
is a syntax similar to DSLs in Ruby:
70+
71+
```hbs
72+
{{#great-toolbar role=user.role as |toolbar|}}
73+
{{toolbar.button}}
74+
{{toolbar.button orWith=additionalProperties}}
75+
{{/great-toolbar
76+
```
77+
78+
As laid out in this proposal, the `great-toolbar` implementation would look
79+
like:
80+
81+
```hbs
82+
{{! app/components/great-toolbar/template.hbs }}
83+
{{yield (hash
84+
button=(component 'great-button' role=user.role)
85+
)}}
86+
```
87+
88+
# Detailed design
89+
90+
### The `(component` helper and `{{component` helper
91+
92+
Much like `(action` creates a closure, it is proposed that the `(component`
93+
helper create something similar. For example with actions:
94+
95+
```hbs
96+
{{#with (action "save" model) as |save|}}
97+
<button {{action save}}>Save</button>
98+
{{/with}}
99+
```
100+
101+
The returned value of the `(action` nested helper (a function) closes over the
102+
action being called (`actions.save` on the context and the `model` property).
103+
The `{{action` helper can accept this resulting value and invoke the action
104+
when the user clicks.
105+
106+
The `(component` helper will close over a component name. The
107+
`{{component` helper will be modified to accept this resulting value and invoke
108+
the component:
109+
110+
```hbs
111+
{{#with (component "user-profile") as |uiPane|}}
112+
{{component uiPane}}
113+
{{/with}}
114+
```
115+
116+
Additionally, a bound value may be passed to the `(component` helper. For
117+
example `(component someComponentName)`.
118+
119+
Attrs for the final component can also be closed over. Used with yield, this
120+
allows for the creation of components that have attrs from other scopes. For
121+
example:
122+
123+
```hbs
124+
{{! app/components/user-profile.hbs }}
125+
{{yield (component "user-profile" user=user.name age=user.age)}}
126+
```
127+
128+
```hbs
129+
{{#user-profile user=model as |profile|}}
130+
{{component profile}}
131+
{{/user-profile}}
132+
```
133+
134+
Of course attrs can also be passed at invocation. They smash any conflicting
135+
attrs that were closed over. For example `{{component profile age=lyingUser.age}}`
136+
137+
Passing the resulting value from `(component` into JavaScript is permitted,
138+
however that object has no public properties or methods. Its only use would
139+
be to set it on state and reference it in template somewhere.
140+
141+
### Hash helper
142+
143+
Unlike values, components are likely to have specific names that are semantically
144+
relevent. When yielded to a new scope, allowing the user to change the name
145+
of the component's variable would quickly lead to confusing addon documentation.
146+
For example:
147+
148+
```hbs
149+
{{#with (component "user-profile") as |dropDatabaseUI|}}
150+
{{component dropDatabaseUI}}
151+
{{/with}}
152+
```
153+
154+
The simplest way to enforce specific names is to make building hashes
155+
of components (or anything) easy. For example:
156+
157+
```hbs
158+
{{#with (hash profile=(component "user-profile")) as |userComponents|}}
159+
{{component userComponents.profile}}
160+
{{/with}}
161+
```
162+
163+
The `(hash` helper is a generic builder of objects, given hash arguments. It
164+
would also be useful in the same manner for actions:
165+
166+
```hbs
167+
{{#with (hash save=(action "save" model)) as |userActions|}}
168+
<button {{action userActions.save}}>Save</button>
169+
{{/with}}
170+
```
171+
172+
### Component helper shorthand
173+
174+
To complete building a viable DSL, `.` invocation for `{{` components will be
175+
introduced. For example this `{{component` invocation:
176+
177+
```hbs
178+
{{#with (hash profile=(component "user-profile")) as |userComponents|}}
179+
{{component userComponents.profile}}
180+
{{/with}}
181+
```
182+
183+
Could be converted to drop the explicit `component` helper call.
184+
185+
```hbs
186+
{{#with (hash profile=(component "user-profile")) as |userComponents|}}
187+
{{userComponents.profile}}
188+
{{/with}}
189+
```
190+
191+
A component can be invoked like this only when it was created by the
192+
`(component` nested helper form. For example unlike with the `{{component`
193+
helper, a string is not acceptable.
194+
195+
To be a valid invocation, one of two criteria must be met:
196+
197+
* The component can be called as a path. For example `{{form.input}}` or `{{this.input}}`
198+
* The component can be called as a helper. For example `{{form.input value=baz}}` or `{{this.input value=baz}}`
199+
200+
And of course a `.` must be present in the path.
201+
202+
# Drawbacks
203+
204+
This proposal encourages aggressive use of the `(` nested helper syntax.
205+
Encouraging this has been slightly controversial.
206+
207+
No solution for angle components is presented here. The syntax for `.`
208+
notation in angle components is coupled to a decision on the syntax for
209+
bound, dynamic angle component invocation (a `{{component` helper for angle
210+
components basically).
211+
212+
`(component 'some-component'` may be too verbose. It may make sense to simply
213+
allow `(some-component`.
214+
215+
Other proposals have leaned more heavy on extending factories in JavaScript
216+
then passing an object created in that space. Some arguments against this:
217+
218+
* Getting the container correct is tricky. Who sets it when?
219+
* Properties on the classes would not be naturally bound, as they are in this proposal.
220+
* As soon as you start setting properties, you likely want a `mut` helper,
221+
`action` helper, etc, in JavaScript space.
222+
* Keeping the component lookup in the template layer allows us to take advantage
223+
of changes to lookup semantics later, such as local lookup in the pods
224+
proposal.
225+
226+
# Alternatives
227+
228+
All pain, no gain. Addons really want this.
229+
230+
# Unresolved questions
231+
232+
There has been discussion of if a similar mechanism should be available for
233+
helpers.

0 commit comments

Comments
 (0)