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