diff --git a/packages/ember-application/lib/system/application.js b/packages/ember-application/lib/system/application.js
index 47da5440fed..d42a759efa1 100644
--- a/packages/ember-application/lib/system/application.js
+++ b/packages/ember-application/lib/system/application.js
@@ -22,6 +22,7 @@ import ArrayController from "ember-runtime/controllers/array_controller";
import Renderer from "ember-views/system/renderer";
import DOMHelper from "dom-helper";
import SelectView from "ember-views/views/select";
+import { OutletView } from "ember-routing-views/views/outlet";
import EmberView from "ember-views/views/view";
import _MetamorphView from "ember-views/views/metamorph_view";
import EventDispatcher from "ember-views/system/event_dispatcher";
@@ -1003,6 +1004,7 @@ Application.reopenClass({
registry.injection('view', 'renderer', 'renderer:-dom');
registry.register('view:select', SelectView);
+ registry.register('view:-outlet', OutletView);
registry.register('view:default', _MetamorphView);
registry.register('view:toplevel', EmberView.extend());
@@ -1011,6 +1013,7 @@ Application.reopenClass({
registry.register('event_dispatcher:main', EventDispatcher);
registry.injection('router:main', 'namespace', 'application:main');
+ registry.injection('view:-outlet', 'namespace', 'application:main');
registry.register('location:auto', AutoLocation);
registry.register('location:hash', HashLocation);
diff --git a/packages/ember-application/tests/system/logging_test.js b/packages/ember-application/tests/system/logging_test.js
index 0fb408efc86..3c01359071d 100644
--- a/packages/ember-application/tests/system/logging_test.js
+++ b/packages/ember-application/tests/system/logging_test.js
@@ -7,6 +7,7 @@ import Controller from "ember-runtime/controllers/controller";
import Route from "ember-routing/system/route";
import RSVP from "ember-runtime/ext/rsvp";
import keys from "ember-metal/keys";
+import compile from "ember-template-compiler/system/compile";
import "ember-routing";
@@ -182,7 +183,7 @@ QUnit.test("log when template and view are missing when flag is active", functio
return;
}
- App.register('template:application', function() { return ''; });
+ App.register('template:application', compile("{{outlet}}"));
run(App, 'advanceReadiness');
visit('/posts').then(function() {
@@ -210,7 +211,7 @@ QUnit.test("log which view is used with a template", function() {
return;
}
- App.register('template:application', function() { return 'Template with default view'; });
+ App.register('template:application', compile('{{outlet}}'));
App.register('template:foo', function() { return 'Template with custom view'; });
App.register('view:posts', View.extend({ templateName: 'foo' }));
run(App, 'advanceReadiness');
diff --git a/packages/ember-routing-htmlbars/lib/helpers/outlet.js b/packages/ember-routing-htmlbars/lib/helpers/outlet.js
index 7ededdb8b88..5a5c9188bff 100644
--- a/packages/ember-routing-htmlbars/lib/helpers/outlet.js
+++ b/packages/ember-routing-htmlbars/lib/helpers/outlet.js
@@ -4,8 +4,6 @@
*/
import Ember from "ember-metal/core"; // assert
-import { set } from "ember-metal/property_set";
-import { OutletView } from "ember-routing-views/views/outlet";
/**
The `outlet` helper is a placeholder that the router will fill in with
@@ -71,7 +69,6 @@ import { OutletView } from "ember-routing-views/views/outlet";
@return {String} HTML string
*/
export function outletHelper(params, hash, options, env) {
- var outletSource;
var viewName;
var viewClass;
var viewFullName;
@@ -83,11 +80,6 @@ export function outletHelper(params, hash, options, env) {
var property = params[0] || 'main';
- outletSource = this;
- while (!outletSource.get('template.isTop')) {
- outletSource = outletSource._parentView;
- }
- set(this, 'outletSource', outletSource);
// provide controller override
viewName = hash.view;
@@ -105,11 +97,8 @@ export function outletHelper(params, hash, options, env) {
);
}
- viewClass = viewName ? this.container.lookupFactory(viewFullName) : hash.viewClass || OutletView;
-
- hash.currentViewBinding = '_view.outletSource._outlets.' + property;
-
+ viewClass = viewName ? this.container.lookupFactory(viewFullName) : hash.viewClass || this.container.lookupFactory('view:-outlet');
+ hash._outletName = property;
options.helperName = options.helperName || 'outlet';
-
return env.helpers.view.helperFunction.call(this, [viewClass], hash, options, env);
}
diff --git a/packages/ember-routing-htmlbars/tests/helpers/outlet_test.js b/packages/ember-routing-htmlbars/tests/helpers/outlet_test.js
index 2a3bd2b08c5..1b510f1bfe5 100644
--- a/packages/ember-routing-htmlbars/tests/helpers/outlet_test.js
+++ b/packages/ember-routing-htmlbars/tests/helpers/outlet_test.js
@@ -3,9 +3,7 @@ import run from "ember-metal/run_loop";
import Namespace from "ember-runtime/system/namespace";
-import _MetamorphView from "ember-views/views/metamorph_view";
-import EmberView from "ember-routing/ext/view";
-import EmberContainerView from "ember-views/views/container_view";
+import EmberView from "ember-views/views/view";
import jQuery from "ember-views/system/jquery";
import { outletHelper } from "ember-routing-htmlbars/helpers/outlet";
@@ -18,7 +16,7 @@ import { buildRegistry } from "ember-routing-htmlbars/tests/utils";
var trim = jQuery.trim;
-var view, registry, container, originalOutletHelper;
+var registry, container, originalOutletHelper, top;
QUnit.module("ember-routing-htmlbars: {{outlet}} helper", {
setup: function() {
@@ -28,6 +26,9 @@ QUnit.module("ember-routing-htmlbars: {{outlet}} helper", {
var namespace = Namespace.create();
registry = buildRegistry(namespace);
container = registry.container();
+
+ var CoreOutlet = container.lookupFactory('view:core-outlet');
+ top = CoreOutlet.create();
},
teardown: function() {
@@ -35,221 +36,148 @@ QUnit.module("ember-routing-htmlbars: {{outlet}} helper", {
helpers['outlet'] = originalOutletHelper;
runDestroy(container);
- runDestroy(view);
- registry = container = view = null;
+ runDestroy(top);
+ registry = container = top = null;
}
});
-QUnit.test("view should support connectOutlet for the main outlet", function() {
- var template = "
HI
{{outlet}}";
- view = EmberView.create({
- template: compile(template)
- });
+QUnit.test("view should render the outlet when set after dom insertion", function() {
+ var routerState = withTemplate("HI
{{outlet}}");
+ top.setOutletState(routerState);
+ runAppend(top);
- runAppend(view);
+ equal(top.$().text(), 'HI');
- equal(view.$().text(), 'HI');
+ routerState.outlets.main = withTemplate("BYE
");
run(function() {
- view.connectOutlet('main', EmberView.create({
- template: compile("BYE
")
- }));
+ top.setOutletState(routerState);
});
// Replace whitespace for older IE
- equal(trim(view.$().text()), 'HIBYE');
+ equal(trim(top.$().text()), 'HIBYE');
});
-QUnit.test("outlet should support connectOutlet in slots in prerender state", function() {
- var template = "HI
{{outlet}}";
- view = EmberView.create({
- template: compile(template)
- });
-
- view.connectOutlet('main', EmberView.create({
- template: compile("BYE
")
- }));
-
- runAppend(view);
+QUnit.test("view should render the outlet when set before dom insertion", function() {
+ var routerState = withTemplate("HI
{{outlet}}");
+ routerState.outlets.main = withTemplate("BYE
");
+ top.setOutletState(routerState);
+ runAppend(top);
- equal(view.$().text(), 'HIBYE');
+ // Replace whitespace for older IE
+ equal(trim(top.$().text()), 'HIBYE');
});
+
QUnit.test("outlet should support an optional name", function() {
- var template = "HI
{{outlet 'mainView'}}";
- view = EmberView.create({
- template: compile(template)
- });
+ var routerState = withTemplate("HI
{{outlet 'mainView'}}");
+ top.setOutletState(routerState);
+ runAppend(top);
- runAppend(view);
+ equal(top.$().text(), 'HI');
- equal(view.$().text(), 'HI');
+ routerState.outlets.mainView = withTemplate("BYE
");
run(function() {
- view.connectOutlet('mainView', EmberView.create({
- template: compile("BYE
")
- }));
+ top.setOutletState(routerState);
});
// Replace whitespace for older IE
- equal(trim(view.$().text()), 'HIBYE');
+ equal(trim(top.$().text()), 'HIBYE');
});
QUnit.test("outlet should correctly lookup a view", function() {
-
- var template,
- ContainerView,
- childView;
-
- ContainerView = EmberContainerView.extend();
-
- registry.register("view:containerView", ContainerView);
-
- template = "HI
{{outlet view='containerView'}}";
-
- view = EmberView.create({
- template: compile(template),
- container : container
+ var CoreOutlet = container.lookupFactory('view:core-outlet');
+ var SpecialOutlet = CoreOutlet.extend({
+ classNames: ['special']
});
- childView = EmberView.create({
- template: compile("BYE
")
- });
+ registry.register("view:special-outlet", SpecialOutlet);
- runAppend(view);
+ var routerState = withTemplate("HI
{{outlet view='special-outlet'}}");
+ top.setOutletState(routerState);
+ runAppend(top);
- equal(view.$().text(), 'HI');
+ equal(top.$().text(), 'HI');
+ routerState.outlets.main = withTemplate("BYE
");
run(function() {
- view.connectOutlet('main', childView);
+ top.setOutletState(routerState);
});
- ok(ContainerView.detectInstance(childView._parentView), "The custom view class should be used for the outlet");
-
// Replace whitespace for older IE
- equal(trim(view.$().text()), 'HIBYE');
-
+ equal(trim(top.$().text()), 'HIBYE');
+ equal(top.$().find('.special').length, 1, "expected to find .special element");
});
QUnit.test("outlet should assert view is specified as a string", function() {
-
- var template = "HI
{{outlet view=containerView}}";
+ top.setOutletState(withTemplate("HI
{{outlet view=containerView}}"));
expectAssertion(function () {
-
- view = EmberView.create({
- template: compile(template),
- container : container
- });
-
- runAppend(view);
-
- });
+ runAppend(top);
+ }, /Using a quoteless view parameter with {{outlet}} is not supported/);
});
QUnit.test("outlet should assert view path is successfully resolved", function() {
-
- var template = "HI
{{outlet view='someViewNameHere'}}";
+ top.setOutletState(withTemplate("HI
{{outlet view='someViewNameHere'}}"));
expectAssertion(function () {
-
- view = EmberView.create({
- template: compile(template),
- container : container
- });
-
- runAppend(view);
-
- });
+ runAppend(top);
+ }, "The view name you supplied 'someViewNameHere' did not resolve to a view.");
});
QUnit.test("outlet should support an optional view class", function() {
- var template = "HI
{{outlet viewClass=view.outletView}}";
- view = EmberView.create({
- template: compile(template),
- outletView: EmberContainerView.extend()
+ var CoreOutlet = container.lookupFactory('view:core-outlet');
+ var SpecialOutlet = CoreOutlet.extend({
+ classNames: ['very-special']
});
+ var routerState = {
+ render: {
+ ViewClass: EmberView.extend({
+ template: compile("HI
{{outlet viewClass=view.outletView}}"),
+ outletView: SpecialOutlet
+ })
+ },
+ outlets: {}
+ };
+ top.setOutletState(routerState);
- runAppend(view);
+ runAppend(top);
- equal(view.$().text(), 'HI');
+ equal(top.$().text(), 'HI');
+ equal(top.$().find('.very-special').length, 1, "Should find .very-special");
- var childView = EmberView.create({
- template: compile("BYE
")
- });
+ routerState.outlets.main = withTemplate("BYE
");
run(function() {
- view.connectOutlet('main', childView);
+ top.setOutletState(routerState);
});
- ok(view.outletView.detectInstance(childView._parentView), "The custom view class should be used for the outlet");
-
// Replace whitespace for older IE
- equal(trim(view.$().text()), 'HIBYE');
+ equal(trim(top.$().text()), 'HIBYE');
});
QUnit.test("Outlets bind to the current view, not the current concrete view", function() {
- var parentTemplate = "HI
{{outlet}}";
- var middleTemplate = "MIDDLE
{{outlet}}";
- var bottomTemplate = "BOTTOM
";
-
- view = EmberView.create({
- template: compile(parentTemplate)
- });
-
- var middleView = _MetamorphView.create({
- template: compile(middleTemplate)
- });
-
- var bottomView = _MetamorphView.create({
- template: compile(bottomTemplate)
- });
-
- runAppend(view);
-
+ var routerState = withTemplate("HI
{{outlet}}");
+ top.setOutletState(routerState);
+ runAppend(top);
+ routerState.outlets.main = withTemplate("MIDDLE
{{outlet}}");
run(function() {
- view.connectOutlet('main', middleView);
+ top.setOutletState(routerState);
});
-
+ routerState.outlets.main.outlets.main = withTemplate("BOTTOM
");
run(function() {
- middleView.connectOutlet('main', bottomView);
+ top.setOutletState(routerState);
});
var output = jQuery('#qunit-fixture h1 ~ h2 ~ h3').text();
equal(output, "BOTTOM", "all templates were rendered");
});
-QUnit.test("view should support disconnectOutlet for the main outlet", function() {
- var template = "HI
{{outlet}}";
- view = EmberView.create({
- template: compile(template)
- });
-
- runAppend(view);
-
- equal(view.$().text(), 'HI');
-
- run(function() {
- view.connectOutlet('main', EmberView.create({
- template: compile("BYE
")
- }));
- });
-
- // Replace whitespace for older IE
- equal(trim(view.$().text()), 'HIBYE');
-
- run(function() {
- view.disconnectOutlet('main');
- });
-
- // Replace whitespace for older IE
- equal(trim(view.$().text()), 'HI');
-});
-
// TODO: Remove flag when {{with}} is fixed.
if (!Ember.FEATURES.isEnabled('ember-htmlbars')) {
// jscs:disable validateIndentation
@@ -258,21 +186,26 @@ QUnit.test("Outlets bind to the current template's view, not inner contexts [DEP
var parentTemplate = "HI
{{#if view.alwaysTrue}}{{#with this}}{{outlet}}{{/with}}{{/if}}";
var bottomTemplate = "BOTTOM
";
- view = EmberView.create({
- alwaysTrue: true,
- template: compile(parentTemplate)
- });
+ var routerState = {
+ render: {
+ ViewClass: EmberView.extend({
+ alwaysTrue: true,
+ template: compile(parentTemplate)
+ })
+ },
+ outlets: {}
+ };
- var bottomView = _MetamorphView.create({
- template: compile(bottomTemplate)
- });
+ top.setOutletState(routerState);
expectDeprecation(function() {
- runAppend(view);
+ runAppend(top);
}, 'Using the context switching form of `{{with}}` is deprecated. Please use the keyword form (`{{with foo as bar}}`) instead.');
+ routerState.outlets.main = withTemplate(bottomTemplate);
+
run(function() {
- view.connectOutlet('main', bottomView);
+ top.setOutletState(routerState);
});
var output = jQuery('#qunit-fixture h1 ~ h3').text();
@@ -285,57 +218,63 @@ QUnit.test("Outlets bind to the current template's view, not inner contexts [DEP
QUnit.test("should support layouts", function() {
var template = "{{outlet}}";
var layout = "HI
{{yield}}";
-
- view = EmberView.create({
- template: compile(template),
- layout: compile(layout)
- });
-
- runAppend(view);
-
- equal(view.$().text(), 'HI');
+ var routerState = {
+ render: {
+ ViewClass: EmberView.extend({
+ template: compile(template),
+ layout: compile(layout)
+ })
+ },
+ outlets: {}
+ };
+ top.setOutletState(routerState);
+ runAppend(top);
+
+ equal(top.$().text(), 'HI');
+
+ routerState.outlets.main = withTemplate("BYE
");
run(function() {
- view.connectOutlet('main', EmberView.create({
- template: compile("BYE
")
- }));
+ top.setOutletState(routerState);
});
+
// Replace whitespace for older IE
- equal(trim(view.$().text()), 'HIBYE');
+ equal(trim(top.$().text()), 'HIBYE');
});
QUnit.test("should not throw deprecations if {{outlet}} is used without a name", function() {
expectNoDeprecation();
- view = EmberView.create({
- template: compile("{{outlet}}")
- });
- runAppend(view);
+ top.setOutletState(withTemplate("{{outlet}}"));
+ runAppend(top);
});
QUnit.test("should not throw deprecations if {{outlet}} is used with a quoted name", function() {
expectNoDeprecation();
- view = EmberView.create({
- template: compile("{{outlet \"foo\"}}")
- });
- runAppend(view);
+ top.setOutletState(withTemplate("{{outlet \"foo\"}}"));
+ runAppend(top);
});
if (Ember.FEATURES.isEnabled('ember-htmlbars')) {
QUnit.test("should throw an assertion if {{outlet}} used with unquoted name", function() {
- view = EmberView.create({
- template: compile("{{outlet foo}}")
- });
+ top.setOutletState(withTemplate("{{outlet foo}}"));
expectAssertion(function() {
- runAppend(view);
+ runAppend(top);
}, "Using {{outlet}} with an unquoted name is not supported.");
});
} else {
QUnit.test("should throw a deprecation if {{outlet}} is used with an unquoted name", function() {
- view = EmberView.create({
- template: compile("{{outlet foo}}")
- });
+ top.setOutletState(withTemplate("{{outlet foo}}"));
expectDeprecation(function() {
- runAppend(view);
+ runAppend(top);
}, 'Using {{outlet}} with an unquoted name is not supported. Please update to quoted usage \'{{outlet "foo"}}\'.');
});
}
+
+function withTemplate(string) {
+ return {
+ render: {
+ template: compile(string)
+ },
+ outlets: {}
+ };
+}
diff --git a/packages/ember-routing-htmlbars/tests/helpers/render_test.js b/packages/ember-routing-htmlbars/tests/helpers/render_test.js
index bd05ad3e111..bb2f10809ff 100644
--- a/packages/ember-routing-htmlbars/tests/helpers/render_test.js
+++ b/packages/ember-routing-htmlbars/tests/helpers/render_test.js
@@ -13,7 +13,7 @@ import { registerHelper } from "ember-htmlbars/helpers";
import helpers from "ember-htmlbars/helpers";
import compile from "ember-template-compiler/system/compile";
-import EmberView from "ember-routing/ext/view";
+import EmberView from "ember-views/views/view";
import jQuery from "ember-views/system/jquery";
import ActionManager from "ember-views/system/action_manager";
@@ -427,30 +427,40 @@ QUnit.test("{{render}} helper should link child controllers to the parent contro
});
QUnit.test("{{render}} helper should be able to render a template again when it was removed", function() {
- var template = "HI
{{outlet}}";
var controller = EmberController.extend({ container: container });
- view = EmberView.create({
- template: compile(template)
- });
+ var CoreOutlet = container.lookupFactory('view:core-outlet');
+ view = CoreOutlet.create();
+ runAppend(view);
Ember.TEMPLATES['home'] = compile("BYE
");
- runAppend(view);
+ var liveRoutes = {
+ render: {
+ template: compile("HI
{{outlet}}")
+ },
+ outlets: {}
+ };
run(function() {
- view.connectOutlet('main', EmberView.create({
- controller: controller.create(),
- template: compile("1{{render 'home'}}
")
- }));
+ liveRoutes.outlets.main = {
+ render: {
+ controller: controller.create(),
+ template: compile("1{{render 'home'}}
")
+ }
+ };
+ view.setOutletState(liveRoutes);
});
equal(view.$().text(), 'HI1BYE');
run(function() {
- view.connectOutlet('main', EmberView.create({
- controller: controller.create(),
- template: compile("2{{render 'home'}}
")
- }));
+ liveRoutes.outlets.main = {
+ render: {
+ controller: controller.create(),
+ template: compile("2{{render 'home'}}
")
+ }
+ };
+ view.setOutletState(liveRoutes);
});
equal(view.$().text(), 'HI2BYE');
diff --git a/packages/ember-routing-htmlbars/tests/utils.js b/packages/ember-routing-htmlbars/tests/utils.js
index afa633a075e..98dce7c8a9c 100644
--- a/packages/ember-routing-htmlbars/tests/utils.js
+++ b/packages/ember-routing-htmlbars/tests/utils.js
@@ -11,7 +11,12 @@ import ObjectController from "ember-runtime/controllers/object_controller";
import ArrayController from "ember-runtime/controllers/array_controller";
import _MetamorphView from "ember-views/views/metamorph_view";
+import EmberView from "ember-views/views/view";
import EmberRouter from "ember-routing/system/router";
+import {
+ OutletView,
+ CoreOutletView
+} from "ember-routing-views/views/outlet";
import HashLocation from "ember-routing/location/hash_location";
@@ -52,6 +57,9 @@ function buildRegistry(namespace) {
registry.register("controller:array", ArrayController, { instantiate: false });
registry.register("view:default", _MetamorphView);
+ registry.register("view:toplevel", EmberView.extend());
+ registry.register("view:-outlet", OutletView);
+ registry.register("view:core-outlet", CoreOutletView);
registry.register("router:main", EmberRouter.extend());
registry.typeInjection("route", "router", "router:main");
diff --git a/packages/ember-routing-views/lib/views/outlet.js b/packages/ember-routing-views/lib/views/outlet.js
index 099523902ec..75e71be0503 100644
--- a/packages/ember-routing-views/lib/views/outlet.js
+++ b/packages/ember-routing-views/lib/views/outlet.js
@@ -5,5 +5,127 @@
import ContainerView from "ember-views/views/container_view";
import { _Metamorph } from "ember-views/views/metamorph_view";
+import { get } from "ember-metal/property_get";
-export var OutletView = ContainerView.extend(_Metamorph);
+export var CoreOutletView = ContainerView.extend({
+ init: function() {
+ this._super();
+ this._childOutlets = [];
+ this._outletState = null;
+ },
+
+ _isOutlet: true,
+
+ _parentOutlet: function() {
+ var parent = this._parentView;
+ while (parent && !parent._isOutlet) {
+ parent = parent._parentView;
+ }
+ return parent;
+ },
+
+ _linkParent: Ember.on('didInsertElement', function() {
+ var parent = this._parentOutlet();
+ if (parent) {
+ parent._childOutlets.push(this);
+ if (parent._outletState) {
+ this.setOutletState(parent._outletState.outlets[this._outletName]);
+ }
+ }
+ }),
+
+ willDestroy: function() {
+ var parent = this._parentOutlet();
+ if (parent) {
+ parent._childOutlets.removeObject(this);
+ }
+ this._super();
+ },
+
+
+ _diffState: function(state) {
+ while (state && emptyRouteState(state)) {
+ state = state.outlets.main;
+ }
+ var different = !sameRouteState(this._outletState, state);
+ this._outletState = state;
+ return different;
+ },
+
+ setOutletState: function(state) {
+ if (!this._diffState(state)) {
+ var children = this._childOutlets;
+ for (var i = 0 ; i < children.length; i++) {
+ var child = children[i];
+ child.setOutletState(this._outletState && this._outletState.outlets[child._outletName]);
+ }
+ } else {
+ var view = this._buildView(this._outletState);
+ var length = get(this, 'length');
+ if (view) {
+ this.replace(0, length, [view]);
+ } else {
+ this.replace(0, length , []);
+ }
+ }
+ },
+
+ _buildView: function(state) {
+ if (!state) { return; }
+
+ var LOG_VIEW_LOOKUPS = get(this, 'namespace.LOG_VIEW_LOOKUPS');
+ var view;
+ var render = state.render;
+ var ViewClass = render.ViewClass;
+ var isDefaultView = false;
+
+ if (!ViewClass) {
+ isDefaultView = true;
+ ViewClass = this.container.lookupFactory(this._isTopLevel ? 'view:toplevel' : 'view:default');
+ }
+
+ view = ViewClass.create({
+ _debugTemplateName: render.name,
+ renderedName: render.name,
+ controller: render.controller
+ });
+
+ if (!get(view, 'template')) {
+ view.set('template', render.template);
+ }
+
+ if (LOG_VIEW_LOOKUPS) {
+ Ember.Logger.info("Rendering " + render.name + " with " + (render.isDefaultView ? "default view " : "") + view, { fullName: 'view:' + render.name });
+ }
+
+ return view;
+ }
+});
+
+function emptyRouteState(state) {
+ return !state.render.ViewClass && !state.render.template;
+}
+
+function sameRouteState(a, b) {
+ if (!a && !b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ a = a.render;
+ b = b.render;
+ for (var key in a) {
+ if (a.hasOwnProperty(key)) {
+ // name is only here for logging & debugging. If two different
+ // names result in otherwise identical states, they're still
+ // identical.
+ if (a[key] !== b[key] && key !== 'name') {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+export var OutletView = CoreOutletView.extend(_Metamorph);
diff --git a/packages/ember-routing/lib/ext/view.js b/packages/ember-routing/lib/ext/view.js
deleted file mode 100644
index 824276a8f95..00000000000
--- a/packages/ember-routing/lib/ext/view.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import { get } from "ember-metal/property_get";
-import { set } from "ember-metal/property_set";
-import run from "ember-metal/run_loop";
-import EmberView from "ember-views/views/view";
-
-/**
-@module ember
-@submodule ember-routing
-*/
-
-EmberView.reopen({
-
- /**
- Sets the private `_outlets` object on the view.
-
- @method init
- */
- init: function() {
- this._outlets = {};
- this._super.apply(this, arguments);
- },
-
- /**
- Manually fill any of a view's `{{outlet}}` areas with the
- supplied view.
-
- Example
-
- ```javascript
- var MyView = Ember.View.extend({
- template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ')
- });
- var myView = MyView.create();
- myView.appendTo('body');
- // The html for myView now looks like:
- // Child view:
-
- var FooView = Ember.View.extend({
- template: Ember.Handlebars.compile('Foo
')
- });
- var fooView = FooView.create();
- myView.connectOutlet('main', fooView);
- // The html for myView now looks like:
- // Child view:
- //
Foo
- //
- ```
- @method connectOutlet
- @param {String} outletName A unique name for the outlet
- @param {Object} view An Ember.View
- */
- connectOutlet: function(outletName, view) {
- if (this._pendingDisconnections) {
- delete this._pendingDisconnections[outletName];
- }
-
- if (this._hasEquivalentView(outletName, view)) {
- view.destroy();
- return;
- }
-
- var outlets = get(this, '_outlets');
- var container = get(this, 'container');
- var router = container && container.lookup('router:main');
- var renderedName = get(view, 'renderedName');
-
- set(outlets, outletName, view);
-
- if (router && renderedName) {
- router._connectActiveView(renderedName, view);
- }
- },
-
- /**
- Determines if the view has already been created by checking if
- the view has the same constructor, template, and context as the
- view in the `_outlets` object.
-
- @private
- @method _hasEquivalentView
- @param {String} outletName The name of the outlet we are checking
- @param {Object} view An Ember.View
- @return {Boolean}
- */
- _hasEquivalentView: function(outletName, view) {
- var existingView = get(this, '_outlets.'+outletName);
- return existingView &&
- existingView.constructor === view.constructor &&
- existingView.get('template') === view.get('template') &&
- existingView.get('context') === view.get('context');
- },
-
- /**
- Removes an outlet from the view.
-
- Example
-
- ```javascript
- var MyView = Ember.View.extend({
- template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ')
- });
- var myView = MyView.create();
- myView.appendTo('body');
- // myView's html:
- // Child view:
-
- var FooView = Ember.View.extend({
- template: Ember.Handlebars.compile('Foo
')
- });
- var fooView = FooView.create();
- myView.connectOutlet('main', fooView);
- // myView's html:
- // Child view:
- //
Foo
- //
-
- myView.disconnectOutlet('main');
- // myView's html:
- // Child view:
- ```
-
- @method disconnectOutlet
- @param {String} outletName The name of the outlet to be removed
- */
- disconnectOutlet: function(outletName) {
- if (!this._pendingDisconnections) {
- this._pendingDisconnections = {};
- }
- this._pendingDisconnections[outletName] = true;
- run.once(this, '_finishDisconnections');
- },
-
- /**
- Gets an outlet that is pending disconnection and then
- nullifies the object on the `_outlet` object.
-
- @private
- @method _finishDisconnections
- */
- _finishDisconnections: function() {
- if (this.isDestroyed) {
- return; // _outlets will be gone anyway
- }
-
- var outlets = get(this, '_outlets');
- var pendingDisconnections = this._pendingDisconnections;
- this._pendingDisconnections = null;
-
- for (var outletName in pendingDisconnections) {
- set(outlets, outletName, null);
- }
- }
-});
-
-export default EmberView;
diff --git a/packages/ember-routing/lib/main.js b/packages/ember-routing/lib/main.js
index 504297d025a..7aab8b3955f 100644
--- a/packages/ember-routing/lib/main.js
+++ b/packages/ember-routing/lib/main.js
@@ -11,7 +11,6 @@ import Ember from "ember-metal/core";
// ES6TODO: Cleanup modules with side-effects below
import "ember-routing/ext/run_loop";
import "ember-routing/ext/controller";
-import "ember-routing/ext/view";
import EmberLocation from "ember-routing/location/api";
import NoneLocation from "ember-routing/location/none_location";
diff --git a/packages/ember-routing/lib/system/route.js b/packages/ember-routing/lib/system/route.js
index 51edc8640c6..e7ab5559271 100644
--- a/packages/ember-routing/lib/system/route.js
+++ b/packages/ember-routing/lib/system/route.js
@@ -3,10 +3,7 @@ import EmberError from "ember-metal/error";
import { get } from "ember-metal/property_get";
import { set } from "ember-metal/property_set";
import getProperties from "ember-metal/get_properties";
-import {
- forEach,
- replace
-}from "ember-metal/enumerable_utils";
+import { forEach } from "ember-metal/enumerable_utils";
import isNone from "ember-metal/is_none";
import { computed } from "ember-metal/computed";
import merge from "ember-metal/merge";
@@ -381,6 +378,7 @@ var Route = EmberObject.extend(ActionHandler, Evented, {
@method enter
*/
enter: function() {
+ this.connections = [];
this.activate();
this.trigger('activate');
},
@@ -1786,6 +1784,7 @@ var Route = EmberObject.extend(ActionHandler, Evented, {
Ember.assert("The name in the given arguments is undefined", arguments.length > 0 ? !isNone(arguments[0]) : true);
var namePassed = typeof _name === 'string' && !!_name;
+ var isDefaultRender = arguments.length === 0 || Ember.isEmpty(arguments[0]);
var name;
if (typeof _name === 'object' && !options) {
@@ -1795,53 +1794,9 @@ var Route = EmberObject.extend(ActionHandler, Evented, {
name = _name;
}
- var templateName;
-
- if (name) {
- name = name.replace(/\//g, '.');
- templateName = name;
- } else {
- name = this.routeName;
- templateName = this.templateName || name;
- }
-
- var renderOptions = buildRenderOptions(this, namePassed, name, options);
-
- var LOG_VIEW_LOOKUPS = get(this.router, 'namespace.LOG_VIEW_LOOKUPS');
- var viewName = options && options.view || namePassed && name || this.viewName || name;
- var view, template;
-
- var ViewClass = this.container.lookupFactory('view:' + viewName);
- if (ViewClass) {
- view = setupView(ViewClass, renderOptions);
- if (!get(view, 'template')) {
- view.set('template', this.container.lookup('template:' + templateName));
- }
- if (LOG_VIEW_LOOKUPS) {
- Ember.Logger.info("Rendering " + renderOptions.name + " with " + view, { fullName: 'view:' + renderOptions.name });
- }
- } else {
- template = this.container.lookup('template:' + templateName);
- if (!template) {
- Ember.assert("Could not find \"" + name + "\" template or view.", arguments.length === 0 || Ember.isEmpty(arguments[0]));
- if (LOG_VIEW_LOOKUPS) {
- Ember.Logger.info("Could not find \"" + name + "\" template or view. Nothing will be rendered", { fullName: 'template:' + name });
- }
- return;
- }
- var defaultView = renderOptions.into ? 'view:default' : 'view:toplevel';
- ViewClass = this.container.lookupFactory(defaultView);
- view = setupView(ViewClass, renderOptions);
- if (!get(view, 'template')) {
- view.set('template', template);
- }
- if (LOG_VIEW_LOOKUPS) {
- Ember.Logger.info("Rendering " + renderOptions.name + " with default view " + view, { fullName: 'view:' + renderOptions.name });
- }
- }
-
- if (renderOptions.outlet === 'main') { this.lastRenderedTemplate = name; }
- appendView(this, view, renderOptions);
+ var renderOptions = buildRenderOptions(this, namePassed, isDefaultRender, name, options);
+ this.connections.push(renderOptions);
+ run.once(this.router, '_setOutlets');
},
/**
@@ -1888,16 +1843,29 @@ var Route = EmberObject.extend(ActionHandler, Evented, {
@param {Object|String} options the options hash or outlet name
*/
disconnectOutlet: function(options) {
+ var outletName;
+ var parentView;
if (!options || typeof options === "string") {
- var outletName = options;
- options = {};
- options.outlet = outletName;
+ outletName = options;
+ } else {
+ outletName = options.outlet;
+ parentView = options.parentView;
+ }
+
+ parentView = parentView && parentView.replace(/\//g, '.');
+ if (parentView === parentRoute(this).routeName) {
+ parentView = undefined;
}
- options.parentView = options.parentView ? options.parentView.replace(/\//g, '.') : parentTemplate(this);
- options.outlet = options.outlet || 'main';
+ outletName = outletName || 'main';
- var parentView = this.router._lookupActiveView(options.parentView);
- if (parentView) { parentView.disconnectOutlet(options.outlet); }
+ 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);
+ run.once(this.router, '_setOutlets');
+ return;
+ }
+ }
},
willDestroy: function() {
@@ -1910,18 +1878,10 @@ var Route = EmberObject.extend(ActionHandler, Evented, {
@method teardownViews
*/
teardownViews: function() {
- // Tear down the top level view
- if (this.teardownTopLevelView) { this.teardownTopLevelView(); }
-
- // Tear down any outlets rendered with 'into'
- var teardownOutletViews = this.teardownOutletViews || [];
- forEach(teardownOutletViews, function(teardownOutletView) {
- teardownOutletView();
- });
-
- delete this.teardownTopLevelView;
- delete this.teardownOutletViews;
- delete this.lastRenderedTemplate;
+ if (this.connections && this.connections.length > 0) {
+ this.connections = [];
+ run.once(this.router, '_setOutlets');
+ }
}
});
@@ -1947,21 +1907,23 @@ function handlerInfoFor(route, handlerInfos, _offset) {
}
}
-function parentTemplate(route) {
- var parent = parentRoute(route);
+function buildRenderOptions(route, namePassed, isDefaultRender, name, options) {
+ var controller = options && options.controller;
+ var templateName;
+ var viewName;
+ var ViewClass;
var template;
+ var LOG_VIEW_LOOKUPS = get(route.router, 'namespace.LOG_VIEW_LOOKUPS');
+ var into = options && options.into && options.into.replace(/\//g, '.');
+ var outlet = (options && options.outlet) || 'main';
- if (!parent) { return; }
-
- if (template = parent.lastRenderedTemplate) {
- return template;
+ if (name) {
+ name = name.replace(/\//g, '.');
+ templateName = name;
} else {
- return parentTemplate(parent);
+ name = route.routeName;
+ templateName = route.templateName || name;
}
-}
-
-function buildRenderOptions(route, namePassed, name, options) {
- var controller = options && options.controller;
if (!controller) {
if (namePassed) {
@@ -1983,55 +1945,41 @@ function buildRenderOptions(route, namePassed, name, options) {
controller.set('model', options.model);
}
- var renderOptions = {
- into: options && options.into ? options.into.replace(/\//g, '.') : parentTemplate(route),
- outlet: (options && options.outlet) || 'main',
- name: name,
- controller: controller
- };
-
- Ember.assert("An outlet ("+renderOptions.outlet+") was specified but was not found.", renderOptions.outlet === 'main' || renderOptions.into);
-
- return renderOptions;
-}
-
-function setupView(ViewClass, options) {
- return ViewClass.create({
- _debugTemplateName: options.name,
- renderedName: options.name,
- controller: options.controller
- });
-}
-
-function appendView(route, view, options) {
- if (options.into) {
- var parentView = route.router._lookupActiveView(options.into);
- var teardownOutletView = generateOutletTeardown(parentView, options.outlet);
- if (!route.teardownOutletViews) { route.teardownOutletViews = []; }
- replace(route.teardownOutletViews, 0, 0, [teardownOutletView]);
- parentView.connectOutlet(options.outlet, view);
- } else {
- // tear down view if one is already rendered
- if (route.teardownTopLevelView) {
- route.teardownTopLevelView();
+ viewName = options && options.view || namePassed && name || route.viewName || name;
+ ViewClass = route.container.lookupFactory('view:' + viewName);
+ template = route.container.lookup('template:' + templateName);
+ if (!ViewClass && !template) {
+ Ember.assert("Could not find \"" + name + "\" template or view.", isDefaultRender);
+ if (LOG_VIEW_LOOKUPS) {
+ Ember.Logger.info("Could not find \"" + name + "\" template or view. Nothing will be rendered", { fullName: 'template:' + name });
}
+ }
- route.router._connectActiveView(options.name, view);
- route.teardownTopLevelView = function() { view.destroy(); };
+ Ember.assert("An outlet ("+outlet+") was specified but was not found.", outlet === 'main' || into);
- // Notify the application instance that we have created the root-most
- // view. It is the responsibility of the instance to tell the root view
- // how to render, typically by appending it to the application's
- // `rootElement`.
- var instance = route.container.lookup('-application-instance:main');
- instance.didCreateRootView(view);
+ Ember.assert(
+ "You attempted to render into '" + into + "' but it was not found",
+ !into || Ember.A(route.router.router.state.handlerInfos).any(function(info) {
+ return Ember.A(info.handler.connections || []).any(function(conn) {
+ return conn.name === into;
+ });
+ })
+ );
+
+ if (into && into === parentRoute(route).routeName) {
+ into = undefined;
}
-}
-function generateOutletTeardown(parentView, outlet) {
- return function() {
- parentView.disconnectOutlet(outlet);
+ var renderOptions = {
+ into: into,
+ outlet: outlet,
+ name: name,
+ controller: controller,
+ ViewClass: ViewClass,
+ template: template
};
+
+ return renderOptions;
}
function getFullQueryParams(router, state) {
diff --git a/packages/ember-routing/lib/system/router.js b/packages/ember-routing/lib/system/router.js
index bc6a5b739f1..48e7c9cdc64 100644
--- a/packages/ember-routing/lib/system/router.js
+++ b/packages/ember-routing/lib/system/router.js
@@ -188,6 +188,45 @@ var EmberRouter = EmberObject.extend(Evented, {
}
},
+ _setOutlets: function() {
+ var handlerInfos = this.router.currentHandlerInfos;
+ var route;
+ var parentRoute;
+ var defaultParentState;
+ var liveRoutes = null;
+
+ if (!handlerInfos) {
+ return;
+ }
+
+ for (var i = 0; i < handlerInfos.length; i++) {
+ route = handlerInfos[i].handler;
+
+ var connections = (route.connections.length > 0) ? route.connections : [{
+ name: route.routeName,
+ outlet: 'main'
+ }];
+
+ var ownState;
+ for (var j = 0; j < connections.length; j++) {
+ var appended = appendLiveRoute(liveRoutes, route, parentRoute, defaultParentState, connections[j]);
+ liveRoutes = appended.liveRoutes;
+ if (appended.ownState.render.name === route.routeName) {
+ ownState = appended.ownState;
+ }
+ }
+ parentRoute = route;
+ defaultParentState = ownState;
+ }
+ if (!this._toplevelView) {
+ var OutletView = this.container.lookupFactory('view:-outlet');
+ this._toplevelView = OutletView.create({ _isTopLevel: true });
+ var instance = this.container.lookup('-application-instance:main');
+ instance.didCreateRootView(this._toplevelView);
+ }
+ this._toplevelView.setOutletState(liveRoutes);
+ },
+
/**
Handles notifying any listeners of an impending URL
change.
@@ -316,6 +355,10 @@ var EmberRouter = EmberObject.extend(Evented, {
},
willDestroy: function() {
+ if (this._toplevelView) {
+ this._toplevelView.destroy();
+ this._toplevelView = null;
+ }
this._super.apply(this, arguments);
this.reset();
},
@@ -949,4 +992,45 @@ function forEachQueryParam(router, targetRouteName, queryParams, callback) {
}
}
+function findLiveRoute(liveRoutes, name) {
+ var stack = [liveRoutes];
+ while (stack.length > 0) {
+ var test = stack.shift();
+ if (test.render.name === name) {
+ return test;
+ }
+ var outlets = test.outlets;
+ for (var outletName in outlets) {
+ stack.push(outlets[outletName]);
+ }
+ }
+}
+
+function appendLiveRoute(liveRoutes, route, parentRoute, defaultParentState, renderOptions) {
+ var targetName;
+ var target;
+ var myState = {
+ render: renderOptions,
+ outlets: Object.create(null)
+ };
+ if (!parentRoute) {
+ liveRoutes = myState;
+ }
+ targetName = renderOptions.into || (parentRoute && parentRoute.routeName);
+ if (renderOptions.into) {
+ target = findLiveRoute(liveRoutes, renderOptions.into);
+ } else {
+ target = defaultParentState;
+ }
+ if (target) {
+ set(target.outlets, renderOptions.outlet, myState);
+ }
+ return {
+ liveRoutes: liveRoutes,
+ ownState: myState
+ };
+}
+
+
+
export default EmberRouter;
diff --git a/packages/ember/tests/routing/basic_test.js b/packages/ember/tests/routing/basic_test.js
index a986e94f4dc..97d30bcad41 100644
--- a/packages/ember/tests/routing/basic_test.js
+++ b/packages/ember/tests/routing/basic_test.js
@@ -168,28 +168,6 @@ QUnit.test("The Home page and the Camelot page with multiple Router.map calls",
equal(Ember.$('h3:contains(Hours)', '#qunit-fixture').length, 1, "The home template was rendered");
});
-QUnit.test("The Homepage register as activeView", function() {
- Router.map(function() {
- this.route("home", { path: "/" });
- this.route("homepage");
- });
-
- App.HomeRoute = Ember.Route.extend({
- });
-
- App.HomepageRoute = Ember.Route.extend({
- });
-
- bootApplication();
-
- ok(router._lookupActiveView('home'), '`home` active view is connected');
-
- handleURL('/homepage');
-
- ok(router._lookupActiveView('homepage'), '`homepage` active view is connected');
- equal(router._lookupActiveView('home'), undefined, '`home` active view is disconnected');
-});
-
QUnit.test("The Homepage with explicit template name in renderTemplate", function() {
Router.map(function() {
this.route("home", { path: "/" });
@@ -2566,6 +2544,22 @@ QUnit.test("Route should tear down multiple outlets", function() {
});
+QUnit.test("Route will assert if you try to explicitly render {into: ...} a missing template", function () {
+ Router.map(function() {
+ this.route("home", { path: "/" });
+ });
+
+ App.HomeRoute = Ember.Route.extend({
+ renderTemplate: function() {
+ this.render({ into: 'nonexistent' });
+ }
+ });
+
+ expectAssertion(function() {
+ bootApplication();
+ }, "You attempted to render into 'nonexistent' but it was not found");
+});
+
QUnit.test("Route supports clearing outlet explicitly", function() {
Ember.TEMPLATES.application = compile("{{outlet}}{{outlet 'modal'}}");
Ember.TEMPLATES.posts = compile("{{outlet}}");
@@ -3434,3 +3428,54 @@ QUnit.test("Exception during load of initial route is not swallowed", function()
bootApplication();
}, /\bboom\b/);
});
+
+QUnit.test("{{outlet}} works when created after initial render", function() {
+ Ember.TEMPLATES.sample = compile("Hi{{#if showTheThing}}{{outlet}}{{/if}}Bye");
+ Ember.TEMPLATES['sample/inner'] = compile("Yay");
+ Ember.TEMPLATES['sample/inner2'] = compile("Boo");
+ Router.map(function() {
+ this.route('sample', { path: '/' }, function() {
+ this.route('inner', { path: '/' });
+ this.route('inner2', { path: '/2' });
+ });
+ });
+
+ bootApplication();
+
+ equal(Ember.$('#qunit-fixture').text(), "HiBye", "initial render");
+
+ Ember.run(function() {
+ container.lookup('controller:sample').set('showTheThing', true);
+ });
+
+ equal(Ember.$('#qunit-fixture').text(), "HiYayBye", "second render");
+
+ handleURL('/2');
+
+ equal(Ember.$('#qunit-fixture').text(), "HiBooBye", "third render");
+});
+
+QUnit.test("Can rerender application view multiple times when it contains an outlet", function() {
+ Ember.TEMPLATES.application = compile("App{{outlet}}");
+ Ember.TEMPLATES.index = compile("Hello world");
+
+ registry.register('view:application', Ember.View.extend({
+ elementId: 'im-special'
+ }));
+
+ bootApplication();
+
+ equal(Ember.$('#qunit-fixture').text(), "AppHello world", "initial render");
+
+ Ember.run(function() {
+ Ember.View.views['im-special'].rerender();
+ });
+
+ equal(Ember.$('#qunit-fixture').text(), "AppHello world", "second render");
+
+ Ember.run(function() {
+ Ember.View.views['im-special'].rerender();
+ });
+
+ equal(Ember.$('#qunit-fixture').text(), "AppHello world", "third render");
+});
diff --git a/tests/node/app-boot-test.js b/tests/node/app-boot-test.js
index 39f7b23429c..4175d4f5c82 100644
--- a/tests/node/app-boot-test.js
+++ b/tests/node/app-boot-test.js
@@ -21,15 +21,24 @@ QUnit.module("App boot");
QUnit.test("App is created without throwing an exception", function() {
var App;
+ var domHelper = new DOMHelper(new SimpleDOM.Document());
Ember.run(function() {
- App = Ember.Application.create();
- App.Router = Ember.Router.extend({
- location: 'none'
+ App = createApplication();
+
+ App.instanceInitializer({
+ name: 'stub-renderer',
+ initialize: function(app) {
+ app.registry.register('renderer:-dom', {
+ create: function() {
+ return new Ember.View._Renderer(domHelper);
+ }
+ });
+ }
});
- App.advanceReadiness();
+ App.visit('/');
});
QUnit.ok(App);
@@ -143,8 +152,9 @@ QUnit.test("It is possible to render a view with {{link-to}} in Node", function(
var run = Ember.run;
var app;
var URL = require('url');
+ var document = new SimpleDOM.Document();
- var domHelper = new DOMHelper(new SimpleDOM.Document());
+ var domHelper = new DOMHelper(document);
domHelper.protocolForURL = function(url) {
var protocol = URL.parse(url).protocol;
return (protocol == null) ? ':' : protocol;
@@ -174,7 +184,7 @@ QUnit.test("It is possible to render a view with {{link-to}} in Node", function(
QUnit.start();
var morph = {
- contextualElement: {},
+ contextualElement: document.body,
setContent: function(element) {
this.element = element;
}