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; }