diff --git a/lib/router/handler-info.js b/lib/router/handler-info.js index d669c2ac..ad59043c 100644 --- a/lib/router/handler-info.js +++ b/lib/router/handler-info.js @@ -1,21 +1,29 @@ import { bind, merge, promiseLabel, applyHook, isPromise } from './utils'; import Promise from 'rsvp/promise'; +var DEFAULT_HANDLER = Object.freeze({}); + function HandlerInfo(_props) { var props = _props || {}; - var name = props.name; - // Setup a handlerPromise so that we can wait for asynchronously loaded handlers - this.handlerPromise = Promise.resolve(props.handler); + // Set a default handler to ensure consistent object shape + this._handler = DEFAULT_HANDLER; - // Wait until the 'handler' property has been updated when chaining to a handler - // that is a promise - if (isPromise(props.handler)) { - this.handlerPromise = this.handlerPromise.then(bind(this, this.updateHandler)); - props.handler = undefined; - } else if (props.handler) { - // Store the name of the handler on the handler for easy checks later - props.handler._handlerName = name; + if (props.handler) { + var name = props.name; + + // Setup a handlerPromise so that we can wait for asynchronously loaded handlers + this.handlerPromise = Promise.resolve(props.handler); + + // Wait until the 'handler' property has been updated when chaining to a handler + // that is a promise + if (isPromise(props.handler)) { + this.handlerPromise = this.handlerPromise.then(bind(this, this.updateHandler)); + props.handler = undefined; + } else if (props.handler) { + // Store the name of the handler on the handler for easy checks later + props.handler._handlerName = name; + } } merge(this, props); @@ -24,7 +32,58 @@ function HandlerInfo(_props) { HandlerInfo.prototype = { name: null, - handler: null, + + getHandler: function() {}, + + fetchHandler: function() { + var handler = this.getHandler(this.name); + + // Setup a handlerPromise so that we can wait for asynchronously loaded handlers + this.handlerPromise = Promise.resolve(handler); + + // Wait until the 'handler' property has been updated when chaining to a handler + // that is a promise + if (isPromise(handler)) { + this.handlerPromise = this.handlerPromise.then(bind(this, this.updateHandler)); + } else if (handler) { + // Store the name of the handler on the handler for easy checks later + handler._handlerName = this.name; + return this.handler = handler; + } + + return this.handler = undefined; + }, + + get handler() { + // _handler could be set to either a handler object or undefined, so we + // compare against a default reference to know when it's been set + if (this._handler !== DEFAULT_HANDLER) { + return this._handler; + } + + return this.fetchHandler(); + }, + + set handler(handler) { + return this._handler = handler; + }, + + _handlerPromise: undefined, + + get handlerPromise() { + if (this._handlerPromise) { + return this._handlerPromise; + } + + this.fetchHandler(); + + return this._handlerPromise; + }, + + set handlerPromise(handlerPromise) { + return this._handlerPromise = handlerPromise; + }, + params: null, context: null, diff --git a/lib/router/handler-info/unresolved-handler-info-by-object.js b/lib/router/handler-info/unresolved-handler-info-by-object.js index 99f3b1ad..021261d1 100644 --- a/lib/router/handler-info/unresolved-handler-info-by-object.js +++ b/lib/router/handler-info/unresolved-handler-info-by-object.js @@ -25,8 +25,7 @@ var UnresolvedHandlerInfoByObject = subclass(HandlerInfo, { serialize: function(_model) { var model = _model || this.context, names = this.names, - handler = this.handler, - serializer = this.serializer || (handler && handler.serialize); + serializer = this.serializer || (this.handler && this.handler.serialize); var object = {}; if (isParam(model)) { diff --git a/lib/router/transition-intent/named-transition-intent.js b/lib/router/transition-intent/named-transition-intent.js index 23e765b9..d35c5390 100644 --- a/lib/router/transition-intent/named-transition-intent.js +++ b/lib/router/transition-intent/named-transition-intent.js @@ -48,24 +48,23 @@ export default subclass(TransitionIntent, { for (i = handlers.length - 1; i >= 0; --i) { var result = handlers[i]; var name = result.handler; - var handler = getHandler(name); var oldHandlerInfo = oldState.handlerInfos[i]; var newHandlerInfo = null; if (result.names.length > 0) { if (i >= invalidateIndex) { - newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + newHandlerInfo = this.createParamHandlerInfo(name, getHandler, result.names, objects, oldHandlerInfo); } else { var serializer = getSerializer(name); - newHandlerInfo = this.getHandlerInfoForDynamicSegment(name, handler, result.names, objects, oldHandlerInfo, targetRouteName, i, serializer); + newHandlerInfo = this.getHandlerInfoForDynamicSegment(name, getHandler, result.names, objects, oldHandlerInfo, targetRouteName, i, serializer); } } else { // This route has no dynamic segment. // Therefore treat as a param-based handlerInfo // with empty params. This will cause the `model` // hook to be called with empty params, which is desirable. - newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + newHandlerInfo = this.createParamHandlerInfo(name, getHandler, result.names, objects, oldHandlerInfo); } if (checkingIfActive) { @@ -116,14 +115,14 @@ export default subclass(TransitionIntent, { } }, - getHandlerInfoForDynamicSegment: function(name, handler, names, objects, oldHandlerInfo, targetRouteName, i, serializer) { + getHandlerInfoForDynamicSegment: function(name, getHandler, names, objects, oldHandlerInfo, targetRouteName, i, serializer) { var objectToUse; if (objects.length > 0) { // Use the objects provided for this transition. objectToUse = objects[objects.length - 1]; if (isParam(objectToUse)) { - return this.createParamHandlerInfo(name, handler, names, objects, oldHandlerInfo); + return this.createParamHandlerInfo(name, getHandler, names, objects, oldHandlerInfo); } else { objects.pop(); } @@ -148,14 +147,14 @@ export default subclass(TransitionIntent, { return handlerInfoFactory('object', { name: name, - handler: handler, + getHandler: getHandler, serializer: serializer, context: objectToUse, names: names }); }, - createParamHandlerInfo: function(name, handler, names, objects, oldHandlerInfo) { + createParamHandlerInfo: function(name, getHandler, names, objects, oldHandlerInfo) { var params = {}; // Soak up all the provided string/numbers @@ -183,7 +182,7 @@ export default subclass(TransitionIntent, { return handlerInfoFactory('param', { name: name, - handler: handler, + getHandler: getHandler, params: params }); } diff --git a/lib/router/transition-intent/url-transition-intent.js b/lib/router/transition-intent/url-transition-intent.js index e2cf3fa5..eea0f0da 100644 --- a/lib/router/transition-intent/url-transition-intent.js +++ b/lib/router/transition-intent/url-transition-intent.js @@ -1,7 +1,7 @@ import TransitionIntent from '../transition-intent'; import TransitionState from '../transition-state'; import handlerInfoFactory from '../handler-info/factory'; -import { merge, subclass, isPromise } from '../utils'; +import { merge, subclass } from '../utils'; import UnrecognizedURLError from './../unrecognized-url-error'; export default subclass(TransitionIntent, { @@ -28,7 +28,7 @@ export default subclass(TransitionIntent, { // For the case where the handler is loaded asynchronously, the error will be // thrown once it is loaded. function checkHandlerAccessibility(handler) { - if (handler.inaccessibleByURL) { + if (handler && handler.inaccessibleByURL) { throw new UnrecognizedURLError(url); } @@ -38,19 +38,18 @@ export default subclass(TransitionIntent, { for (i = 0, len = results.length; i < len; ++i) { var result = results[i]; var name = result.handler; - var handler = getHandler(name); - - checkHandlerAccessibility(handler); - var newHandlerInfo = handlerInfoFactory('param', { name: name, - handler: handler, + getHandler: getHandler, params: result.params }); + var handler = newHandlerInfo.handler; - // If the hanlder is being loaded asynchronously, check again if we can - // access it after it has resolved - if (isPromise(handler)) { + if (handler) { + checkHandlerAccessibility(handler); + } else { + // If the hanlder is being loaded asynchronously, check if we can + // access it after it has resolved newHandlerInfo.handlerPromise = newHandlerInfo.handlerPromise.then(checkHandlerAccessibility); } diff --git a/package.json b/package.json index c01aabe3..e8fe3b75 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "grunt-broccoli": "^0.2.0", "grunt-cli": "~0.1.11", "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-qunit": "~0.3.0", + "grunt-contrib-qunit": "^1.2.0", "grunt-s3": "~0.2.0-alpha.2", "load-grunt-config": "~0.5.0", "load-grunt-tasks": "~0.2.0" diff --git a/test/tests/router_test.js b/test/tests/router_test.js index 4a0077c7..158ed755 100644 --- a/test/tests/router_test.js +++ b/test/tests/router_test.js @@ -2596,6 +2596,21 @@ test("Generate works w queryparams", function(assert) { assert.equal(router.generate('index', { queryParams: { foo: '123', bar: '456' } }), '/index?bar=456&foo=123', "just index"); }); +if (scenario.async) { + test("Generate does not invoke getHandler", function(assert) { + var originalGetHandler = router.getHandler; + router.getHandler = function() { + assert.ok(false, 'getHandler should not be called'); + }; + + assert.equal(router.generate('index'), '/index', "just index"); + assert.equal(router.generate('index', { queryParams: { foo: '123' } }), '/index?foo=123', "just index"); + assert.equal(router.generate('index', { queryParams: { foo: '123', bar: '456' } }), '/index?bar=456&foo=123', "just index"); + + router.getHandler = originalGetHandler; + }); +} + test("errors in enter/setup hooks fire `error`", function(assert) { assert.expect(4);