Permalink
Browse files

Make routing unwind properly

This commit is a number of things that help with
the goal of making the back/forward buttons work:

* The route method on a router will send the
  `unroute` event before the `route` event. This
  will cause the route to unwind to a shared
  parent state.
* Add a bunch of inline docs to routable.js
* Move routing concerns to `Router`
* Make stateMeta more general
  • Loading branch information...
wycats committed Jun 4, 2012
1 parent c233d1d commit 7ff23ee362b142f301b595e439afa964f3895b7c
@@ -42,59 +42,103 @@ Ember.Routable = Ember.Mixin.create({
Ember.assert("You cannot use `redirectsTo` on a state that has child states", !redirection || (!!redirection && !!get(this, 'isLeaf')));
},
/**
@private
Whenever a routable state is entered, the context it was entered with
is stashed so that we can regenerate the state's `absoluteURL` on
demand.
*/
stashContext: function(manager, context) {
var meta = get(manager, 'stateMeta'),
serialized = this.serialize(manager, context);
var serialized = this.serialize(manager, context);
meta.set(this, serialized);
manager.setStateMeta(this, 'serialized', serialized);
if (get(this, 'isRoutable')) {
this.updateRoute(manager, get(manager, 'location'));
}
},
/**
@private
Whenever a routable state is entered, the router's location object
is notified to set the URL to the current absolute path.
In general, this will update the browser's URL.
*/
updateRoute: function(manager, location) {
if (location && get(this, 'isLeaf')) {
var path = this.absoluteRoute(manager);
location.setURL(path);
}
},
/**
@private
Get the absolute route for the current state and a given
hash.
This method is private, as it expects a serialized hash,
not the original context object.
*/
absoluteRoute: function(manager, hash) {
var parentState = get(this, 'parentState');
var path = '';
var path = '', generated;
// If the parent state is routable, use its current path
// as this route's prefix.
if (get(parentState, 'isRoutable')) {
path = parentState.absoluteRoute(manager, hash);
}
var matcher = get(this, 'routeMatcher'),
meta = get(manager, 'stateMeta').get(this);
serialized = manager.getStateMeta(this, 'serialized');
// merge the existing serialized object in with the passed
// in hash.
hash = hash || {};
merge(hash, meta);
Ember.assert("couldn't find route property for " + get(this, 'path'), !!matcher);
merge(hash, serialized);
var generated = matcher.generate(hash);
generated = matcher && matcher.generate(hash);
if (generated !== '') {
if (generated) {
path = path + '/' + generated;
}
return path;
},
/**
@private
At the moment, a state is routable if it has a string `route`
property. This heuristic may change.
*/
isRoutable: Ember.computed(function() {
return typeof this.route === 'string';
}).cacheable(),
/**
@private
A _RouteMatcher object generated from the current route's `route`
string property.
*/
routeMatcher: Ember.computed(function() {
if (get(this, 'route')) {
return Ember._RouteMatcher.create({ route: get(this, 'route') });
}
}).cacheable(),
/**
@private
The model class associated with the current state. This property
uses the `modelType` property, in order to allow it to be
specified as a String.
*/
modelClass: Ember.computed(function() {
var modelType = get(this, 'modelType');
@@ -105,17 +149,30 @@ Ember.Routable = Ember.Mixin.create({
}
}).cacheable(),
modelClassFor: function(manager) {
var modelClass, namespace, routeMatcher, identifiers, match, className;
/**
@private
// if an explicit modelType was specified, use that
if (modelClass = get(this, 'modelClass')) { return modelClass; }
Get the model class for the state. The heuristic is:
* The state must have a single dynamic segment
* The dynamic segment must end in `_id`
* A dynamic segment like `blog_post_id` is converted into `BlogPost`
* The name is then looked up on the passed in namespace
The process of initializing an application with a router will
pass the application's namespace into the router, which will be
used here.
*/
modelClassFor: function(namespace) {
// if the router has no lookup namespace, we won't be able to guess
// the modelType
namespace = get(manager, 'namespace');
if (!namespace) { return; }
var modelClass, routeMatcher, identifiers, match, className;
// if an explicit modelType was specified, use that
if (modelClass = get(this, 'modelClass')) { return modelClass; }
// make sure this state is actually a routable state
routeMatcher = get(this, 'routeMatcher');
if (!routeMatcher) { return; }
@@ -137,20 +194,39 @@ Ember.Routable = Ember.Mixin.create({
return get(namespace, className);
},
/**
The default method that takes a `params` object and converts
it into an object.
By default, a params hash that looks like `{ post_id: 1 }`
will be looked up as `namespace.Post.find(1)`. This is
designed to work seamlessly with Ember Data, but will work
fine with any class that has a `find` method.
*/
deserialize: function(manager, params) {
var modelClass, routeMatcher, param;
if (modelClass = this.modelClassFor(manager)) {
if (modelClass = this.modelClassFor(get(manager, 'namespace'))) {
return modelClass.find(params[paramForClass(modelClass)]);
}
return params;
},
/**
The default method that takes an object and converts it into
a params hash.
By default, if there is a single dynamic segment named
`blog_post_id` and the object is a `BlogPost` with an
`id` of `12`, the serialize method will produce:
{ blog_post_id: 12 }
*/
serialize: function(manager, context) {
var modelClass, routeMatcher, namespace, param, id;
if (modelClass = this.modelClassFor(manager)) {
if (modelClass = this.modelClassFor(get(manager, 'namespace'))) {
param = paramForClass(modelClass);
id = get(context, 'id');
context = {};
@@ -160,6 +236,16 @@ Ember.Routable = Ember.Mixin.create({
return context;
},
/**
@private
Once `unroute` has finished unwinding, `routePath` will be called
with the remainder of the route.
For example, if you were in the /posts/1/comments state, and you
moved into the /posts/2/comments state, `routePath` will be called
on the state whose path is `/posts` with the path `/2/comments`.
*/
routePath: function(manager, path) {
if (get(this, 'isLeaf')) { return; }
@@ -181,6 +267,48 @@ Ember.Routable = Ember.Mixin.create({
manager.send('routePath', match.remaining);
},
/**
@private
When you move to a new route by pressing the back
or forward button, this method is called first.
Its job is to move the state manager into a parent
state of the state it will eventually move into.
*/
unroutePath: function(router, path) {
// If we're at the root state, we're done
if (get(this, 'parentState') === router) {
return;
}
path = path.replace(/^(?=[^\/])/, "/");
var absolutePath = this.absoluteRoute(router);
// If the current path is empty, move up one state,
// because the index ('/') state must be a leaf node.
if (absolutePath !== '') {
// If the current path is a prefix of the path we're trying
// to go to, we're done.
var index = path.indexOf(absolutePath),
next = path.charAt(absolutePath.length);
if (index === 0 && (next === "/" || next === "")) {
return;
}
}
// Transition to the parent and call unroute again.
var parentPath = get(get(this, 'parentState'), 'path');
router.transitionTo(parentPath);
router.send('unroutePath', path);
},
/**
The `connectOutlets` method will be triggered once a
state has been entered. It will be called with the
route's context.
*/
connectOutlets: Ember.K
});
@@ -2,7 +2,7 @@ require('ember-states/state');
require('ember-states/route_matcher');
require('ember-states/routable');
var get = Ember.get;
var get = Ember.get, getPath = Ember.getPath;
/**
@class
@@ -34,11 +34,30 @@ Ember.Router = Ember.StateManager.extend(
transitionEvent: 'connectOutlets',
route: function(path) {
if (path.charAt(0) === '/') {
path = path.substr(1);
path = path.replace(/^(?=[^\/])/, "/");
this.send('unroutePath', path);
var currentURL = get(this, 'currentState').absoluteRoute(this);
var rest = path.substr(currentURL.length);
this.send('routePath', rest);
},
urlFor: function(path, hash) {
var currentState = get(this, 'currentState') || this,
state = this.findStateByPath(currentState, path);
Ember.assert("To get a URL for a state, it must have a `route` property.", !!get(state, 'routeMatcher'));
var location = get(this, 'location'),
url = state.absoluteRoute(this, hash);
if (location) {
url = location.formatURL(url);
}
this.send('routePath', path);
return url;
},
urlForEvent: function(eventName, context) {
@@ -139,9 +139,15 @@ Ember.State = Ember.Object.extend(Ember.Evented,
exit: Ember.K
});
var Event = Ember.$ && Ember.$.Event;
Ember.State.transitionTo = function(target) {
var event = function(router, event) {
router.transitionTo(target, event.context);
var event = function(router, context) {
if (Event && context instanceof Event) {
context = context.context;
}
router.transitionTo(target, context);
};
event.transitionTarget = target;
@@ -394,6 +394,26 @@ Ember.StateManager = Ember.State.extend(
}
},
stateMetaFor: function(state) {
var meta = get(this, 'stateMeta'),
stateMeta = meta.get(state);
if (!stateMeta) {
stateMeta = {};
meta.set(state, stateMeta);
}
return stateMeta;
},
setStateMeta: function(state, key, value) {
return set(this.stateMetaFor(state), key, value);
},
getStateMeta: function(state, key) {
return get(this.stateMetaFor(state), key);
},
/**
The current state from among the manager's possible states. This property should
not be set directly. Use `transitionTo` to move between states by name.
@@ -422,6 +442,7 @@ Ember.StateManager = Ember.State.extend(
send: function(event, context) {
Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState'));
if (arguments.length === 1) { context = {}; }
this.sendRecursively(event, get(this, 'currentState'), context);
},
@@ -521,19 +542,6 @@ Ember.StateManager = Ember.State.extend(
}).join(".");
},
urlFor: function(path, hash) {
var currentState = get(this, 'currentState') || this,
state = this.findStateByPath(currentState, path),
location = get(this, 'location'),
url = state.absoluteRoute(this, hash);
if (location) {
url = location.formatURL(url);
}
return url;
},
transitionTo: function(name, context) {
// 1. Normalize arguments
// 2. Ensure that we are in the correct state
Oops, something went wrong.

0 comments on commit 7ff23ee

Please sign in to comment.