Skip to content

Commit

Permalink
Lazily reify router’s location
Browse files Browse the repository at this point in the history
Users can override a router’s location by specifying its `location`
property as a string. For example, to change the router from the “auto”
location to the “none” location, users can do the following:

```js
// app/router.js
export default Ember.Router.extend({
 location: 'none'
});
```

Previously, the reification of the string value into a concrete Location
implementation happened at router creation time.

This immediate reification made it difficult to make changes to the
router configuration, since it had to be done at creation time. In
general, classes that require configuration to be set at creation time
are unwieldy to use with the container, since the container itself
manages creation.

For example, in the case of the Application's `visit()` API, the
application instance would like to override the router to use the
`none` location.

When reification was at creation time, the router was created with the
wrong location. Before the default could be overridden, the router would
try to set up things like listeners on the browser's address bar, which
would cause an exception to be thrown in Node environments where there
is no notion of a URL.

In this commit, reifying and setting up the location are deferred until
the last possible moment, when routing starts (either by calling
`startRouting()`, which starts the app at the browser's current URL, or
by calling `handleURL()`, which starts the app at a provided URL). This
allows the application to detect if it is in autoboot mode or not, and
override the router's location before routing begins.
  • Loading branch information
tilde-engineering authored and tomdale committed Mar 3, 2015
1 parent 2638767 commit 8e130e5
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 24 deletions.
46 changes: 31 additions & 15 deletions packages/ember-application/lib/system/application-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
@private
*/

import { get } from "ember-metal/property_get";
import { set } from "ember-metal/property_set";
import EmberObject from "ember-runtime/system/object";
import run from "ember-metal/run_loop";
import { computed } from "ember-metal/computed";
import Registry from 'container/registry';

/**
Expand Down Expand Up @@ -103,23 +105,24 @@ export default EmberObject.extend({
this.registry.register('-application-instance:main', this, { instantiate: false });
},

router: computed(function() {
return this.container.lookup('router:main');
}),

/**
Instantiates and sets up the router, optionally overriding the default
Instantiates and sets up the router, specifically overriding the default
location. This is useful for manually starting the app in FastBoot or
testing environments, where trying to modify the URL would be
inappropriate.
@param options
@private
*/
setupRouter: function(options) {
var router = this.container.lookup('router:main');
overrideRouterLocation: function(options) {
var location = options && options.location;
var router = get(this, 'router');

var location = options.location;
if (location) { set(router, 'location', location); }

router._setupLocation();
router.setupRouter(true);
},

/**
Expand All @@ -143,17 +146,30 @@ export default EmberObject.extend({
current URL of the page to determine the initial URL to start routing to.
To start the app at a specific URL, call `handleURL` instead.
Ensure that you have called `setupRouter()` on the instance before using
this method.
@private
*/
startRouting: function() {
var router = this.container.lookup('router:main');
if (!router) { return; }

var router = get(this, 'router');
var isModuleBasedResolver = !!this.registry.resolver.moduleBasedResolver;

router.startRouting(isModuleBasedResolver);
this._didSetupRouter = true;
},

/** @private
Sets up the router, initializing the child router and configuring the
location before routing begins.
Because setup should only occur once, multiple calls to `setupRouter`
beyond the first call have no effect.
*/
setupRouter: function() {
if (this._didSetupRouter) { return; }
this._didSetupRouter = true;

var router = get(this, 'router');
var isModuleBasedResolver = !!this.registry.resolver.moduleBasedResolver;
router.setupRouter(isModuleBasedResolver);
},

/**
Expand All @@ -165,8 +181,9 @@ export default EmberObject.extend({
@private
*/
handleURL: function(url) {
var router = this.container.lookup('router:main');
var router = get(this, 'router');

this.setupRouter();
return router.handleURL(url);
},

Expand All @@ -175,7 +192,6 @@ export default EmberObject.extend({
*/
setupEventDispatcher: function() {
var dispatcher = this.container.lookup('event_dispatcher:main');

dispatcher.setup(this.customEvents, this.rootElement);

return dispatcher;
Expand Down
5 changes: 2 additions & 3 deletions packages/ember-routing/lib/system/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ var EmberRouter = EmberObject.extend(Evented, {

init: function() {
this._activeViews = {};
this._setupLocation();
this._qpCache = {};
this._queuedQPChanges = {};
},
Expand All @@ -126,9 +125,8 @@ var EmberRouter = EmberObject.extend(Evented, {
*/
startRouting: function(moduleBasedResolver) {
var initialURL = get(this, 'initialURL');
var location = get(this, 'location');

if (this.setupRouter(moduleBasedResolver, location)) {
if (this.setupRouter(moduleBasedResolver)) {
if (typeof initialURL === "undefined") {
initialURL = get(this, 'location').getURL();
}
Expand All @@ -141,6 +139,7 @@ var EmberRouter = EmberObject.extend(Evented, {

setupRouter: function(moduleBasedResolver) {
this._initRouterJs(moduleBasedResolver);
this._setupLocation();

var router = this.router;
var location = get(this, 'location');
Expand Down
22 changes: 18 additions & 4 deletions packages/ember-routing/tests/system/router_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import { runDestroy } from "ember-runtime/tests/utils";

var registry, container;

function createRouter(overrides) {
function createRouter(overrides, disableSetup) {
var opts = merge({ container: container }, overrides);
var routerWithContainer = Router.extend();
var router = routerWithContainer.create(opts);

return routerWithContainer.create(opts);
if (!disableSetup) {
router.setupRouter();
}

return router;
}

QUnit.module("Ember Router", {
Expand All @@ -35,17 +40,26 @@ QUnit.module("Ember Router", {
});

QUnit.test("can create a router without a container", function() {
createRouter({ container: null });
createRouter({ container: null }, true);

ok(true, 'no errors were thrown when creating without a container');
});

QUnit.test("should not create a router.js instance upon init", function() {
var router = createRouter();
var router = createRouter(null, true);

ok(!router.router);
});

QUnit.test("should not reify location until setupRouter is called", function() {
var router = createRouter(null, true);
equal(typeof router.location, 'string', "location is specified as a string");

router.setupRouter();

equal(typeof router.location, 'object', "location is reified into an object");
});

QUnit.test("should destroy its location upon destroying the routers container.", function() {
var router = createRouter();
var location = router.get('location');
Expand Down
2 changes: 1 addition & 1 deletion packages/ember-testing/lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ function focus(el) {

function visit(app, url) {
var router = app.__container__.lookup('router:main');
router.location.setURL(url);

if (app._readinessDeferrals > 0) {
router['initialURL'] = url;
run(app, 'advanceReadiness');
delete router['initialURL'];
} else {
router.location.setURL(url);
run(app.__deprecatedInstance__, 'handleURL', url);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ember-testing/tests/helpers_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ QUnit.test("Ember.Application#setupForTesting", function() {
App.setupForTesting();
});

equal(App.__container__.lookup('router:main').location.implementation, 'none');
equal(App.__container__.lookup('router:main').location, 'none');
});

QUnit.test("Ember.Application.setupForTesting sets the application to `testing`.", function() {
Expand Down

0 comments on commit 8e130e5

Please sign in to comment.