Skip to content

Commit

Permalink
Merge pull request #10093 from lukemelia/helper-binding-support
Browse files Browse the repository at this point in the history
Implement {{component}} helper
  • Loading branch information
mmun committed Jan 3, 2015
2 parents 651e0d5 + 16b7023 commit eff1014
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 0 deletions.
1 change: 1 addition & 0 deletions features.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"ember-htmlbars": true,
"ember-htmlbars-block-params": true,
"ember-htmlbars-component-generation": null,
"ember-htmlbars-component-helper": null,
"ember-htmlbars-inline-if-helper": true,
"ember-htmlbars-attribute-syntax": null,
"ember-routing-transitioning-classes": true,
Expand Down
92 changes: 92 additions & 0 deletions packages/ember-htmlbars/lib/helpers/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
@module ember
@submodule ember-htmlbars
*/
import Ember from "ember-metal/core"; // Ember.warn, Ember.assert
import { isStream, read } from "ember-metal/streams/utils";
import { readComponentFactory } from "ember-views/streams/utils";
import EmberError from "ember-metal/error";
import BoundComponentView from "ember-views/views/bound_component_view";
import mergeViewBindings from "ember-htmlbars/system/merge-view-bindings";
import appendTemplatedView from "ember-htmlbars/system/append-templated-view";

/**
The `{{component}}` helper lets you add instances of `Ember.Component` to a
template. See [Ember.Component](/api/classes/Ember.Component.html) for
additional information on how a `Component` functions.
`{{component}}`'s primary use is for cases where you want to dynamically
change which type of component is rendered as the state of your application
changes.
The provided block will be applied as the template for the component.
Given an empty `<body>` the following template:
```handlebars
{{! application.hbs }}
{{component infographicComponentName}}
```
And the following application code
```javascript
App = Ember.Application.create();
App.ApplicationController = Ember.Controller.extend({
infographicComponentName: function(){
if (this.get('isMarketOpen')) {
return "live-updating-chart";
} else {
return "market-close-summary";
}
}.property('isMarketOpen')
});
```
The `live-updating-chart` component will be appended when `isMarketOpen` is
`true`, and the `market-close-summary` component will be appended when
`isMarketOpen` is `false`. If the value changes while the app is running,
the component will be automatically swapped out accordingly.
Note: You should not use this helper when you are consistently rendering the same
component. In that case, use standard component syntax, for example:
```handlebars
{{! application.hbs }}
{{live-updating-chart}}
```
@method component
@for Ember.Handlebars.helpers
*/
export function componentHelper(params, hash, options, env) {
Ember.assert(
"The `component` helper expects exactly one argument, plus name/property values.",
params.length === 1
);

var componentNameParam = params[0];
var container = this.container || read(this._keywords.view).container;

var props = {
helperName: options.helperName || 'component'
};
if (options.template) {
props.template = options.template;
}

var viewClass;
if (isStream(componentNameParam)) {
viewClass = BoundComponentView;
props = { _boundComponentOptions: Ember.merge(hash, props) };
props._boundComponentOptions.componentNameStream = componentNameParam;
} else {
viewClass = readComponentFactory(componentNameParam, container);
if (!viewClass) {
throw new EmberError('HTMLBars error: Could not find component named "' + componentNameParam + '".');
}
mergeViewBindings(this, props, hash);
}

appendTemplatedView(this, options.morph, viewClass, props);
}
4 changes: 4 additions & 0 deletions packages/ember-htmlbars/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
default as helpers
} from "ember-htmlbars/helpers";
import { viewHelper } from "ember-htmlbars/helpers/view";
import { componentHelper } from "ember-htmlbars/helpers/component";
import { yieldHelper } from "ember-htmlbars/helpers/yield";
import { withHelper } from "ember-htmlbars/helpers/with";
import { logHelper } from "ember-htmlbars/helpers/log";
Expand Down Expand Up @@ -59,6 +60,9 @@ import "ember-htmlbars/system/bootstrap";
import "ember-htmlbars/compat";

registerHelper('view', viewHelper);
if (Ember.FEATURES.isEnabled('ember-htmlbars-component-helper')) {
registerHelper('component', componentHelper);
}
registerHelper('yield', yieldHelper);
registerHelper('with', withHelper);
registerHelper('if', ifHelper);
Expand Down
167 changes: 167 additions & 0 deletions packages/ember-htmlbars/tests/helpers/component_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import ComponentLookup from "ember-views/component_lookup";
import Registry from "container/registry";
import EmberView from "ember-views/views/view";
import compile from "ember-template-compiler/system/compile";
import { runAppend, runDestroy } from "ember-runtime/tests/utils";

var set = Ember.set;
var get = Ember.get;
var view, registry, container;

if (Ember.FEATURES.isEnabled('ember-htmlbars-component-helper')) {
QUnit.module("ember-htmlbars: {{#component}} helper", {
setup: function() {
registry = new Registry();
container = registry.container();

registry.optionsForType('template', { instantiate: false });
registry.register('component-lookup:main', ComponentLookup);
},

teardown: function() {
runDestroy(view);
runDestroy(container);
registry = container = view = null;
}
});

test("component helper with unquoted string is bound", function() {
registry.register('template:components/foo-bar', compile('yippie! {{location}} {{yield}}'));
registry.register('template:components/baz-qux', compile('yummy {{location}} {{yield}}'));

view = EmberView.create({
container: container,
dynamicComponent: 'foo-bar',
location: 'Caracas',
template: compile('{{#component view.dynamicComponent location=view.location}}arepas!{{/component}}')
});

runAppend(view);
equal(view.$().text(), 'yippie! Caracas arepas!', 'component was looked up and rendered');

Ember.run(function() {
set(view, "dynamicComponent", 'baz-qux');
set(view, "location", 'Loisaida');
});
equal(view.$().text(), 'yummy Loisaida arepas!', 'component was updated and re-rendered');
});

test("component helper with actions", function() {
registry.register('template:components/foo-bar', compile('yippie! {{yield}}'));
registry.register('component:foo-bar', Ember.Component.extend({
classNames: 'foo-bar',
didInsertElement: function() {
// trigger action on click in absence of app's EventDispatcher
var self = this;
this.$().on('click', function() {
self.sendAction('fooBarred');
});
},
willDestroyElement: function() {
this.$().off('click');
}
}));

var actionTriggered = 0;
var controller = Ember.Controller.extend({
dynamicComponent: 'foo-bar',
actions: {
mappedAction: function() {
actionTriggered++;
}
}
}).create();
view = EmberView.create({
container: container,
controller: controller,
template: compile('{{#component dynamicComponent fooBarred="mappedAction"}}arepas!{{/component}}')
});

runAppend(view);
Ember.run(function() {
view.$('.foo-bar').trigger('click');
});
equal(actionTriggered, 1, 'action was triggered');
});

test('component helper maintains expected logical parentView', function() {
registry.register('template:components/foo-bar', compile('yippie! {{yield}}'));
var componentInstance;
registry.register('component:foo-bar', Ember.Component.extend({
didInsertElement: function() {
componentInstance = this;
}
}));

view = EmberView.create({
container: container,
dynamicComponent: 'foo-bar',
template: compile('{{#component view.dynamicComponent}}arepas!{{/component}}')
});

runAppend(view);
equal(get(componentInstance, 'parentView'), view, 'component\'s parentView is the view invoking the helper');
});

test("nested component helpers", function() {
registry.register('template:components/foo-bar', compile('yippie! {{location}} {{yield}}'));
registry.register('template:components/baz-qux', compile('yummy {{location}} {{yield}}'));
registry.register('template:components/corge-grault', compile('delicious {{location}} {{yield}}'));

view = EmberView.create({
container: container,
dynamicComponent1: 'foo-bar',
dynamicComponent2: 'baz-qux',
location: 'Caracas',
template: compile('{{#component view.dynamicComponent1 location=view.location}}{{#component view.dynamicComponent2 location=view.location}}arepas!{{/component}}{{/component}}')
});

runAppend(view);
equal(view.$().text(), 'yippie! Caracas yummy Caracas arepas!', 'components were looked up and rendered');

Ember.run(function() {
set(view, "dynamicComponent1", 'corge-grault');
set(view, "location", 'Loisaida');
});
equal(view.$().text(), 'delicious Loisaida yummy Loisaida arepas!', 'components were updated and re-rendered');
});

test("component helper can be used with a quoted string (though you probably would not do this)", function() {
registry.register('template:components/foo-bar', compile('yippie! {{location}} {{yield}}'));

view = EmberView.create({
container: container,
location: 'Caracas',
template: compile('{{#component "foo-bar" location=view.location}}arepas!{{/component}}')
});

runAppend(view);

equal(view.$().text(), 'yippie! Caracas arepas!', 'component was looked up and rendered');
});

test("component with unquoted param resolving to non-existent component", function() {
view = EmberView.create({
container: container,
dynamicComponent: 'does-not-exist',
location: 'Caracas',
template: compile('{{#component view.dynamicComponent location=view.location}}arepas!{{/component}}')
});

throws(function() {
runAppend(view);
}, /HTMLBars error: Could not find component named "does-not-exist"./);
});

test("component with quoted param for non-existent component", function() {
view = EmberView.create({
container: container,
location: 'Caracas',
template: compile('{{#component "does-not-exist" location=view.location}}arepas!{{/component}}')
});

throws(function() {
runAppend(view);
}, /HTMLBars error: Could not find component named "does-not-exist"./);
});
}
9 changes: 9 additions & 0 deletions packages/ember-views/lib/streams/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ export function readViewFactory(object, container) {
return viewClass;
}

export function readComponentFactory(nameOrStream, container) {
var name = read(nameOrStream);
var componentLookup = container.lookup('component-lookup:main');
Ember.assert("Could not find 'component-lookup:main' on the provided container," +
" which is necessary for performing component lookups", componentLookup);

return componentLookup.lookupFactory(name, container);
}

export function readUnwrappedModel(object) {
if (isStream(object)) {
var result = object.value();
Expand Down
50 changes: 50 additions & 0 deletions packages/ember-views/lib/views/bound_component_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
@module ember
@submodule ember-views
*/

import { _Metamorph } from "ember-views/views/metamorph_view";
import { read, chain, subscribe, unsubscribe } from "ember-metal/streams/utils";
import { readComponentFactory } from "ember-views/streams/utils";
import mergeViewBindings from "ember-htmlbars/system/merge-view-bindings";
import EmberError from "ember-metal/error";

export default Ember.ContainerView.extend(_Metamorph, {
init: function() {
this._super();
var componentNameStream = this._boundComponentOptions.componentNameStream;
var container = this.container;
this.componentClassStream = chain(componentNameStream, function() {
return readComponentFactory(componentNameStream, container);
});

subscribe(this.componentClassStream, this._updateBoundChildComponent, this);
this._updateBoundChildComponent();
},
willDestroy: function() {
unsubscribe(this.componentClassStream, this._updateBoundChildComponent, this);
this._super();
},
_updateBoundChildComponent: function() {
this.replace(0, 1, [this._createNewComponent()]);
},
_createNewComponent: function() {
var componentClass = read(this.componentClassStream);
if (!componentClass) {
throw new EmberError('HTMLBars error: Could not find component named "' + read(this._boundComponentOptions.componentNameStream) + '".');
}
var hash = this._boundComponentOptions;
var ignore = ["_boundComponentOptions", "componentClassStream"];
var hashForComponent = {};

var prop;
for (prop in hash) {
if (ignore.indexOf(prop) !== -1) { continue; }
hashForComponent[prop] = hash[prop];
}

var props = {};
mergeViewBindings(this, props, hashForComponent);
return this.createChildView(componentClass, props);
}
});

0 comments on commit eff1014

Please sign in to comment.