From d8a16a4a1a14d22ff3cf4f65136db055eb84dcfe Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Mon, 23 Mar 2015 00:28:42 -0400 Subject: [PATCH] [BUGFIX beta] fix outlets inside render helper Closes #10602 --- .../lib/helpers/render.js | 46 ++++++++ .../ember-routing-views/lib/views/outlet.js | 2 +- packages/ember-routing/lib/system/router.js | 31 ++++- packages/ember/tests/routing/basic_test.js | 110 ++++++++++++++++++ 4 files changed, 186 insertions(+), 3 deletions(-) diff --git a/packages/ember-routing-htmlbars/lib/helpers/render.js b/packages/ember-routing-htmlbars/lib/helpers/render.js index f8d1e39b10b..b384e58048f 100644 --- a/packages/ember-routing-htmlbars/lib/helpers/render.js +++ b/packages/ember-routing-htmlbars/lib/helpers/render.js @@ -13,6 +13,7 @@ import { import { isStream } from "ember-metal/streams/utils"; import mergeViewBindings from "ember-htmlbars/system/merge-view-bindings"; import appendTemplatedView from "ember-htmlbars/system/append-templated-view"; +import create from 'ember-metal/platform/create'; /** Calling ``{{render}}`` from within a template will insert another @@ -191,6 +192,51 @@ export function renderHelper(params, hash, options, env) { helperName: 'render "' + name + '"' }; + impersonateAnOutlet(currentView, view, name); mergeViewBindings(currentView, props, hash); appendTemplatedView(currentView, options.morph, view, props); } + +// Megahax to make outlets inside the render helper work, until we +// can kill that behavior at 2.0. +function impersonateAnOutlet(currentView, view, name) { + view._childOutlets = Ember.A(); + view._isOutlet = true; + view._outletName = '__ember_orphans__'; + view._matchOutletName = name; + view.setOutletState = function(state) { + var ownState; + if (state && (ownState = state.outlets[this._matchOutletName])) { + this._outletState = { + render: { name: 'render helper stub' }, + outlets: create(null) + }; + this._outletState.outlets[ownState.render.outlet] = ownState; + ownState.wasUsed = true; + } else { + this._outletState = null; + } + for (var i = 0; i < this._childOutlets.length; i++) { + var child = this._childOutlets[i]; + child.setOutletState(this._outletState && this._outletState.outlets[child._outletName]); + } + }; + + var pointer = currentView; + var po; + while (pointer && !pointer._isOutlet) { + pointer = pointer._parentView; + } + while (pointer && (po = pointer._parentOutlet())) { + pointer = po; + } + if (pointer) { + // we've found the toplevel outlet. Subscribe to its + // __ember_orphan__ child outlet, which is our hack convention for + // stashing outlet state that may target the render helper. + pointer._childOutlets.push(view); + if (pointer._outletState) { + view.setOutletState(pointer._outletState.outlets[view._outletName]); + } + } +} diff --git a/packages/ember-routing-views/lib/views/outlet.js b/packages/ember-routing-views/lib/views/outlet.js index 96dfe8d2547..6d9a3a53e7d 100644 --- a/packages/ember-routing-views/lib/views/outlet.js +++ b/packages/ember-routing-views/lib/views/outlet.js @@ -10,7 +10,7 @@ import { get } from "ember-metal/property_get"; export var CoreOutletView = ContainerView.extend({ init() { this._super(); - this._childOutlets = []; + this._childOutlets = Ember.A(); this._outletState = null; }, diff --git a/packages/ember-routing/lib/system/router.js b/packages/ember-routing/lib/system/router.js index d0aeb3f718e..317b76d5f7a 100644 --- a/packages/ember-routing/lib/system/router.js +++ b/packages/ember-routing/lib/system/router.js @@ -1022,8 +1022,17 @@ function appendLiveRoute(liveRoutes, defaultParentState, renderOptions) { if (target) { set(target.outlets, renderOptions.outlet, myState); } else { - Ember.assert("You attempted to render into '" + renderOptions.into + "' but it was not found", !renderOptions.into); - liveRoutes = myState; + if (renderOptions.into) { + // Megahax time. Post-2.0-breaking-changes, we will just assert + // right here that the user tried to target a nonexistent + // thing. But for now we still need to support the `render` + // helper, and people are allowed to target templates rendered + // by the render helper. So instead we defer doing anyting with + // these orphan renders until afterRender. + appendOrphan(liveRoutes, renderOptions.into, myState); + } else { + liveRoutes = myState; + } } return { liveRoutes: liveRoutes, @@ -1031,6 +1040,24 @@ function appendLiveRoute(liveRoutes, defaultParentState, renderOptions) { }; } +function appendOrphan(liveRoutes, into, myState) { + if (!liveRoutes.outlets.__ember_orphans__) { + liveRoutes.outlets.__ember_orphans__ = { + render: { + name: '__ember_orphans__' + }, + outlets: create(null) + }; + } + liveRoutes.outlets.__ember_orphans__.outlets[into] = myState; + Ember.run.schedule('afterRender', function() { + // `wasUsed` gets set by the render helper. See the function + // `impersonateAnOutlet`. + Ember.assert("You attempted to render into '" + into + "' but it was not found", + liveRoutes.outlets.__ember_orphans__.outlets[into].wasUsed); + }); +} + function representEmptyRoute(liveRoutes, defaultParentState, route) { // the route didn't render anything var alreadyAppended = findLiveRoute(liveRoutes, route.routeName); diff --git a/packages/ember/tests/routing/basic_test.js b/packages/ember/tests/routing/basic_test.js index 829e820db43..95fa0c72c61 100644 --- a/packages/ember/tests/routing/basic_test.js +++ b/packages/ember/tests/routing/basic_test.js @@ -3738,3 +3738,113 @@ QUnit.test("Allows any route to disconnectOutlet another route's templates", fun Ember.run(router, 'send', 'close'); equal(Ember.$('#qunit-fixture').text().trim(), 'hi'); }); + +QUnit.test("Can render({into:...}) the render helper", function() { + Ember.TEMPLATES.application = compile('{{render "foo"}}'); + Ember.TEMPLATES.foo = compile('
{{outlet}}
'); + Ember.TEMPLATES.index = compile('other'); + Ember.TEMPLATES.bar = compile('bar'); + + App.IndexRoute = Ember.Route.extend({ + renderTemplate() { + this.render({ into: 'foo' }); + }, + actions: { + changeToBar: function() { + this.disconnectOutlet({ + parentView: 'foo', + outlet: 'main' + }); + this.render('bar', { into: 'foo' }); + } + } + }); + + bootApplication(); + equal(Ember.$('#qunit-fixture .foo').text(), 'other'); + Ember.run(router, 'send', 'changeToBar'); + equal(Ember.$('#qunit-fixture .foo').text(), 'bar'); +}); + +QUnit.test("Can disconnect from the render helper", function() { + Ember.TEMPLATES.application = compile('{{render "foo"}}'); + Ember.TEMPLATES.foo = compile('
{{outlet}}
'); + Ember.TEMPLATES.index = compile('other'); + + App.IndexRoute = Ember.Route.extend({ + renderTemplate() { + this.render({ into: 'foo' }); + }, + actions: { + disconnect: function() { + this.disconnectOutlet({ + parentView: 'foo', + outlet: 'main' + }); + } + } + }); + + bootApplication(); + equal(Ember.$('#qunit-fixture .foo').text(), 'other'); + Ember.run(router, 'send', 'disconnect'); + equal(Ember.$('#qunit-fixture .foo').text(), ''); +}); + + +QUnit.test("Can render({into:...}) the render helper's children", function() { + Ember.TEMPLATES.application = compile('{{render "foo"}}'); + Ember.TEMPLATES.foo = compile('
{{outlet}}
'); + Ember.TEMPLATES.index = compile('
{{outlet}}
'); + Ember.TEMPLATES.other = compile('other'); + Ember.TEMPLATES.bar = compile('bar'); + + App.IndexRoute = Ember.Route.extend({ + renderTemplate() { + this.render({ into: 'foo' }); + this.render('other', { into: 'index' }); + }, + actions: { + changeToBar: function() { + this.disconnectOutlet({ + parentView: 'index', + outlet: 'main' + }); + this.render('bar', { into: 'index' }); + } + } + }); + + bootApplication(); + equal(Ember.$('#qunit-fixture .foo .index').text(), 'other'); + Ember.run(router, 'send', 'changeToBar'); + equal(Ember.$('#qunit-fixture .foo .index').text(), 'bar'); + +}); + +QUnit.test("Can disconnect from the render helper's children", function() { + Ember.TEMPLATES.application = compile('{{render "foo"}}'); + Ember.TEMPLATES.foo = compile('
{{outlet}}
'); + Ember.TEMPLATES.index = compile('
{{outlet}}
'); + Ember.TEMPLATES.other = compile('other'); + + App.IndexRoute = Ember.Route.extend({ + renderTemplate() { + this.render({ into: 'foo' }); + this.render('other', { into: 'index' }); + }, + actions: { + disconnect: function() { + this.disconnectOutlet({ + parentView: 'index', + outlet: 'main' + }); + } + } + }); + + bootApplication(); + equal(Ember.$('#qunit-fixture .foo .index').text(), 'other'); + Ember.run(router, 'send', 'disconnect'); + equal(Ember.$('#qunit-fixture .foo .index').text(), ''); +});