-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10093 from lukemelia/helper-binding-support
Implement {{component}} helper
- Loading branch information
Showing
6 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
packages/ember-htmlbars/tests/helpers/component_test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"./); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); |