Skip to content

Commit

Permalink
Merge pull request #3655 from machty/substates-enhancements
Browse files Browse the repository at this point in the history
[FEATURE ember-routing-loading-error-substates] Named substate entry
  • Loading branch information
machty committed Nov 1, 2013
2 parents 6a29470 + 7fda592 commit cf43a8c
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 83 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
### Ember 1.3.0 _(TBD)_

* Add query params support to the ember router. You can now define which query params your routes respond to, use them in your route hooks to affect model loading or controller state, and transition query parameters with the link-to helper and the transitionTo method

* Add named substates; e.g. when resolving a `loading` or `error` substate to enter, Ember will take into account the name of the immediate child route that the `error`/`loading` action originated from, e.g. 'foo' if `FooRoute`, and try and enter `foo_error` or `foo_loading` if it exists. This also adds the ability for a top-level `application_loading` or `application_error` state to be entered for `loading`/`error` events emitted from `ApplicationRoute`.

### Ember 1.2.0 _(TBD)_

Expand Down
11 changes: 11 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,14 @@ for a detailed explanation.

Added in [#3568](https://github.com/emberjs/ember.js/pull/3568) and feature
flagged in [#3617](https://github.com/emberjs/ember.js/pull/3617).

* `ember-routing-named-substates`

Add named substates; e.g. when resolving a `loading` or `error`
substate to enter, Ember will take into account the name of the
immediate child route that the `error`/`loading` action originated
from, e.g. 'foo' if `FooRoute`, and try and enter `foo_error` or
`foo_loading` if it exists. This also adds the ability for a
top-level `application_loading` or `application_error` state to
be entered for `loading`/`error` events emitted from
`ApplicationRoute`.
3 changes: 2 additions & 1 deletion features.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"ember-testing-wait-hooks": null,
"query-params": null,
"string-humanize": null,
"propertyBraceExpansion": null
"propertyBraceExpansion": null,
"ember-routing-named-substates": null
}
46 changes: 32 additions & 14 deletions packages/ember-routing/lib/system/dsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,23 @@ DSL.prototype = {

if (callback) {
var dsl = new DSL(name);
dsl.route('loading');
dsl.route('error', { path: "/_unused_dummy_error_path/:error" });
route(dsl, 'loading');
route(dsl, 'error', { path: "/_unused_dummy_error_path_route_" + name + "/:error" });
callback.call(dsl);
this.push(options.path, name, dsl.generate(), options.queryParams);
} else {
this.push(options.path, name, null, options.queryParams);
}


if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) {
// For namespace-preserving nested resource (e.g. resource('foo.bar') within
// resource('foo')) we only want to use the last route name segment to determine
// the names of the error/loading substates (e.g. 'bar_loading')
name = name.split('.').pop();
route(this, name + '_loading');
route(this, name + '_error', { path: "/_unused_dummy_error_path_route_" + name + "/:error" });
}
},

push: function(url, name, callback, queryParams) {
Expand All @@ -42,19 +52,11 @@ DSL.prototype = {
},

route: function(name, options) {
Ember.assert("You must use `this.resource` to nest", typeof options !== 'function');

options = options || {};

if (typeof options.path !== 'string') {
options.path = "/" + name;
}

if (this.parent && this.parent !== 'application') {
name = this.parent + "." + name;
route(this, name, options);
if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) {
route(this, name + '_loading');
route(this, name + '_error', { path: "/_unused_dummy_error_path_route_" + name + "/:error" });
}

this.push(options.path, name, null, options.queryParams);
},

generate: function() {
Expand All @@ -78,6 +80,22 @@ DSL.prototype = {
}
};

function route(dsl, name, options) {
Ember.assert("You must use `this.resource` to nest", typeof options !== 'function');

options = options || {};

if (typeof options.path !== 'string') {
options.path = "/" + name;
}

if (dsl.parent && dsl.parent !== 'application') {
name = dsl.parent + "." + name;
}

dsl.push(options.path, name, null, options.queryParams);
}

DSL.map = function(callback) {
var dsl = new DSL();
callback.call(dsl);
Expand Down
58 changes: 2 additions & 56 deletions packages/ember-routing/lib/system/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,6 @@ var get = Ember.get, set = Ember.set,
a_forEach = Ember.EnumerableUtils.forEach,
a_replace = Ember.EnumerableUtils.replace;

var defaultActionHandlers = {

willResolveModel: function(transition, originRoute) {
this.router._scheduleLoadingEvent(transition, originRoute);
},

error: function(error, transition, originRoute) {
if (Ember.FEATURES.isEnabled("ember-routing-loading-error-substates")) {
if (this !== originRoute) {
var childErrorRouteName = findChildRouteName(this, 'error');
if (childErrorRouteName) {
this.intermediateTransitionTo(childErrorRouteName, error);
return;
}
}
}

if (this.routeName === 'application') {
Ember.Logger.assert(false, 'Error while loading route: ' + Ember.inspect(error));
}

return true;
},

loading: function(transition, originRoute) {
if (Ember.FEATURES.isEnabled("ember-routing-loading-error-substates")) {
if (this === originRoute) {
// This is the route with the error; just bubble
// so that the parent route can look up its child loading route.
return true;
}

var childLoadingRouteName = findChildRouteName(this, 'loading');
if (childLoadingRouteName) {
this.intermediateTransitionTo(childLoadingRouteName);
} else if (transition.pivotHandler !== this) {
return true;
}
}
}
};

/**
The `Ember.Route` class is used to define individual routes. Refer to
Expand Down Expand Up @@ -296,15 +255,13 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, {
*/
actions: null,

_actions: defaultActionHandlers,

/**
@deprecated
Please use `actions` instead.
@method events
*/
events: defaultActionHandlers,
events: null,

mergedProperties: ['events'],

Expand Down Expand Up @@ -848,7 +805,7 @@ Ember.Route = Ember.Object.extend(Ember.ActionHandler, {
instance would be used.
Example
```js
App.PostRoute = Ember.Route.extend({
setupController: function(controller, model) {
Expand Down Expand Up @@ -1275,15 +1232,4 @@ function generateOutletTeardown(parentView, outlet) {
return function() { parentView.disconnectOutlet(outlet); };
}

function findChildRouteName(route, name) {
var container = route.container;

var childName = route.routeName === 'application' ? name : route.routeName + '.' + name;

var hasChild = route.router.hasRoute(childName) &&
(container.has('template:' + childName) ||
container.has('route:' + childName));

return hasChild && childName;
}

146 changes: 143 additions & 3 deletions packages/ember-routing/lib/system/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,133 @@ Ember.Router = Ember.Object.extend(Ember.Evented, {
}
});

/**
@private
Helper function for iterating root-ward, starting
from (but not including) the provided `originRoute`.
Returns true if the last callback fired requested
to bubble upward.
*/
function forEachRouteAbove(originRoute, transition, callback) {
var handlerInfos = transition.handlerInfos,
originRouteFound = false;

for (var i = handlerInfos.length - 1; i >= 0; --i) {
var handlerInfo = handlerInfos[i],
route = handlerInfo.handler;

if (!originRouteFound) {
if (originRoute === route) {
originRouteFound = true;
}
continue;
}

if (callback(route, handlerInfos[i + 1].handler) !== true) {
return false;
}
}
return true;
}

// These get invoked when an action bubbles above ApplicationRoute
// and are not meant to be overridable.
var defaultActionHandlers = {

willResolveModel: function(transition, originRoute) {
originRoute.router._scheduleLoadingEvent(transition, originRoute);
},

error: function(error, transition, originRoute) {
if (Ember.FEATURES.isEnabled("ember-routing-loading-error-substates")) {

// Attempt to find an appropriate error substate to enter.
var router = originRoute.router;

var tryTopLevel = forEachRouteAbove(originRoute, transition, function(route, childRoute) {
var childErrorRouteName = findChildRouteName(route, childRoute, 'error');
if (childErrorRouteName) {
router.intermediateTransitionTo(childErrorRouteName, error);
return;
}
return true;
});

if (tryTopLevel) {
// Check for top-level error state to enter.
if (routeHasBeenDefined(originRoute.router, 'application_error')) {
router.intermediateTransitionTo('application_error', error);
return;
}
} else {
// Don't fire an assertion if we found an error substate.
return;
}
}

Ember.Logger.assert(false, 'Error while loading route: ' + Ember.inspect(error));
},

loading: function(transition, originRoute) {
if (Ember.FEATURES.isEnabled("ember-routing-loading-error-substates")) {

// Attempt to find an appropriate loading substate to enter.
var router = originRoute.router;

var tryTopLevel = forEachRouteAbove(originRoute, transition, function(route, childRoute) {
var childLoadingRouteName = findChildRouteName(route, childRoute, 'loading');

if (childLoadingRouteName) {
router.intermediateTransitionTo(childLoadingRouteName);
return;
}

// Don't bubble above pivot route.
if (transition.pivotHandler !== route) {
return true;
}
});

if (tryTopLevel) {
// Check for top-level loading state to enter.
if (routeHasBeenDefined(originRoute.router, 'application_loading')) {
router.intermediateTransitionTo('application_loading');
return;
}
}
}
}
};

function findChildRouteName(parentRoute, originatingChildRoute, name) {
var router = parentRoute.router,
childName,
targetChildRouteName = originatingChildRoute.routeName.split('.').pop(),
namespace = parentRoute.routeName === 'application' ? '' : parentRoute.routeName + '.';

if (Ember.FEATURES.isEnabled("ember-routing-named-substates")) {
// First, try a named loading state, e.g. 'foo_loading'
childName = namespace + targetChildRouteName + '_' + name;
if (routeHasBeenDefined(router, childName)) {
return childName;
}
}

// Second, try general loading state, e.g. 'loading'
childName = namespace + name;
if (routeHasBeenDefined(router, childName)) {
return childName;
}
}

function routeHasBeenDefined(router, name) {
var container = router.container;
return router.hasRoute(name) &&
(container.has('template:' + name) || container.has('route:' + name));
}

function triggerEvent(handlerInfos, ignoreFailure, args) {
var name = args.shift();

Expand All @@ -320,14 +447,27 @@ function triggerEvent(handlerInfos, ignoreFailure, args) {
}
}

if (defaultActionHandlers[name]) {
defaultActionHandlers[name].apply(null, args);
return;
}

if (!eventWasHandled && !ignoreFailure) {
throw new Ember.Error("Nothing handled the action '" + name + "'. If you did handle the action, this error can be caused by returning true from an action handler, causing the action to bubble.");
throw new Ember.Error("Nothing handled the action '" + name + "'.");
}
}

function updatePaths(router) {
var appController = router.container.lookup('controller:application'),
infos = router.router.currentHandlerInfos,
var appController = router.container.lookup('controller:application');

if (!appController) {
// appController might not exist when top-level loading/error
// substates have been entered since ApplicationRoute hasn't
// actually been entered at that point.
return;
}

var infos = router.router.currentHandlerInfos,
path = Ember.Router._routePath(infos);

if (!('currentPath' in appController)) {
Expand Down
4 changes: 3 additions & 1 deletion packages/ember/tests/routing/basic_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,9 @@ asyncTest("The Special page returning an error invokes SpecialRoute's error hand
});

function testOverridableErrorHandler(handlersName) {

expect(2);

Router.map(function() {
this.route("home", { path: "/" });
this.resource("special", { path: "/specials/:menu_item_id" });
Expand All @@ -770,7 +773,6 @@ function testOverridableErrorHandler(handlersName) {
var attrs = {};
attrs[handlersName] = {
error: function(reason) {
ok(typeof this._super === 'function', "_super is available");
equal(reason, 'Setup error', "error was correctly passed to custom ApplicationRoute handler");
start();
}
Expand Down
Loading

0 comments on commit cf43a8c

Please sign in to comment.