Permalink
Browse files

Adding positional parameter support to components

A longstanding pattern is to use helpers to simulate components with
positional parameters. But under Glimmer, helpers are pure functions, so
this doesn't work anymore.

Thankfully, we can do something even better, which is to stop wrapping
components in helpers just to get positional params, and instead make
components that can natively accept positional params. That's what this
PR does.

To use, you define your component like:

```js
Ember.Component.extend({
  positionalParams: ['name', 'city']
});
```

Then you can invoke it like:

```handlebars
{{my-component "Ed" "Somerville"}}
```

Which is equivalent to:

```handlebars
{{my-component name="Ed" city="Somerville"}}
```

(cherry picked from commit f0d28c2)
  • Loading branch information...
ef4 authored and rwjblue committed May 5, 2015
1 parent 1ca99f9 commit 995e2d2e0d5e6ae54afb6f3095c3d1efb20cdcfc
View
@@ -23,7 +23,7 @@
"express": "^4.5.0",
"github": "^0.2.3",
"glob": "~4.3.2",
"htmlbars": "0.13.13",
"htmlbars": "0.13.15",
"qunit-extras": "^1.3.0",
"qunitjs": "^1.16.0",
"route-recognizer": "0.1.5",
@@ -1,6 +1,6 @@
import ComponentNodeManager from "ember-htmlbars/node-managers/component-node-manager";
export default function componentHook(renderNode, env, scope, tagName, attrs, template, visitor) {
export default function componentHook(renderNode, env, scope, tagName, params, attrs, template, visitor) {
var state = renderNode.state;
// Determine if this is an initial render or a re-render
@@ -14,6 +14,7 @@ export default function componentHook(renderNode, env, scope, tagName, attrs, te
var manager = ComponentNodeManager.create(renderNode, env, {
tagName,
params,
attrs,
parentView,
template,
@@ -22,6 +22,6 @@ export default {
return;
}
env.hooks.component(morph, env, scope, componentPath, hash, template, visitor);
env.hooks.component(morph, env, scope, componentPath, params, hash, template, visitor);
}
};
@@ -13,7 +13,7 @@ export default {
},
render(morph, env, scope, params, hash, template, inverse, visitor) {
env.hooks.component(morph, env, scope, morph.state.componentName, hash, template, visitor);
env.hooks.component(morph, env, scope, morph.state.componentName, params, hash, template, visitor);
},
rerender(...args) {
@@ -4,6 +4,6 @@
*/
export default function textarea(morph, env, scope, originalParams, hash, template, inverse, visitor) {
env.hooks.component(morph, env, scope, '-text-area', hash, template, visitor);
env.hooks.component(morph, env, scope, '-text-area', originalParams, hash, template, visitor);
return true;
}
@@ -28,6 +28,7 @@ export default ComponentNodeManager;
ComponentNodeManager.create = function(renderNode, env, options) {
let { tagName,
params,
attrs,
parentView,
parentScope,
@@ -89,6 +90,13 @@ ComponentNodeManager.create = function(renderNode, env, options) {
}
renderNode.emberView = component;
if (component.positionalParams) {
let pp = component.positionalParams;
for (let i=0; i<pp.length; i++) {
attrs[pp[i]] = params[i];
}
}
}
var results = buildComponentTemplate({ layout: layout, component: component }, attrs, {
@@ -233,3 +233,43 @@ if (Ember.FEATURES.isEnabled('ember-views-component-block-info')) {
equal(jQuery('#qunit-fixture').text(), 'In block No Block Param!');
});
}
QUnit.test('static positional parameters', function() {
registry.register('template:components/sample-component', compile('{{attrs.name}}{{attrs.age}}'));
registry.register('component:sample-component', Component.extend({
positionalParams: ['name', 'age']
}));
view = EmberView.extend({
layout: compile('{{sample-component "Quint" 4}}'),
container: container
}).create();
runAppend(view);
equal(jQuery('#qunit-fixture').text(), 'Quint4');
});
QUnit.test('dynamic positional parameters', function() {
registry.register('template:components/sample-component', compile('{{attrs.name}}{{attrs.age}}'));
registry.register('component:sample-component', Component.extend({
positionalParams: ['name', 'age']
}));
view = EmberView.extend({
layout: compile('{{sample-component myName myAge}}'),
container: container,
context: {
myName: 'Quint',
myAge: 4
}
}).create();
runAppend(view);
equal(jQuery('#qunit-fixture').text(), 'Quint4');
run(function() {
Ember.set(view.context, 'myName', 'Edward');
Ember.set(view.context, 'myAe', '5');

This comment has been minimized.

Show comment
Hide comment
@SamvelRaja

SamvelRaja May 7, 2015

typo error it must be myAge instead of myAe

@SamvelRaja

SamvelRaja May 7, 2015

typo error it must be myAge instead of myAe

This comment has been minimized.

Show comment
Hide comment
@rwjblue

rwjblue May 7, 2015

Member

Was fixed in #11042.

@rwjblue

rwjblue May 7, 2015

Member

Was fixed in #11042.

This comment has been minimized.

Show comment
Hide comment
@SamvelRaja
@SamvelRaja
});
});
@@ -297,7 +297,7 @@ export default {
attrs.escaped = !morph.parseTextAsHTML;
env.hooks.component(morph, env, scope, '-link-to', attrs, template, visitor);
env.hooks.component(morph, env, scope, '-link-to', params, attrs, template, visitor);
},
rerender(morph, env, scope, params, hash, template, inverse, visitor) {

8 comments on commit 995e2d2

@sandstrom

This comment has been minimized.

Show comment
Hide comment
@sandstrom

sandstrom May 6, 2015

Contributor

I've always wanted this! 😄 Thanks @ef4!

Contributor

sandstrom replied May 6, 2015

I've always wanted this! 😄 Thanks @ef4!

@simi

This comment has been minimized.

Show comment
Hide comment
@simi

simi May 6, 2015

Contributor

❤️

Contributor

simi replied May 6, 2015

❤️

@stefanpenner

This comment has been minimized.

Show comment
Hide comment
@stefanpenner

stefanpenner May 6, 2015

Member

do these inherit and merge?

Member

stefanpenner replied May 6, 2015

do these inherit and merge?

@knownasilya

This comment has been minimized.

Show comment
Hide comment
@knownasilya

knownasilya May 6, 2015

Contributor

do these inherit and merge?

Would also like to know

Contributor

knownasilya replied May 6, 2015

do these inherit and merge?

Would also like to know

@rwjblue

This comment has been minimized.

Show comment
Hide comment
@rwjblue

rwjblue May 6, 2015

Member

They would inherit but not merge. If you extend a component that has positionalParams specified and you want to use the same params, you do not need to re-declare them. However, if you want to change the params (or augment them) you would have to declare all of them in the child component.

Member

rwjblue replied May 6, 2015

They would inherit but not merge. If you extend a component that has positionalParams specified and you want to use the same params, you do not need to re-declare them. However, if you want to change the params (or augment them) you would have to declare all of them in the child component.

@stefanpenner

This comment has been minimized.

Show comment
Hide comment
@stefanpenner

stefanpenner May 6, 2015

Member

@rwjblue thanks :)

Member

stefanpenner replied May 6, 2015

@rwjblue thanks :)

@mupkoo

This comment has been minimized.

Show comment
Hide comment
@mupkoo

mupkoo Aug 4, 2015

How is this going to look with angle bracket components?

mupkoo replied Aug 4, 2015

How is this going to look with angle bracket components?

@rwjblue

This comment has been minimized.

Show comment
Hide comment
@rwjblue

rwjblue Aug 4, 2015

Member

@mupkoo - positionalParams map directly to attributes that can be provided already in the hash. See the commit description for an explanation. The declaration of positionalParams in the example above has been tweaked to need to be done via reopenClass:

var MyComponent = Ember.Component.extend();
MyComponent.reopenClass({
  positionalParams: ['name', 'city']
});

But the rest of the commit description is still accurate, and explains that you can invoke the component with either ordered arguments or hash arguments. Angle bracket components would essentially require the hash argument form (making positionalParams only valid on components invoked with curlies).

Member

rwjblue replied Aug 4, 2015

@mupkoo - positionalParams map directly to attributes that can be provided already in the hash. See the commit description for an explanation. The declaration of positionalParams in the example above has been tweaked to need to be done via reopenClass:

var MyComponent = Ember.Component.extend();
MyComponent.reopenClass({
  positionalParams: ['name', 'city']
});

But the rest of the commit description is still accurate, and explains that you can invoke the component with either ordered arguments or hash arguments. Angle bracket components would essentially require the hash argument form (making positionalParams only valid on components invoked with curlies).

Please sign in to comment.