Skip to content

Commit

Permalink
[BUGFIX beta] Fix outlet regressions
Browse files Browse the repository at this point in the history
Closes emberjs#10478 by changing the way we account for routes that don't do a
default `render()`.

Fixes issue in comments of emberjs#10606 to make us tolerant of multiple renders on top of each other.

Close emberjs#10658 by respecting a non-default template rendered into a
main outlet as our own template.

Close emberjs#10606 by running disconnectOutlet against all active routes, not
just the current one. (*shudder*)
  • Loading branch information
ef4 committed Mar 20, 2015
1 parent 9fbd5e0 commit a994491
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 36 deletions.
32 changes: 25 additions & 7 deletions packages/ember-routing/lib/system/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -1882,27 +1882,45 @@ var Route = EmberObject.extend(ActionHandler, Evented, {
disconnectOutlet(options) {
var outletName;
var parentView;
var parent;
if (!options || typeof options === "string") {
outletName = options;
} else {
outletName = options.outlet;
parentView = options.parentView;
}

parentView = parentView && parentView.replace(/\//g, '.');
parent = parentRoute(this);
outletName = outletName || 'main';
this._disconnectOutlet(outletName, parentView);
for (var i = 0; i < this.router.router.currentHandlerInfos.length; i++) {
// This non-local state munging is sadly necessary to maintain
// backward compatibility with our existing semantics, which allow
// any route to disconnectOutlet things originally rendered by any
// other route. This should all get cut in 2.0.
this.router.router.
currentHandlerInfos[i].handler._disconnectOutlet(outletName, parentView);
}
},

_disconnectOutlet(outletName, parentView) {
var parent = parentRoute(this);
if (parent && parentView === parent.routeName) {
parentView = undefined;
}
outletName = outletName || 'main';

for (var i = 0; i < this.connections.length; i++) {
var connection = this.connections[i];
if (connection.outlet === outletName && connection.into === parentView) {
this.connections.splice(i, 1);
// This neuters the disconnected outlet such that it doesn't
// render anything, but it leaves an entry in the outlet
// hierarchy so that any existing other renders that target it
// don't suddenly blow up. They will still stick themselves
// into its outlets, which won't render anywhere. All of this
// statefulness should get the machete in 2.0.
this.connections[i] = {
into: connection.into,
outlet: connection.outlet,
name: connection.name
};
run.once(this.router, '_setOutlets');
return;
}
}
},
Expand Down
54 changes: 25 additions & 29 deletions packages/ember-routing/lib/system/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,18 @@ var EmberRouter = EmberObject.extend(Evented, {

for (var i = 0; i < handlerInfos.length; i++) {
route = handlerInfos[i].handler;
var connections = normalizedConnections(route);
var connections = route.connections;
var ownState;
for (var j = 0; j < connections.length; j++) {
var appended = appendLiveRoute(liveRoutes, defaultParentState, connections[j]);
liveRoutes = appended.liveRoutes;
if (appended.ownState.render.name === route.routeName) {
if (appended.ownState.render.name === route.routeName || appended.ownState.render.outlet === 'main') {
ownState = appended.ownState;
}
}
if (connections.length === 0) {
ownState = representEmptyRoute(liveRoutes, defaultParentState, route);
}
defaultParentState = ownState;
}
if (!this._toplevelView) {
Expand Down Expand Up @@ -1028,34 +1031,27 @@ function appendLiveRoute(liveRoutes, defaultParentState, renderOptions) {
};
}

function normalizedConnections(route) {
var connections = route.connections;
var mainConnections = [];
var otherConnections = [];

for (var i = 0; i < connections.length; i++) {
var connection = connections[i];
if (connection.outlet === 'main') {
mainConnections.push(connection);
} else {
otherConnections.push(connection);
}
}

if (mainConnections.length === 0) {
// There's always an entry to represent the route, even if it
// doesn't actually render anything into its own
// template. This gives other routes a place to target.
mainConnections.push({
name: route.routeName,
outlet: 'main'
});
function representEmptyRoute(liveRoutes, defaultParentState, route) {
// the route didn't render anything
var alreadyAppended = findLiveRoute(liveRoutes, route.routeName);
if (alreadyAppended) {
// But some other route has already rendered our default
// template, so that becomes the default target for any
// children we may have.
return alreadyAppended;
} else {
// Create an entry to represent our default template name,
// just so other routes can target it and inherit its place
// in the outlet hierarchy.
defaultParentState.outlets.main = {
render: {
name: route.routeName,
outlet: 'main'
},
outlets: {}
};
return defaultParentState;
}

// We process main connections first, because a main connection may
// be targeted by other connections.
return mainConnections.concat(otherConnections);
}


export default EmberRouter;
131 changes: 131 additions & 0 deletions packages/ember/tests/routing/basic_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3607,3 +3607,134 @@ QUnit.test("Can render into a named outlet at the top level, later", function()
//router._setOutlets();
equal(Ember.$('#qunit-fixture').text(), "A-The index-B-Hello world-C", "second render");
});

QUnit.test("Can render routes with no 'main' outlet and their children", function() {
Ember.TEMPLATES.application = compile('<div id="application">{{outlet "app"}}</div>');
Ember.TEMPLATES.app = compile('<div id="app-common">{{outlet "common"}}</div><div id="app-sub">{{outlet "sub"}}</div>');
Ember.TEMPLATES.common = compile('<div id="common"></div>');
Ember.TEMPLATES.sub = compile('<div id="sub"></div>');

Router.map(function() {
this.route('app', { path: "/app" }, function() {
this.resource('sub', { path: "/sub" });
});
});

App.AppRoute = Ember.Route.extend({
renderTemplate : function() {
this.render('app', {
outlet: 'app',
into: 'application'
});
this.render('common', {
outlet: 'common',
into: 'app'
});
}
});

App.SubRoute = Ember.Route.extend({
renderTemplate : function() {
this.render('sub', {
outlet: 'sub',
into: 'app'
});
}
});

bootApplication();
handleURL('/app');
equal(Ember.$('#app-common #common').length, 1, "Finds common while viewing /app");
handleURL('/app/sub');
equal(Ember.$('#app-common #common').length, 1, "Finds common while viewing /app/sub");
equal(Ember.$('#app-sub #sub').length, 1, "Finds sub while viewing /app/sub");
});

QUnit.test("Tolerates stacked renders", function() {
Ember.TEMPLATES.application = compile('{{outlet}}{{outlet "modal"}}');
Ember.TEMPLATES.index = compile('hi');
Ember.TEMPLATES.layer = compile('layer');
App.ApplicationRoute = Ember.Route.extend({
actions: {
openLayer: function() {
this.render('layer', {
into: 'application',
outlet: 'modal'
});
},
close: function() {
this.disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
}
}
});
bootApplication();
equal(Ember.$('#qunit-fixture').text().trim(), 'hi');
Ember.run(router, 'send', 'openLayer');
equal(Ember.$('#qunit-fixture').text().trim(), 'hilayer');
Ember.run(router, 'send', 'openLayer');
equal(Ember.$('#qunit-fixture').text().trim(), 'hilayer');
Ember.run(router, 'send', 'close');
equal(Ember.$('#qunit-fixture').text().trim(), 'hi');
});

QUnit.test("Renders child into parent with non-default template name", function() {
Ember.TEMPLATES.application = compile('<div class="a">{{outlet}}</div>');
Ember.TEMPLATES['exports/root'] = compile('<div class="b">{{outlet}}</div>');
Ember.TEMPLATES['exports/index'] = compile('<div class="c"></div>');

Router.map(function() {
this.route('root', function() {
});
});

App.RootRoute = Ember.Route.extend({
renderTemplate() {
this.render('exports/root');
}
});

App.RootIndexRoute = Ember.Route.extend({
renderTemplate() {
this.render('exports/index');
}
});

bootApplication();
handleURL('/root');
equal(Ember.$('#qunit-fixture .a .b .c').length, 1);
});

QUnit.test("Allows any route to disconnectOutlet another route's templates", function() {
Ember.TEMPLATES.application = compile('{{outlet}}{{outlet "modal"}}');
Ember.TEMPLATES.index = compile('hi');
Ember.TEMPLATES.layer = compile('layer');
App.ApplicationRoute = Ember.Route.extend({
actions: {
openLayer: function() {
this.render('layer', {
into: 'application',
outlet: 'modal'
});
}
}
});
App.IndexRoute = Ember.Route.extend({
actions: {
close: function() {
this.disconnectOutlet({
parentView: 'application',
outlet: 'modal'
});
}
}
});
bootApplication();
equal(Ember.$('#qunit-fixture').text().trim(), 'hi');
Ember.run(router, 'send', 'openLayer');
equal(Ember.$('#qunit-fixture').text().trim(), 'hilayer');
Ember.run(router, 'send', 'close');
equal(Ember.$('#qunit-fixture').text().trim(), 'hi');
});

0 comments on commit a994491

Please sign in to comment.