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

[FEATURE "ember-routing-transitioning-classes”] #9919

Merged
merged 1 commit into from
Dec 19, 2014
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
11 changes: 11 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,14 @@ for a detailed explanation.
to perform the lookup instead. Replaces the need for `needs` in controllers.

Added in [#5162](https://github.com/emberjs/ember.js/pull/5162).

* `ember-routing-transitioning-classes`

Disables eager URL updates during slow transitions in favor of new CSS
classes added to `link-to`s (in addition to `active` class):

- `transitioning-in`: link-to is not currently active, but will be
when the current underway (slow) transition completes.
- `transitioning-out`: link-to is currently active, but will no longer
be active when the current underway (slow) transition completes.

2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"devDependencies": {
"backburner": "https://github.com/ebryn/backburner.js.git#f4bd6a2df221240ed36d140f0c53c036a7ecacad",
"rsvp": "https://github.com/tildeio/rsvp.js.git#3.0.14",
"router.js": "https://github.com/tildeio/router.js.git#9471aaaa28c11907d8fdf8969b70c6a93ad19fd1",
"router.js": "https://github.com/tildeio/router.js.git#a1ffd97dc66a6d9d4e8dd89a72c1c4e21a3328c5",
"route-recognizer": "https://github.com/tildeio/route-recognizer.git#8e1058e29de741b8e05690c69da9ec402a167c69",
"dag-map": "https://github.com/krisselden/dag-map.git#e307363256fe918f426e5a646cb5f5062d3245be",
"ember-dev": "https://github.com/emberjs/ember-dev.git#1c30a1666273ab2a9b134a42bad28c774f9ecdfc"
Expand Down
3 changes: 2 additions & 1 deletion features.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"ember-htmlbars-block-params": true,
"ember-htmlbars-component-generation": null,
"ember-htmlbars-inline-if-helper": null,
"ember-htmlbars-attribute-syntax": null
"ember-htmlbars-attribute-syntax": null,
"ember-routing-transitioning-classes": null
},
"debugStatements": [
"Ember.warn",
Expand Down
116 changes: 65 additions & 51 deletions packages/ember-routing-views/lib/views/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ var numberOfContextsAcceptedByHandler = function(handler, handlerInfos) {
return req;
};

var linkViewClassNameBindings = ['active', 'loading', 'disabled'];
if (Ember.FEATURES.isEnabled('ember-routing-transitioning-classes')) {
linkViewClassNameBindings = ['active', 'loading', 'disabled', 'transitioningIn', 'transitioningOut'];
}

/**
`Ember.LinkView` renders an element whose `click` event triggers a
transition of the application's instance of `Ember.Router` to
Expand Down Expand Up @@ -149,7 +154,7 @@ var LinkView = Ember.LinkView = EmberComponent.extend({
@type Array
@default ['active', 'loading', 'disabled']
**/
classNameBindings: ['active', 'loading', 'disabled'],
classNameBindings: linkViewClassNameBindings,

/**
By default the `{{link-to}}` helper responds to the `click` event. You
Expand Down Expand Up @@ -289,56 +294,32 @@ var LinkView = Ember.LinkView = EmberComponent.extend({
@property active
**/
active: computed('loadedParams', function computeLinkViewActive() {
if (get(this, 'loading')) { return false; }
var router = get(this, 'router');
if (!router) { return; }
return computeActive(this, router.currentState);
}),

willBeActive: computed('router.targetState', function() {
var router = get(this, 'router');
var loadedParams = get(this, 'loadedParams');
var contexts = loadedParams.models;
var currentWhen = this['current-when'] || this.currentWhen;
var isCurrentWhenSpecified = Boolean(currentWhen);
currentWhen = currentWhen || loadedParams.targetRouteName;

function isActiveForRoute(routeName) {
var handlers = router.router.recognizer.handlersFor(routeName);
var leafName = handlers[handlers.length-1].handler;
var maximumContexts = numberOfContextsAcceptedByHandler(routeName, handlers);

// NOTE: any ugliness in the calculation of activeness is largely
// due to the fact that we support automatic normalizing of
// `resource` -> `resource.index`, even though there might be
// dynamic segments / query params defined on `resource.index`
// which complicates (and makes somewhat ambiguous) the calculation
// of activeness for links that link to `resource` instead of
// directly to `resource.index`.

// if we don't have enough contexts revert back to full route name
// this is because the leaf route will use one of the contexts
if (contexts.length > maximumContexts) {
routeName = leafName;
}
if (!router) { return; }
var targetState = router.targetState;
if (router.currentState === targetState) { return; }

var args = routeArgs(routeName, contexts, null);
var isActive = router.isActive.apply(router, args);
if (!isActive) { return false; }
return !!computeActive(this, targetState);
}),

var emptyQueryParams = Ember.isEmpty(Ember.keys(loadedParams.queryParams));
transitioningIn: computed('active', 'willBeActive', function() {
var willBeActive = get(this, 'willBeActive');
if (typeof willBeActive === 'undefined') { return false; }

if (!isCurrentWhenSpecified && !emptyQueryParams && isActive) {
var visibleQueryParams = {};
merge(visibleQueryParams, loadedParams.queryParams);
router._prepareQueryParams(loadedParams.targetRouteName, loadedParams.models, visibleQueryParams);
isActive = shallowEqual(visibleQueryParams, router.router.state.queryParams);
}
return !get(this, 'active') && willBeActive;
}),

return isActive;
}
transitioningOut: computed('active', 'willBeActive', function() {
var willBeActive = get(this, 'willBeActive');
if (typeof willBeActive === 'undefined') { return false; }

currentWhen = currentWhen.split(' ');
for (var i = 0, len = currentWhen.length; i < len; i++) {
if (isActiveForRoute(currentWhen[i])) {
return get(this, 'activeClass');
}
}
return get(this, 'active') && !willBeActive;
}),

/**
Expand Down Expand Up @@ -408,6 +389,10 @@ var LinkView = Ember.LinkView = EmberComponent.extend({
transition.method('replace');
}

if (Ember.FEATURES.isEnabled('ember-routing-transitioning-classes')) {
return;
}

// Schedule eager URL update, but after we've given the transition
// a chance to synchronously redirect.
// We need to always generate the URL instead of using the href because
Expand Down Expand Up @@ -591,15 +576,44 @@ function paramsAreLoaded(params) {
return true;
}

function shallowEqual(a, b) {
var k;
for (k in a) {
if (a.hasOwnProperty(k) && a[k] !== b[k]) { return false; }
function computeActive(route, routerState) {
if (get(route, 'loading')) { return false; }

var currentWhen = route['current-when'] || route.currentWhen;
var isCurrentWhenSpecified = !!currentWhen;
currentWhen = currentWhen || get(route, 'loadedParams').targetRouteName;
currentWhen = currentWhen.split(' ');
for (var i = 0, len = currentWhen.length; i < len; i++) {
if (isActiveForRoute(route, currentWhen[i], isCurrentWhenSpecified, routerState)) {
return get(route, 'activeClass');
}
}
for (k in b) {
if (b.hasOwnProperty(k) && a[k] !== b[k]) { return false; }
}

function isActiveForRoute(route, routeName, isCurrentWhenSpecified, routerState) {
var router = get(route, 'router');
var loadedParams = get(route, 'loadedParams');
var contexts = loadedParams.models;

var handlers = router.router.recognizer.handlersFor(routeName);
var leafName = handlers[handlers.length-1].handler;
var maximumContexts = numberOfContextsAcceptedByHandler(routeName, handlers);

// NOTE: any ugliness in the calculation of activeness is largely
// due to the fact that we support automatic normalizing of
// `resource` -> `resource.index`, even though there might be
// dynamic segments / query params defined on `resource.index`
// which complicates (and makes somewhat ambiguous) the calculation
// of activeness for links that link to `resource` instead of
// directly to `resource.index`.

// if we don't have enough contexts revert back to full route name
// this is because the leaf route will use one of the contexts
if (contexts.length > maximumContexts) {
routeName = leafName;
}
return true;

return routerState.isActiveIntent(routeName, contexts, loadedParams.queryParams, !isCurrentWhenSpecified);
}

export {
Expand Down
48 changes: 35 additions & 13 deletions packages/ember-routing/lib/system/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
} from "ember-routing/utils";
import { create } from "ember-metal/platform";

import RouterState from "./router_state";

/**
@module ember
@submodule ember-routing
Expand Down Expand Up @@ -147,9 +149,10 @@ var EmberRouter = EmberObject.extend(Evented, {
didTransition: function(infos) {
updatePaths(this);

this._cancelLoadingEvent();
this._cancelSlowTransitionTimer();

this.notifyPropertyChange('url');
this.set('currentState', this.targetState);

// Put this in the runloop so url will be accurate. Seems
// less surprising than didTransition being out of sync.
Expand All @@ -169,7 +172,7 @@ var EmberRouter = EmberObject.extend(Evented, {

_doURLTransition: function(routerJsMethod, url) {
var transition = this.router[routerJsMethod](url || '/');
listenForTransitionErrors(transition);
didBeginTransition(transition, this);
return transition;
},

Expand Down Expand Up @@ -237,8 +240,7 @@ var EmberRouter = EmberObject.extend(Evented, {
@since 1.7.0
*/
isActiveIntent: function(routeName, models, queryParams) {
var router = this.router;
return router.isActive.apply(router, arguments);
return this.currentState.isActiveIntent(routeName, models, queryParams);
},

send: function(name, context) {
Expand Down Expand Up @@ -441,7 +443,7 @@ var EmberRouter = EmberObject.extend(Evented, {
var transitionArgs = routeArgs(targetRouteName, models, queryParams);
var transitionPromise = this.router.transitionTo.apply(this.router, transitionArgs);

listenForTransitionErrors(transitionPromise);
didBeginTransition(transitionPromise, this);

return transitionPromise;
},
Expand Down Expand Up @@ -539,25 +541,34 @@ var EmberRouter = EmberObject.extend(Evented, {
},

_scheduleLoadingEvent: function(transition, originRoute) {
this._cancelLoadingEvent();
this._loadingStateTimer = run.scheduleOnce('routerTransitions', this, '_fireLoadingEvent', transition, originRoute);
this._cancelSlowTransitionTimer();
this._slowTransitionTimer = run.scheduleOnce('routerTransitions', this, '_handleSlowTransition', transition, originRoute);
},

_fireLoadingEvent: function(transition, originRoute) {
currentState: null,
targetState: null,

_handleSlowTransition: function(transition, originRoute) {
if (!this.router.activeTransition) {
// Don't fire an event if we've since moved on from
// the transition that put us in a loading state.
return;
}

this.set('targetState', RouterState.create({
emberRouter: this,
routerJs: this.router,
routerJsState: this.router.activeTransition.state
}));

transition.trigger(true, 'loading', transition, originRoute);
},

_cancelLoadingEvent: function () {
if (this._loadingStateTimer) {
run.cancel(this._loadingStateTimer);
_cancelSlowTransitionTimer: function () {
if (this._slowTransitionTimer) {
run.cancel(this._slowTransitionTimer);
}
this._loadingStateTimer = null;
this._slowTransitionTimer = null;
}
});

Expand Down Expand Up @@ -859,7 +870,18 @@ EmberRouter.reopenClass({
}
});

function listenForTransitionErrors(transition) {
function didBeginTransition(transition, router) {
var routerState = RouterState.create({
emberRouter: router,
routerJs: router.router,
routerJsState: transition.state
});

if (!router.currentState) {
router.set('currentState', routerState);
}
router.set('targetState', routerState);

transition.then(null, function(error) {
if (!error || !error.name) { return; }

Expand Down
40 changes: 40 additions & 0 deletions packages/ember-routing/lib/system/router_state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Ember from "ember-metal/core";
import EmberObject from "ember-runtime/system/object";
import merge from "ember-metal/merge";

var RouterState = EmberObject.extend({
emberRouter: null,
routerJs: null,
routerJsState: null,

isActiveIntent: function(routeName, models, queryParams, queryParamsMustMatch) {
var state = this.routerJsState;
if (!this.routerJs.isActiveIntent(routeName, models, null, state)) { return false; }

var emptyQueryParams = Ember.isEmpty(Ember.keys(queryParams));

if (queryParamsMustMatch && !emptyQueryParams) {
var visibleQueryParams = {};
merge(visibleQueryParams, queryParams);

this.emberRouter._prepareQueryParams(routeName, models, visibleQueryParams);
return shallowEqual(visibleQueryParams, state.queryParams);
}

return true;
}
});

function shallowEqual(a, b) {
var k;
for (k in a) {
if (a.hasOwnProperty(k) && a[k] !== b[k]) { return false; }
}
for (k in b) {
if (b.hasOwnProperty(k) && a[k] !== b[k]) { return false; }
}
return true;
}

export default RouterState;

Loading