-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Implement {{component}} helper #10093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
| 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"./); | ||
| }); | ||
| } |
| 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, { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we only have a single child we do not need the power of I created a JSBin here with a tiny demo (would still need the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rwjblue I'm open to this approach. I'm curious about what the expected gain is, though. Is it faster or cheaper (in terms of memory)? It seems to me that what we are doing here is very similar what On that note, I wonder what consideration we should give to extension points for animation (e.g. LiquidFire). @ef4 or others familiar with it?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lukemelia - I'm not unhappy with your current implementation, I just think it could be better/simpler. I'm happy to wait for the real view experts (obviously not me) to chime in...
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rwjblue makes sense. I'll stay tuned.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will using a View instead of ContainerView fix the parentView problem?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @igorT I don't think there is a parentView problem in the current implementation, unless I am misunderstanding. I'll add an assertion to make sure this remains true.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a new test to confirm this in the latest revision. Let me know if this is not what you meant @igorT
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| 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); | ||
| } | ||
| }); | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe flagged aswell so we don't pull in the extra bytes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow. Do you have an example of this, @stefanpenner?