Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get handlers lazily #187

Merged
merged 3 commits into from
Aug 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 71 additions & 12 deletions lib/router/handler-info.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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,

Expand Down
3 changes: 1 addition & 2 deletions lib/router/handler-info/unresolved-handler-info-by-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
17 changes: 8 additions & 9 deletions lib/router/transition-intent/named-transition-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
Expand All @@ -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
Expand Down Expand Up @@ -183,7 +182,7 @@ export default subclass(TransitionIntent, {

return handlerInfoFactory('param', {
name: name,
handler: handler,
getHandler: getHandler,
params: params
});
}
Expand Down
19 changes: 9 additions & 10 deletions lib/router/transition-intent/url-transition-intent.js
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions test/tests/router_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down