Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

WIP new router

  • Loading branch information...
commit 6710993558511dc8e201354b3aa0956dfa86ae33 1 parent 3ab87c6
@wycats wycats authored
Showing with 1,523 additions and 589 deletions.
  1. +4 −3 Assetfile
  2. 0  packages/{ember-routing → ember-old-router}/lib/location.js
  3. 0  packages/{ember-routing → ember-old-router}/lib/location/api.js
  4. 0  packages/{ember-routing → ember-old-router}/lib/location/hash_location.js
  5. 0  packages/{ember-routing → ember-old-router}/lib/location/history_location.js
  6. 0  packages/{ember-routing → ember-old-router}/lib/location/none_location.js
  7. +11 −0 packages/ember-old-router/lib/main.js
  8. 0  packages/{ember-routing → ember-old-router}/lib/resolved_state.js
  9. 0  packages/{ember-routing → ember-old-router}/lib/routable.js
  10. 0  packages/{ember-routing → ember-old-router}/lib/route.js
  11. 0  packages/{ember-routing → ember-old-router}/lib/route_matcher.js
  12. +636 −0 packages/ember-old-router/lib/router.js
  13. +32 −0 packages/ember-old-router/package.json
  14. 0  packages/{ember-routing → ember-old-router}/tests/location_test.js
  15. 0  packages/{ember-routing → ember-old-router}/tests/render_test.js
  16. 0  packages/{ember-routing → ember-old-router}/tests/routable_test.js
  17. 0  packages/{ember-routing → ember-old-router}/tests/router_test.js
  18. +0 −11 packages/ember-routing/lib/main.js
  19. +475 −0 packages/ember-routing/lib/route-recognizer.js
  20. +365 −574 packages/ember-routing/lib/router.js
  21. +0 −1  packages/ember-routing/package.json
View
7 Assetfile
@@ -114,9 +114,10 @@ class VersionInfo < Rake::Pipeline::Filter
end
distros = {
- "runtime" => %w(ember-metal ember-runtime),
- "data-deps" => %w(ember-metal ember-runtime ember-states),
- "full" => %w(ember-metal rsvp ember-runtime ember-application ember-views ember-states ember-routing ember-viewstates metamorph ember-handlebars)
+ "runtime" => %w(ember-metal ember-runtime),
+ "data-deps" => %w(ember-metal ember-runtime ember-states),
+ "full" => %w(ember-metal rsvp ember-runtime ember-application ember-views ember-states ember-routing metamorph ember-handlebars),
+ "old-router" => %w(ember-metal rsvp ember-runtime ember-application ember-views ember-states ember-old-router ember-viewstates metamorph ember-handlebars)
}
output "dist"
View
0  packages/ember-routing/lib/location.js → packages/ember-old-router/lib/location.js
File renamed without changes
View
0  packages/ember-routing/lib/location/api.js → packages/ember-old-router/lib/location/api.js
File renamed without changes
View
0  packages/ember-routing/lib/location/hash_location.js → ...es/ember-old-router/lib/location/hash_location.js
File renamed without changes
View
0  ...es/ember-routing/lib/location/history_location.js → ...ember-old-router/lib/location/history_location.js
File renamed without changes
View
0  packages/ember-routing/lib/location/none_location.js → ...es/ember-old-router/lib/location/none_location.js
File renamed without changes
View
11 packages/ember-old-router/lib/main.js
@@ -0,0 +1,11 @@
+require('ember-states');
+require('ember-routing/route');
+require('ember-routing/router');
+
+/**
+Ember Routing
+
+@module ember
+@submodule ember-routing
+@requires ember-states
+*/
View
0  packages/ember-routing/lib/resolved_state.js → packages/ember-old-router/lib/resolved_state.js
File renamed without changes
View
0  packages/ember-routing/lib/routable.js → packages/ember-old-router/lib/routable.js
File renamed without changes
View
0  packages/ember-routing/lib/route.js → packages/ember-old-router/lib/route.js
File renamed without changes
View
0  packages/ember-routing/lib/route_matcher.js → packages/ember-old-router/lib/route_matcher.js
File renamed without changes
View
636 packages/ember-old-router/lib/router.js
@@ -0,0 +1,636 @@
+require('ember-routing/route_matcher');
+require('ember-routing/routable');
+require('ember-routing/location');
+
+/**
+@module ember
+@submodule ember-routing
+*/
+
+var get = Ember.get, set = Ember.set;
+
+var merge = function(original, hash) {
+ for (var prop in hash) {
+ if (!hash.hasOwnProperty(prop)) { continue; }
+ if (original.hasOwnProperty(prop)) { continue; }
+
+ original[prop] = hash[prop];
+ }
+};
+
+/**
+ `Ember.Router` is the subclass of `Ember.StateManager` responsible for providing URL-based
+ application state detection. The `Ember.Router` instance of an application detects the browser URL
+ at application load time and attempts to match it to a specific application state. Additionally
+ the router will update the URL to reflect an application's state changes over time.
+
+ ## Adding a Router Instance to Your Application
+ An instance of Ember.Router can be associated with an instance of Ember.Application in one of two ways:
+
+ You can provide a subclass of Ember.Router as the `Router` property of your application. An instance
+ of this Router class will be instantiated and route detection will be enabled when the application's
+ `initialize` method is called. The Router instance will be available as the `router` property
+ of the application:
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({ ... })
+ });
+
+ App.initialize();
+ App.get('router') // an instance of App.Router
+
+ If you want to define a Router instance elsewhere, you can pass the instance to the application's
+ `initialize` method:
+
+ App = Ember.Application.create();
+ aRouter = Ember.Router.create({ ... });
+
+ App.initialize(aRouter);
+ App.get('router') // aRouter
+
+ ## Adding Routes to a Router
+ The `initialState` property of Ember.Router instances is named `root`. The state stored in this
+ property must be a subclass of Ember.Route. The `root` route acts as the container for the
+ set of routable states but is not routable itself. It should have states that are also subclasses
+ of Ember.Route which each have a `route` property describing the URL pattern you would like to detect.
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ index: Ember.Route.extend({
+ route: '/'
+ }),
+ ... additional Ember.Routes ...
+ })
+ })
+ });
+ App.initialize();
+
+
+ When an application loads, Ember will parse the URL and attempt to find an Ember.Route within
+ the application's states that matches. (The example URL-matching below will use the default
+ 'hash syntax' provided by `Ember.HashLocation`.)
+
+ In the following route structure:
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/'
+ }),
+ bRoute: Ember.Route.extend({
+ route: '/alphabeta'
+ })
+ })
+ })
+ });
+ App.initialize();
+
+ Loading the page at the URL '#/' will detect the route property of 'root.aRoute' ('/') and
+ transition the router first to the state named 'root' and then to the substate 'aRoute'.
+
+ Respectively, loading the page at the URL '#/alphabeta' would detect the route property of
+ 'root.bRoute' ('/alphabeta') and transition the router first to the state named 'root' and
+ then to the substate 'bRoute'.
+
+ ## Adding Nested Routes to a Router
+ Routes can contain nested subroutes each with their own `route` property describing the nested
+ portion of the URL they would like to detect and handle. Router, like all instances of StateManager,
+ cannot call `transitonTo` with an intermediary state. To avoid transitioning the Router into an
+ intermediary state when detecting URLs, a Route with nested routes must define both a base `route`
+ property for itself and a child Route with a `route` property of `'/'` which will be transitioned
+ to when the base route is detected in the URL:
+
+ Given the following application code:
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/theBaseRouteForThisSet',
+
+ indexSubRoute: Ember.Route.extend({
+ route: '/'
+ }),
+
+ subRouteOne: Ember.Route.extend({
+ route: '/subroute1'
+ }),
+
+ subRouteTwo: Ember.Route.extend({
+ route: '/subRoute2'
+ })
+
+ })
+ })
+ })
+ });
+ App.initialize();
+
+ When the application is loaded at '/theBaseRouteForThisSet' the Router will transition to the route
+ at path 'root.aRoute' and then transition to state 'indexSubRoute'.
+
+ When the application is loaded at '/theBaseRouteForThisSet/subRoute1' the Router will transition to
+ the route at path 'root.aRoute' and then transition to state 'subRouteOne'.
+
+ ## Route Transition Events
+ Transitioning between Ember.Route instances (including the transition into the detected
+ route when loading the application) triggers the same transition events as state transitions for
+ base `Ember.State`s. However, the default `setup` transition event is named `connectOutlets` on
+ Ember.Router instances (see 'Changing View Hierarchy in Response To State Change').
+
+ The following route structure when loaded with the URL "#/"
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/',
+ enter: function(router) {
+ console.log("entering root.aRoute from", router.get('currentState.name'));
+ },
+ connectOutlets: function(router) {
+ console.log("entered root.aRoute, fully transitioned to", router.get('currentState.path'));
+ }
+ })
+ })
+ })
+ });
+ App.initialize();
+
+ Will result in console output of:
+
+ 'entering root.aRoute from root'
+ 'entered root.aRoute, fully transitioned to root.aRoute '
+
+ Ember.Route has two additional callbacks for handling URL serialization and deserialization. See
+ 'Serializing/Deserializing URLs'
+
+ ## Routes With Dynamic Segments
+ An Ember.Route's `route` property can reference dynamic sections of the URL by prefacing a URL segment
+ with the ':' character. The values of these dynamic segments will be passed as a hash to the
+ `deserialize` method of the matching Route (see 'Serializing/Deserializing URLs').
+
+ ## Serializing/Deserializing URLs
+ Ember.Route has two callbacks for associating a particular object context with a URL: `serialize`
+ for converting an object into a parameters hash to fill dynamic segments of a URL and `deserialize`
+ for converting a hash of dynamic segments from the URL into the appropriate object.
+
+ ### Deserializing A URL's Dynamic Segments
+ When an application is first loaded or the URL is changed manually (e.g. through the browser's
+ back button) the `deserialize` method of the URL's matching Ember.Route will be called with
+ the application's router as its first argument and a hash of the URL's dynamic segments and values
+ as its second argument.
+
+ The following route structure when loaded with the URL "#/fixed/thefirstvalue/anotherFixed/thesecondvalue":
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/fixed/:dynamicSectionA/anotherFixed/:dynamicSectionB',
+ deserialize: function(router, params) {}
+ })
+ })
+ })
+ });
+ App.initialize();
+
+ Will call the 'deserialize' method of the Route instance at the path 'root.aRoute' with the
+ following hash as its second argument:
+
+ {
+ dynamicSectionA: 'thefirstvalue',
+ dynamicSectionB: 'thesecondvalue'
+ }
+
+ Within `deserialize` you should use this information to retrieve or create an appropriate context
+ object for the given URL (e.g. by loading from a remote API or accessing the browser's
+ `localStorage`). This object must be the `return` value of `deserialize` and will be
+ passed to the Route's `connectOutlets` and `serialize` methods.
+
+ When an application's state is changed from within the application itself, the context provided for
+ the transition will be passed and `deserialize` is not called (see 'Transitions Between States').
+
+ ### Serializing An Object For URLs with Dynamic Segments
+ When transitioning into a Route whose `route` property contains dynamic segments the Route's
+ `serialize` method is called with the Route's router as the first argument and the Route's
+ context as the second argument. The return value of `serialize` will be used to populate the
+ dynamic segments and should be an object with keys that match the names of the dynamic sections.
+
+ Given the following route structure:
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/'
+ }),
+ bRoute: Ember.Route.extend({
+ route: '/staticSection/:someDynamicSegment',
+ serialize: function(router, context) {
+ return {
+ someDynamicSegment: context.get('name')
+ }
+ }
+ })
+ })
+ })
+ });
+ App.initialize();
+
+
+ Transitioning to "root.bRoute" with a context of `Object.create({name: 'Yehuda'})` will call
+ the Route's `serialize` method with the context as its second argument and update the URL to
+ '#/staticSection/Yehuda'.
+
+ ## Transitions Between States
+ Once a routed application has initialized its state based on the entry URL, subsequent transitions to other
+ states will update the URL if the entered Route has a `route` property. Given the following route structure
+ loaded at the URL '#/':
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/',
+ moveElsewhere: Ember.Route.transitionTo('bRoute')
+ }),
+ bRoute: Ember.Route.extend({
+ route: '/someOtherLocation'
+ })
+ })
+ })
+ });
+ App.initialize();
+
+ And application code:
+
+ App.get('router').send('moveElsewhere');
+
+ Will transition the application's state to 'root.bRoute' and trigger an update of the URL to
+ '#/someOtherLocation'.
+
+ For URL patterns with dynamic segments a context can be supplied as the second argument to `send`.
+ The router will match dynamic segments names to keys on this object and fill in the URL with the
+ supplied values. Given the following state structure loaded at the URL '#/':
+
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/',
+ moveElsewhere: Ember.Route.transitionTo('bRoute')
+ }),
+ bRoute: Ember.Route.extend({
+ route: '/a/route/:dynamicSection/:anotherDynamicSection',
+ connectOutlets: function(router, context) {},
+ })
+ })
+ })
+ });
+ App.initialize();
+
+ And application code:
+
+ App.get('router').send('moveElsewhere', {
+ dynamicSection: '42',
+ anotherDynamicSection: 'Life'
+ });
+
+ Will transition the application's state to 'root.bRoute' and trigger an update of the URL to
+ '#/a/route/42/Life'.
+
+ The context argument will also be passed as the second argument to the `serialize` method call.
+
+ ## Injection of Controller Singletons
+ During application initialization Ember will detect properties of the application ending in 'Controller',
+ create singleton instances of each class, and assign them as properties on the router. The property name
+ will be the UpperCamel name converted to lowerCamel format. These controller classes should be subclasses
+ of Ember.ObjectController, Ember.ArrayController, Ember.Controller, or a custom Ember.Object that includes the
+ Ember.ControllerMixin mixin.
+
+ ``` javascript
+ App = Ember.Application.create({
+ FooController: Ember.Object.create(Ember.ControllerMixin),
+ Router: Ember.Router.extend({ ... })
+ });
+
+ App.get('router.fooController'); // instance of App.FooController
+ ```
+
+ The controller singletons will have their `namespace` property set to the application and their `target`
+ property set to the application's router singleton for easy integration with Ember's user event system.
+ See 'Changing View Hierarchy in Response To State Change' and 'Responding to User-initiated Events.'
+
+ ## Responding to User-initiated Events
+ Controller instances injected into the router at application initialization have their `target` property
+ set to the application's router instance. These controllers will also be the default `context` for their
+ associated views. Uses of the `{{action}}` helper will automatically target the application's router.
+
+ Given the following application entered at the URL '#/':
+
+ ``` javascript
+ App = Ember.Application.create({
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/',
+ anActionOnTheRouter: function(router, context) {
+ router.transitionTo('anotherState', context);
+ }
+ })
+ anotherState: Ember.Route.extend({
+ route: '/differentUrl',
+ connectOutlets: function(router, context) {
+
+ }
+ })
+ })
+ })
+ });
+ App.initialize();
+ ```
+
+ The following template:
+
+ ``` handlebars
+ <script type="text/x-handlebars" data-template-name="aView">
+ <h1><a {{action anActionOnTheRouter}}>{{title}}</a></h1>
+ </script>
+ ```
+
+ Will delegate `click` events on the rendered `h1` to the application's router instance. In this case the
+ `anActionOnTheRouter` method of the state at 'root.aRoute' will be called with the view's controller
+ as the context argument. This context will be passed to the `connectOutlets` as its second argument.
+
+ Different `context` can be supplied from within the `{{action}}` helper, allowing specific context passing
+ between application states:
+
+ ``` handlebars
+ <script type="text/x-handlebars" data-template-name="photos">
+ {{#each photo in controller}}
+ <h1><a {{action showPhoto photo}}>{{title}}</a></h1>
+ {{/each}}
+ </script>
+ ```
+
+ See `Handlebars.helpers.action` for additional usage examples.
+
+
+ ## Changing View Hierarchy in Response To State Change
+
+ Changes in application state that change the URL should be accompanied by associated changes in view
+ hierarchy. This can be accomplished by calling 'connectOutlet' on the injected controller singletons from
+ within the 'connectOutlets' event of an Ember.Route:
+
+ ``` javascript
+ App = Ember.Application.create({
+ OneController: Ember.ObjectController.extend(),
+ OneView: Ember.View.extend(),
+
+ AnotherController: Ember.ObjectController.extend(),
+ AnotherView: Ember.View.extend(),
+
+ Router: Ember.Router.extend({
+ root: Ember.Route.extend({
+ aRoute: Ember.Route.extend({
+ route: '/',
+ connectOutlets: function(router, context) {
+ router.get('oneController').connectOutlet('another');
+ },
+ })
+ })
+ })
+ });
+ App.initialize();
+ ```
+
+
+ This will detect the '{{outlet}}' portion of `oneController`'s view (an instance of `App.OneView`) and
+ fill it with a rendered instance of `App.AnotherView` whose `context` will be the single instance of
+ `App.AnotherController` stored on the router in the `anotherController` property.
+
+ For more information about Outlets, see `Ember.Handlebars.helpers.outlet`. For additional information on
+ the `connectOutlet` method, see `Ember.Controller.connectOutlet`. For more information on
+ controller injections, see `Ember.Application#initialize()`. For additional information about view context,
+ see `Ember.View`.
+
+ @class Router
+ @namespace Ember
+ @extends Ember.StateManager
+*/
+Ember.Router = Ember.StateManager.extend(
+/** @scope Ember.Router.prototype */ {
+
+ /**
+ @property initialState
+ @type String
+ @default 'root'
+ */
+ initialState: 'root',
+
+ /**
+ The `Ember.Location` implementation to be used to manage the application
+ URL state. The following values are supported:
+
+ * 'hash': Uses URL fragment identifiers (like #/blog/1) for routing.
+ * 'history': Uses the browser's history.pushstate API for routing. Only works in
+ modern browsers with pushstate support.
+ * 'none': Does not read or set the browser URL, but still allows for
+ routing to happen. Useful for testing.
+
+ @property location
+ @type String
+ @default 'hash'
+ */
+ location: 'hash',
+
+ /**
+ This is only used when a history location is used so that applications that
+ don't live at the root of the domain can append paths to their root.
+
+ @property rootURL
+ @type String
+ @default '/'
+ */
+
+ rootURL: '/',
+
+ transitionTo: function() {
+ this.abortRoutingPromises();
+ this._super.apply(this, arguments);
+ },
+
+ route: function(path) {
+ this.abortRoutingPromises();
+
+ set(this, 'isRouting', true);
+
+ var routableState;
+
+ try {
+ path = path.replace(get(this, 'rootURL'), '');
+ path = path.replace(/^(?=[^\/])/, "/");
+
+ this.send('navigateAway');
+ this.send('unroutePath', path);
+
+ routableState = get(this, 'currentState');
+ while (routableState && !routableState.get('isRoutable')) {
+ routableState = get(routableState, 'parentState');
+ }
+ var currentURL = routableState ? routableState.absoluteRoute(this) : '';
+ var rest = path.substr(currentURL.length);
+
+ this.send('routePath', rest);
+ } finally {
+ set(this, 'isRouting', false);
+ }
+
+ routableState = get(this, 'currentState');
+ while (routableState && !routableState.get('isRoutable')) {
+ routableState = get(routableState, 'parentState');
+ }
+
+ if (routableState) {
+ routableState.updateRoute(this, get(this, 'location'));
+ }
+ },
+
+ urlFor: function(path, hashes) {
+ var currentState = get(this, 'currentState') || this,
+ state = this.findStateByPath(currentState, path);
+
+ Ember.assert(Ember.String.fmt("Could not find route with path '%@'", [path]), state);
+ Ember.assert(Ember.String.fmt("To get a URL for the state '%@', it must have a `route` property.", [path]), get(state, 'routeMatcher'));
+
+ var location = get(this, 'location'),
+ absoluteRoute = state.absoluteRoute(this, hashes);
+
+ return location.formatURL(absoluteRoute);
+ },
+
+ urlForEvent: function(eventName) {
+ var contexts = Array.prototype.slice.call(arguments, 1),
+ currentState = get(this, 'currentState'),
+ targetStateName = currentState.lookupEventTransition(eventName),
+ targetState,
+ hashes;
+
+ Ember.assert(Ember.String.fmt("You must specify a target state for event '%@' in order to link to it in the current state '%@'.", [eventName, get(currentState, 'path')]), targetStateName);
+
+ targetState = this.findStateByPath(currentState, targetStateName);
+
+ Ember.assert("Your target state name " + targetStateName + " for event " + eventName + " did not resolve to a state", targetState);
+
+
+ hashes = this.serializeRecursively(targetState, contexts, []);
+
+ return this.urlFor(targetStateName, hashes);
+ },
+
+ serializeRecursively: function(state, contexts, hashes) {
+ var parentState,
+ context = get(state, 'hasContext') ? contexts.pop() : null,
+ hash = context ? state.serialize(this, context) : null;
+
+ hashes.push(hash);
+ parentState = state.get("parentState");
+
+ if (parentState && parentState instanceof Ember.Route) {
+ return this.serializeRecursively(parentState, contexts, hashes);
+ } else {
+ return hashes;
+ }
+ },
+
+ abortRoutingPromises: function() {
+ if (this._routingPromises) {
+ this._routingPromises.abort();
+ this._routingPromises = null;
+ }
+ },
+
+ handleStatePromises: function(states, complete) {
+ this.abortRoutingPromises();
+
+ this.set('isLocked', true);
+
+ var manager = this;
+
+ this._routingPromises = Ember._PromiseChain.create({
+ promises: states.slice(),
+
+ successCallback: function() {
+ manager.set('isLocked', false);
+ complete();
+ },
+
+ failureCallback: function() {
+ throw "Unable to load object";
+ },
+
+ promiseSuccessCallback: function(item, args) {
+ set(item, 'object', args[0]);
+ },
+
+ abortCallback: function() {
+ manager.set('isLocked', false);
+ }
+ }).start();
+ },
+
+ moveStatesIntoRoot: function() {
+ this.root = Ember.Route.extend();
+
+ for (var name in this) {
+ if (name === "constructor") { continue; }
+
+ var state = this[name];
+
+ if (state instanceof Ember.Route || Ember.Route.detect(state)) {
+ this.root[name] = state;
+ delete this[name];
+ }
+ }
+ },
+
+ init: function() {
+ if (!this.root) {
+ this.moveStatesIntoRoot();
+ }
+
+ this._super();
+
+ var location = get(this, 'location'),
+ rootURL = get(this, 'rootURL');
+
+ if ('string' === typeof location) {
+ set(this, 'location', Ember.Location.create({
+ implementation: location,
+ rootURL: rootURL
+ }));
+ }
+
+ this.assignRouter(this, this);
+ },
+
+ assignRouter: function(state, router) {
+ state.router = router;
+
+ var childStates = state.states;
+
+ if (childStates) {
+ for (var stateName in childStates) {
+ if (!childStates.hasOwnProperty(stateName)) { continue; }
+ this.assignRouter(childStates[stateName], router);
+ }
+ }
+ },
+
+ willDestroy: function() {
+ get(this, 'location').destroy();
+ }
+});
View
32 packages/ember-old-router/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "ember-routing",
+ "summary": "Ember Routing",
+ "description": "The Ember routing system",
+ "homepage": "http://emberjs.com",
+ "author": "Yehuda Katz",
+ "version": "1.0.0-pre.2",
+
+ "directories": {
+ "lib": "lib"
+ },
+
+ "dependencies": {
+ "spade": "~> 1.0",
+ "ember-views": "1.0.0-pre.2",
+ "ember-states": "1.0.0-pre.2"
+ },
+
+ "dependencies:development": {
+ "spade-qunit": "~> 1.0.0"
+ },
+
+ "bpm:build": {
+
+ "bpm_libs.js": {
+ "files": ["lib"],
+ "modes": "*"
+ }
+ }
+}
+
+
View
0  packages/ember-routing/tests/location_test.js → packages/ember-old-router/tests/location_test.js
File renamed without changes
View
0  packages/ember-routing/tests/render_test.js → packages/ember-old-router/tests/render_test.js
File renamed without changes
View
0  packages/ember-routing/tests/routable_test.js → packages/ember-old-router/tests/routable_test.js
File renamed without changes
View
0  packages/ember-routing/tests/router_test.js → packages/ember-old-router/tests/router_test.js
File renamed without changes
View
11 packages/ember-routing/lib/main.js
@@ -1,11 +0,0 @@
-require('ember-states');
-require('ember-routing/route');
-require('ember-routing/router');
-
-/**
-Ember Routing
-
-@module ember
-@submodule ember-routing
-@requires ember-states
-*/
View
475 packages/ember-routing/lib/route-recognizer.js
@@ -0,0 +1,475 @@
+define("route-recognizer",
+ [],
+ function() {
+ "use strict";
+ var specials = [
+ '/', '.', '*', '+', '?', '|',
+ '(', ')', '[', ']', '{', '}', '\\'
+ ];
+
+ var escapeRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
+
+ // A Segment represents a segment in the original route description.
+ // Each Segment type provides an `eachChar` and `regex` method.
+ //
+ // The `eachChar` method invokes the callback with one or more character
+ // specifications. A character specification consumes one or more input
+ // characters.
+ //
+ // The `regex` method returns a regex fragment for the segment. If the
+ // segment is a dynamic of star segment, the regex fragment also includes
+ // a capture.
+ //
+ // A character specification contains:
+ //
+ // * `validChars`: a String with a list of all valid characters, or
+ // * `invalidChars`: a String with a list of all invalid characters
+ // * `repeat`: true if the character specification can repeat
+
+ function StaticSegment(string) { this.string = string; }
+ StaticSegment.prototype = {
+ eachChar: function(callback) {
+ var string = this.string, char;
+
+ for (var i=0, l=string.length; i<l; i++) {
+ char = string.charAt(i);
+ callback({ validChars: char });
+ }
+ },
+
+ regex: function() {
+ return this.string.replace(escapeRegex, '\\$1');
+ },
+
+ generate: function() {
+ return this.string;
+ }
+ };
+
+ function DynamicSegment(name) { this.name = name; }
+ DynamicSegment.prototype = {
+ eachChar: function(callback) {
+ callback({ invalidChars: "/", repeat: true });
+ },
+
+ regex: function() {
+ return "([^/]+)";
+ },
+
+ generate: function(params) {
+ return params[this.name];
+ }
+ };
+
+ function StarSegment(name) { this.name = name; }
+ StarSegment.prototype = {
+ eachChar: function(callback) {
+ callback({ invalidChars: "", repeat: true });
+ },
+
+ regex: function() {
+ return "(.+)";
+ },
+
+ generate: function(params) {
+ return params[this.name];
+ }
+ };
+
+ function EpsilonSegment() {}
+ EpsilonSegment.prototype = {
+ eachChar: function() {},
+ regex: function() { return ""; },
+ generate: function() { return ""; }
+ };
+
+ function parse(route, names, types) {
+ // normalize route as not starting with a "/". Recognition will
+ // also normalize.
+ if (route.charAt(0) === "/") { route = route.substr(1); }
+
+ var segments = route.split("/"), results = [];
+
+ for (var i=0, l=segments.length; i<l; i++) {
+ var segment = segments[i], match;
+
+ if (match = segment.match(/^:([^\/]+)$/)) {
+ results.push(new DynamicSegment(match[1]));
+ names.push(match[1]);
+ types.dynamics++;
+ } else if (match = segment.match(/^\*([^\/]+)$/)) {
+ results.push(new StarSegment(match[1]));
+ names.push(match[1]);
+ types.stars++;
+ } else if(segment === "") {
+ results.push(new EpsilonSegment());
+ } else {
+ results.push(new StaticSegment(segment));
+ types.statics++;
+ }
+ }
+
+ return results;
+ }
+
+ // A State has a character specification and (`charSpec`) and a list of possible
+ // subsequent states (`nextStates`).
+ //
+ // If a State is an accepting state, it will also have several additional
+ // properties:
+ //
+ // * `regex`: A regular expression that is used to extract parameters from paths
+ // that reached this accepting state.
+ // * `handlers`: Information on how to convert the list of captures into calls
+ // to registered handlers with the specified parameters
+ // * `types`: How many static, dynamic or star segments in this route. Used to
+ // decide which route to use if multiple registered routes match a path.
+ //
+ // Currently, State is implemented naively by looping over `nextStates` and
+ // comparing a character specification against a character. A more efficient
+ // implementation would use a hash of keys pointing at one or more next states.
+
+ function State(charSpec) {
+ this.charSpec = charSpec;
+ this.nextStates = [];
+ }
+
+ State.prototype = {
+ get: function(charSpec) {
+ var nextStates = this.nextStates;
+
+ for (var i=0, l=nextStates.length; i<l; i++) {
+ var child = nextStates[i];
+
+ var isEqual = child.charSpec.validChars === charSpec.validChars;
+ isEqual = isEqual && child.charSpec.invalidChars === charSpec.invalidChars;
+
+ if (isEqual) { return child; }
+ }
+ },
+
+ put: function(charSpec) {
+ var state;
+
+ // If the character specification already exists in a child of the current
+ // state, just return that state.
+ if (state = this.get(charSpec)) { return state; }
+
+ // Make a new state for the character spec
+ state = new State(charSpec);
+
+ // Insert the new state as a child of the current state
+ this.nextStates.push(state);
+
+ // If this character specification repeats, insert the new state as a child
+ // of itself. Note that this will not trigger an infinite loop because each
+ // transition during recognition consumes a character.
+ if (charSpec.repeat) {
+ state.nextStates.push(state);
+ }
+
+ // Return the new state
+ return state;
+ },
+
+ // Find a list of child states matching the next character
+ match: function(char) {
+ // DEBUG "Processing `" + char + "`:"
+ var nextStates = this.nextStates,
+ child, charSpec, chars;
+
+ // DEBUG " " + debugState(this)
+ var returned = [];
+
+ for (var i=0, l=nextStates.length; i<l; i++) {
+ child = nextStates[i];
+
+ charSpec = child.charSpec;
+
+ if (chars = charSpec.validChars) {
+ if (chars.indexOf(char) !== -1) { returned.push(child); }
+ } else if (chars = charSpec.invalidChars) {
+ if (chars.indexOf(char) === -1) { returned.push(child); }
+ }
+ }
+
+ return returned;
+ }
+
+ /** IF DEBUG
+ , debug: function() {
+ var charSpec = this.charSpec,
+ debug = "[",
+ chars = charSpec.validChars || charSpec.invalidChars;
+
+ if (charSpec.invalidChars) { debug += "^"; }
+ debug += chars;
+ debug += "]";
+
+ if (charSpec.repeat) { debug += "+"; }
+
+ return debug;
+ }
+ END IF **/
+ };
+
+ /** IF DEBUG
+ function debug(log) {
+ console.log(log);
+ }
+
+ function debugState(state) {
+ return state.nextStates.map(function(n) {
+ if (n.nextStates.length === 0) { return "( " + n.debug() + " [accepting] )"; }
+ return "( " + n.debug() + " <then> " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )";
+ }).join(", ")
+ }
+ END IF **/
+
+ // This is a somewhat naive strategy, but should work in a lot of cases
+ // A better strategy would properly resolve /posts/:id/new and /posts/edit/:id
+ function sortSolutions(states) {
+ return states.sort(function(a, b) {
+ if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; }
+ if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; }
+ if (a.types.statics !== b.types.statics) { return a.types.statics - b.types.statics; }
+
+ return 0;
+ });
+ }
+
+ function recognizeChar(states, char) {
+ var nextStates = [];
+
+ for (var i=0, l=states.length; i<l; i++) {
+ var state = states[i];
+
+ nextStates = nextStates.concat(state.match(char));
+ }
+
+ return nextStates;
+ }
+
+ function handler(state, path) {
+ var handlers = state.handlers, regex = state.regex;
+ var captures = path.match(regex), currentCapture = 1;
+ var result = [];
+
+ for (var i=0, l=handlers.length; i<l; i++) {
+ var handler = handlers[i], names = handler.names, params = {};
+
+ for (var j=0, m=names.length; j<m; j++) {
+ params[names[j]] = captures[currentCapture++];
+ }
+
+ result.push({ handler: handler.handler, params: params });
+ }
+
+ return result;
+ }
+
+ function addSegment(currentState, segment) {
+ segment.eachChar(function(char) {
+ var state;
+
+ currentState = currentState.put(char);
+ });
+
+ return currentState;
+ }
+
+ // The main interface
+
+ var RouteRecognizer = function() {
+ this.rootState = new State();
+ this.names = {};
+ };
+
+
+ RouteRecognizer.prototype = {
+ add: function(routes, options) {
+ var currentState = this.rootState, regex = "^",
+ types = { statics: 0, dynamics: 0, stars: 0 },
+ handlers = [], allSegments = [], name;
+
+ for (var i=0, l=routes.length; i<l; i++) {
+ var route = routes[i], names = [];
+
+ var segments = parse(route.path, names, types);
+
+ allSegments = allSegments.concat(segments);
+
+ for (var j=0, m=segments.length; j<m; j++) {
+ var segment = segments[j];
+
+ if (segment instanceof EpsilonSegment) { continue; }
+
+ // Add a "/" for the new segment
+ currentState = currentState.put({ validChars: "/" });
+ regex += "/";
+
+ // Add a representation of the segment to the NFA and regex
+ currentState = addSegment(currentState, segment);
+ regex += segment.regex();
+ }
+
+ handlers.push({ handler: route.handler, names: names });
+ }
+
+ currentState.handlers = handlers;
+ currentState.regex = new RegExp(regex + "$");
+ currentState.types = types;
+
+ if (name = options && options.as) {
+ this.names[name] = {
+ segments: allSegments,
+ handlers: handlers
+ }
+ }
+ },
+
+ handlersFor: function(name) {
+ var route = this.names[name], result = [];
+ if (!route) { throw new Error("There is no route named " + name); }
+
+ for (var i=0, l=route.handlers.length; i<l; i++) {
+ result.push(route.handlers[i]);
+ }
+
+ return result;
+ },
+
+ generate: function(name, params) {
+ var route = this.names[name], output = "";
+ if (!route) { throw new Error("There is no route named " + name); }
+
+ var segments = route.segments;
+
+ for (var i=0, l=segments.length; i<l; i++) {
+ var segment = segments[i];
+
+ if (segment instanceof EpsilonSegment) { continue; }
+
+ output += "/";
+ output += segment.generate(params);
+ }
+
+ return output;
+ },
+
+ recognize: function(path) {
+ var states = [ this.rootState ];
+
+ // DEBUG GROUP path
+
+ if (path.charAt(0) !== "/") { path = "/" + path; }
+
+ for (var i=0, l=path.length; i<l; i++) {
+ states = recognizeChar(states, path.charAt(i));
+ if (!states.length) { break; }
+ }
+
+ // END DEBUG GROUP
+
+ var solutions = [];
+ for (var i=0, l=states.length; i<l; i++) {
+ if (states[i].handlers) { solutions.push(states[i]); }
+ }
+
+ states = sortSolutions(solutions);
+
+ var state = solutions[0];
+
+ if (state && state.handlers) {
+ return handler(state, path);
+ }
+ }
+ };
+
+ function Target(path, matcher) {
+ this.path = path;
+ this.matcher = matcher;
+ }
+
+ Target.prototype = {
+ to: function(target, callback) {
+ this.matcher.add(this.path, target);
+
+ if (callback) {
+ this.matcher.addChild(this.path, callback)
+ }
+ }
+ }
+
+ function Matcher() {
+ this.routes = {};
+ this.children = {};
+ }
+
+ Matcher.prototype = {
+ add: function(path, handler) {
+ this.routes[path] = handler;
+ },
+
+ addChild: function(path, callback) {
+ var matcher = new Matcher();
+ this.children[path] = matcher;
+ callback(generateMatch(path, matcher));
+ }
+ }
+
+ function generateMatch(startingPath, matcher) {
+ return function(path, nestedCallback) {
+ var fullPath = startingPath + path;
+
+ if (nestedCallback) {
+ nestedCallback(generateMatch(fullPath, matcher));
+ } else {
+ return new Target(startingPath + path, matcher);
+ }
+ }
+ }
+
+ function addRoute(routeArray, path, handler) {
+ var len = 0;
+ for (var i=0, l=routeArray.length; i<l; i++) {
+ len += routeArray[i].path.length;
+ }
+
+ path = path.substr(len);
+ routeArray.push({ path: path, handler: handler });
+ }
+
+ function eachRoute(baseRoute, matcher, callback, binding) {
+ var routes = matcher.routes;
+
+ for (var path in routes) {
+ if (routes.hasOwnProperty(path)) {
+ var routeArray = baseRoute.slice();
+ addRoute(routeArray, path, routes[path]);
+
+ if (matcher.children[path]) {
+ eachRoute(routeArray, matcher.children[path], callback, binding);
+ } else {
+ callback.call(binding, routeArray);
+ }
+ }
+ }
+ }
+
+ RouteRecognizer.prototype.map = function(callback, addRouteCallback) {
+ var matcher = new Matcher();
+
+ function match(path, nestedCallback) {
+ return new Target(path, matcher);
+ }
+
+ callback(generateMatch("", matcher));
+
+ eachRoute([], matcher, function(route) {
+ if (addRouteCallback) { addRouteCallback(this, route); }
+ else { this.add(route); }
+ }, this);
+ };
+ return RouteRecognizer;
+ });
View
939 packages/ember-routing/lib/router.js
@@ -1,636 +1,427 @@
-require('ember-routing/route_matcher');
-require('ember-routing/routable');
-require('ember-routing/location');
-
-/**
-@module ember
-@submodule ember-routing
-*/
-
-var get = Ember.get, set = Ember.set;
-
-var merge = function(original, hash) {
- for (var prop in hash) {
- if (!hash.hasOwnProperty(prop)) { continue; }
- if (original.hasOwnProperty(prop)) { continue; }
-
- original[prop] = hash[prop];
- }
-};
-
-/**
- `Ember.Router` is the subclass of `Ember.StateManager` responsible for providing URL-based
- application state detection. The `Ember.Router` instance of an application detects the browser URL
- at application load time and attempts to match it to a specific application state. Additionally
- the router will update the URL to reflect an application's state changes over time.
-
- ## Adding a Router Instance to Your Application
- An instance of Ember.Router can be associated with an instance of Ember.Application in one of two ways:
-
- You can provide a subclass of Ember.Router as the `Router` property of your application. An instance
- of this Router class will be instantiated and route detection will be enabled when the application's
- `initialize` method is called. The Router instance will be available as the `router` property
- of the application:
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({ ... })
- });
+define("router",
+ ["route_recognizer"],
+ function(RouteRecognizer) {
+ "use strict";
+ /**
+ @private
- App.initialize();
- App.get('router') // an instance of App.Router
-
- If you want to define a Router instance elsewhere, you can pass the instance to the application's
- `initialize` method:
-
- App = Ember.Application.create();
- aRouter = Ember.Router.create({ ... });
-
- App.initialize(aRouter);
- App.get('router') // aRouter
-
- ## Adding Routes to a Router
- The `initialState` property of Ember.Router instances is named `root`. The state stored in this
- property must be a subclass of Ember.Route. The `root` route acts as the container for the
- set of routable states but is not routable itself. It should have states that are also subclasses
- of Ember.Route which each have a `route` property describing the URL pattern you would like to detect.
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- index: Ember.Route.extend({
- route: '/'
- }),
- ... additional Ember.Routes ...
- })
- })
- });
- App.initialize();
-
-
- When an application loads, Ember will parse the URL and attempt to find an Ember.Route within
- the application's states that matches. (The example URL-matching below will use the default
- 'hash syntax' provided by `Ember.HashLocation`.)
-
- In the following route structure:
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/'
- }),
- bRoute: Ember.Route.extend({
- route: '/alphabeta'
- })
- })
- })
- });
- App.initialize();
-
- Loading the page at the URL '#/' will detect the route property of 'root.aRoute' ('/') and
- transition the router first to the state named 'root' and then to the substate 'aRoute'.
-
- Respectively, loading the page at the URL '#/alphabeta' would detect the route property of
- 'root.bRoute' ('/alphabeta') and transition the router first to the state named 'root' and
- then to the substate 'bRoute'.
-
- ## Adding Nested Routes to a Router
- Routes can contain nested subroutes each with their own `route` property describing the nested
- portion of the URL they would like to detect and handle. Router, like all instances of StateManager,
- cannot call `transitonTo` with an intermediary state. To avoid transitioning the Router into an
- intermediary state when detecting URLs, a Route with nested routes must define both a base `route`
- property for itself and a child Route with a `route` property of `'/'` which will be transitioned
- to when the base route is detected in the URL:
-
- Given the following application code:
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/theBaseRouteForThisSet',
-
- indexSubRoute: Ember.Route.extend({
- route: '/'
- }),
-
- subRouteOne: Ember.Route.extend({
- route: '/subroute1'
- }),
-
- subRouteTwo: Ember.Route.extend({
- route: '/subRoute2'
- })
-
- })
- })
- })
- });
- App.initialize();
-
- When the application is loaded at '/theBaseRouteForThisSet' the Router will transition to the route
- at path 'root.aRoute' and then transition to state 'indexSubRoute'.
-
- When the application is loaded at '/theBaseRouteForThisSet/subRoute1' the Router will transition to
- the route at path 'root.aRoute' and then transition to state 'subRouteOne'.
-
- ## Route Transition Events
- Transitioning between Ember.Route instances (including the transition into the detected
- route when loading the application) triggers the same transition events as state transitions for
- base `Ember.State`s. However, the default `setup` transition event is named `connectOutlets` on
- Ember.Router instances (see 'Changing View Hierarchy in Response To State Change').
-
- The following route structure when loaded with the URL "#/"
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/',
- enter: function(router) {
- console.log("entering root.aRoute from", router.get('currentState.name'));
- },
- connectOutlets: function(router) {
- console.log("entered root.aRoute, fully transitioned to", router.get('currentState.path'));
- }
- })
- })
- })
- });
- App.initialize();
-
- Will result in console output of:
-
- 'entering root.aRoute from root'
- 'entered root.aRoute, fully transitioned to root.aRoute '
-
- Ember.Route has two additional callbacks for handling URL serialization and deserialization. See
- 'Serializing/Deserializing URLs'
-
- ## Routes With Dynamic Segments
- An Ember.Route's `route` property can reference dynamic sections of the URL by prefacing a URL segment
- with the ':' character. The values of these dynamic segments will be passed as a hash to the
- `deserialize` method of the matching Route (see 'Serializing/Deserializing URLs').
-
- ## Serializing/Deserializing URLs
- Ember.Route has two callbacks for associating a particular object context with a URL: `serialize`
- for converting an object into a parameters hash to fill dynamic segments of a URL and `deserialize`
- for converting a hash of dynamic segments from the URL into the appropriate object.
-
- ### Deserializing A URL's Dynamic Segments
- When an application is first loaded or the URL is changed manually (e.g. through the browser's
- back button) the `deserialize` method of the URL's matching Ember.Route will be called with
- the application's router as its first argument and a hash of the URL's dynamic segments and values
- as its second argument.
-
- The following route structure when loaded with the URL "#/fixed/thefirstvalue/anotherFixed/thesecondvalue":
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/fixed/:dynamicSectionA/anotherFixed/:dynamicSectionB',
- deserialize: function(router, params) {}
- })
- })
- })
- });
- App.initialize();
+ This file references several internal structures:
- Will call the 'deserialize' method of the Route instance at the path 'root.aRoute' with the
- following hash as its second argument:
+ ## `RecognizedHandler`
- {
- dynamicSectionA: 'thefirstvalue',
- dynamicSectionB: 'thesecondvalue'
- }
+ * `{String} handler`: A handler name
+ * `{Object} params`: A hash of recognized parameters
- Within `deserialize` you should use this information to retrieve or create an appropriate context
- object for the given URL (e.g. by loading from a remote API or accessing the browser's
- `localStorage`). This object must be the `return` value of `deserialize` and will be
- passed to the Route's `connectOutlets` and `serialize` methods.
-
- When an application's state is changed from within the application itself, the context provided for
- the transition will be passed and `deserialize` is not called (see 'Transitions Between States').
-
- ### Serializing An Object For URLs with Dynamic Segments
- When transitioning into a Route whose `route` property contains dynamic segments the Route's
- `serialize` method is called with the Route's router as the first argument and the Route's
- context as the second argument. The return value of `serialize` will be used to populate the
- dynamic segments and should be an object with keys that match the names of the dynamic sections.
-
- Given the following route structure:
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/'
- }),
- bRoute: Ember.Route.extend({
- route: '/staticSection/:someDynamicSegment',
- serialize: function(router, context) {
- return {
- someDynamicSegment: context.get('name')
- }
- }
- })
- })
- })
- });
- App.initialize();
-
-
- Transitioning to "root.bRoute" with a context of `Object.create({name: 'Yehuda'})` will call
- the Route's `serialize` method with the context as its second argument and update the URL to
- '#/staticSection/Yehuda'.
-
- ## Transitions Between States
- Once a routed application has initialized its state based on the entry URL, subsequent transitions to other
- states will update the URL if the entered Route has a `route` property. Given the following route structure
- loaded at the URL '#/':
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/',
- moveElsewhere: Ember.Route.transitionTo('bRoute')
- }),
- bRoute: Ember.Route.extend({
- route: '/someOtherLocation'
- })
- })
- })
- });
- App.initialize();
-
- And application code:
-
- App.get('router').send('moveElsewhere');
-
- Will transition the application's state to 'root.bRoute' and trigger an update of the URL to
- '#/someOtherLocation'.
-
- For URL patterns with dynamic segments a context can be supplied as the second argument to `send`.
- The router will match dynamic segments names to keys on this object and fill in the URL with the
- supplied values. Given the following state structure loaded at the URL '#/':
-
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/',
- moveElsewhere: Ember.Route.transitionTo('bRoute')
- }),
- bRoute: Ember.Route.extend({
- route: '/a/route/:dynamicSection/:anotherDynamicSection',
- connectOutlets: function(router, context) {},
- })
- })
- })
- });
- App.initialize();
+ ## `UnresolvedHandlerInfo`
- And application code:
+ * `{String} name`: the name of a handler
+ * `{Object} context`: the active context for the handler
- App.get('router').send('moveElsewhere', {
- dynamicSection: '42',
- anotherDynamicSection: 'Life'
- });
+ ## `HandlerInfo`
- Will transition the application's state to 'root.bRoute' and trigger an update of the URL to
- '#/a/route/42/Life'.
+ * `{Object} handler`: a handler object
+ * `{Object} context`: the active context for the handler
+ */
- The context argument will also be passed as the second argument to the `serialize` method call.
- ## Injection of Controller Singletons
- During application initialization Ember will detect properties of the application ending in 'Controller',
- create singleton instances of each class, and assign them as properties on the router. The property name
- will be the UpperCamel name converted to lowerCamel format. These controller classes should be subclasses
- of Ember.ObjectController, Ember.ArrayController, Ember.Controller, or a custom Ember.Object that includes the
- Ember.ControllerMixin mixin.
+ function Router() {
+ this.recognizer = new RouteRecognizer();
+ }
- ``` javascript
- App = Ember.Application.create({
- FooController: Ember.Object.create(Ember.ControllerMixin),
- Router: Ember.Router.extend({ ... })
- });
- App.get('router.fooController'); // instance of App.FooController
- ```
+ Router.prototype = {
+ /**
+ The main entry point into the router. The API is essentially
+ the same as the `map` method in `route-recognizer`.
- The controller singletons will have their `namespace` property set to the application and their `target`
- property set to the application's router singleton for easy integration with Ember's user event system.
- See 'Changing View Hierarchy in Response To State Change' and 'Responding to User-initiated Events.'
+ This method extracts the String handler at the last `.to()`
+ call and uses it as the name of the whole route.
- ## Responding to User-initiated Events
- Controller instances injected into the router at application initialization have their `target` property
- set to the application's router instance. These controllers will also be the default `context` for their
- associated views. Uses of the `{{action}}` helper will automatically target the application's router.
+ @param {Function} callback
+ */
+ map: function(callback) {
+ this.recognizer.map(callback, function(recognizer, route) {
+ var lastHandler = route[route.length - 1].handler;
+ var args = [route, { as: lastHandler }];
+ recognizer.add.apply(recognizer, args);
+ });
+ },
- Given the following application entered at the URL '#/':
+ /**
+ Take a named route and a list of params and generate a
+ URL. Used by `transitionTo` to set the new URL when a
+ route is directly transitioned to from inside the app.
- ``` javascript
- App = Ember.Application.create({
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/',
- anActionOnTheRouter: function(router, context) {
- router.transitionTo('anotherState', context);
- }
- })
- anotherState: Ember.Route.extend({
- route: '/differentUrl',
- connectOutlets: function(router, context) {
+ @param {String} name the name of the route to generate
+ a URL for
+ @param {Object} params a hash of parameters
- }
- })
- })
- })
- });
- App.initialize();
- ```
-
- The following template:
-
- ``` handlebars
- <script type="text/x-handlebars" data-template-name="aView">
- <h1><a {{action anActionOnTheRouter}}>{{title}}</a></h1>
- </script>
- ```
-
- Will delegate `click` events on the rendered `h1` to the application's router instance. In this case the
- `anActionOnTheRouter` method of the state at 'root.aRoute' will be called with the view's controller
- as the context argument. This context will be passed to the `connectOutlets` as its second argument.
-
- Different `context` can be supplied from within the `{{action}}` helper, allowing specific context passing
- between application states:
-
- ``` handlebars
- <script type="text/x-handlebars" data-template-name="photos">
- {{#each photo in controller}}
- <h1><a {{action showPhoto photo}}>{{title}}</a></h1>
- {{/each}}
- </script>
- ```
-
- See `Handlebars.helpers.action` for additional usage examples.
-
-
- ## Changing View Hierarchy in Response To State Change
-
- Changes in application state that change the URL should be accompanied by associated changes in view
- hierarchy. This can be accomplished by calling 'connectOutlet' on the injected controller singletons from
- within the 'connectOutlets' event of an Ember.Route:
-
- ``` javascript
- App = Ember.Application.create({
- OneController: Ember.ObjectController.extend(),
- OneView: Ember.View.extend(),
-
- AnotherController: Ember.ObjectController.extend(),
- AnotherView: Ember.View.extend(),
-
- Router: Ember.Router.extend({
- root: Ember.Route.extend({
- aRoute: Ember.Route.extend({
- route: '/',
- connectOutlets: function(router, context) {
- router.get('oneController').connectOutlet('another');
- },
- })
- })
- })
- });
- App.initialize();
- ```
-
-
- This will detect the '{{outlet}}' portion of `oneController`'s view (an instance of `App.OneView`) and
- fill it with a rendered instance of `App.AnotherView` whose `context` will be the single instance of
- `App.AnotherController` stored on the router in the `anotherController` property.
-
- For more information about Outlets, see `Ember.Handlebars.helpers.outlet`. For additional information on
- the `connectOutlet` method, see `Ember.Controller.connectOutlet`. For more information on
- controller injections, see `Ember.Application#initialize()`. For additional information about view context,
- see `Ember.View`.
-
- @class Router
- @namespace Ember
- @extends Ember.StateManager
-*/
-Ember.Router = Ember.StateManager.extend(
-/** @scope Ember.Router.prototype */ {
-
- /**
- @property initialState
- @type String
- @default 'root'
- */
- initialState: 'root',
-
- /**
- The `Ember.Location` implementation to be used to manage the application
- URL state. The following values are supported:
-
- * 'hash': Uses URL fragment identifiers (like #/blog/1) for routing.
- * 'history': Uses the browser's history.pushstate API for routing. Only works in
- modern browsers with pushstate support.
- * 'none': Does not read or set the browser URL, but still allows for
- routing to happen. Useful for testing.
-
- @property location
- @type String
- @default 'hash'
- */
- location: 'hash',
-
- /**
- This is only used when a history location is used so that applications that
- don't live at the root of the domain can append paths to their root.
-
- @property rootURL
- @type String
- @default '/'
- */
-
- rootURL: '/',
-
- transitionTo: function() {
- this.abortRoutingPromises();
- this._super.apply(this, arguments);
- },
-
- route: function(path) {
- this.abortRoutingPromises();
-
- set(this, 'isRouting', true);
-
- var routableState;
-
- try {
- path = path.replace(get(this, 'rootURL'), '');
- path = path.replace(/^(?=[^\/])/, "/");
-
- this.send('navigateAway');
- this.send('unroutePath', path);
-
- routableState = get(this, 'currentState');
- while (routableState && !routableState.get('isRoutable')) {
- routableState = get(routableState, 'parentState');
- }
- var currentURL = routableState ? routableState.absoluteRoute(this) : '';
- var rest = path.substr(currentURL.length);
+ @returns {String} a URL
+ */
+ generate: function(name, params) {
+ return this.recognizer.generate(name, params);
+ },
- this.send('routePath', rest);
- } finally {
- set(this, 'isRouting', false);
- }
+ /**
+ The entry point for handling a change to the URL (usually
+ via the back and forward button).
- routableState = get(this, 'currentState');
- while (routableState && !routableState.get('isRoutable')) {
- routableState = get(routableState, 'parentState');
- }
+ Returns an Array of handlers and the parameters associated
+ with those parameters.
- if (routableState) {
- routableState.updateRoute(this, get(this, 'location'));
- }
- },
+ @param {String} url a URL to process
- urlFor: function(path, hashes) {
- var currentState = get(this, 'currentState') || this,
- state = this.findStateByPath(currentState, path);
+ @returns {Array} an Array of `[handler, parameter]` tuples
+ */
+ handleURL: function(url) {
+ var results = this.recognizer.recognize(url),
+ objects = [];
- Ember.assert(Ember.String.fmt("Could not find route with path '%@'", [path]), state);
- Ember.assert(Ember.String.fmt("To get a URL for the state '%@', it must have a `route` property.", [path]), get(state, 'routeMatcher'));
+ collectObjects(this, results, 0, []);
+ },
- var location = get(this, 'location'),
- absoluteRoute = state.absoluteRoute(this, hashes);
+ /**
+ Transition into the specified named route.
+
+ If necessary, trigger the exit callback on any handlers
+ that are no longer represented by the target route.
+
+ @param {String} name the name of the route
+ */
+ transitionTo: function(name) {
+ var handlers = this.recognizer.handlersFor(name),
+ objects = [].slice.call(arguments, 1),
+ params = {},
+ setupHandlers = false,
+ toSetup = [];
+
+ for (var i=0, l=handlers.length; i<l; i++) {
+ var handlerObj = handlers[i],
+ handler = this.getHandler(handlerObj.handler),
+ names = handlerObj.names,
+ object, params;
+
+ if (names.length) {
+ object = objects.shift();
+ if (handler.serialize) { merge(params, handler.serialize(object)); }
+ } else {
+ object = handler.deserialize && handler.deserialize({});
+ }
- return location.formatURL(absoluteRoute);
- },
+ toSetup.push({ handler: handlerObj.handler, context: object });
+ }
- urlForEvent: function(eventName) {
- var contexts = Array.prototype.slice.call(arguments, 1),
- currentState = get(this, 'currentState'),
- targetStateName = currentState.lookupEventTransition(eventName),
- targetState,
- hashes;
+ setupContexts(router, toSetup);
+ var url = this.recognizer.generate(name, params);
+ this.updateURL(url);
+ },
- Ember.assert(Ember.String.fmt("You must specify a target state for event '%@' in order to link to it in the current state '%@'.", [eventName, get(currentState, 'path')]), targetStateName);
+ trigger: function(name) {
+ trigger(router, name);
+ }
+ }
- targetState = this.findStateByPath(currentState, targetStateName);
+ function merge(hash, other) {
+ for (var prop in other) {
+ if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; }
+ }
+ }
- Ember.assert("Your target state name " + targetStateName + " for event " + eventName + " did not resolve to a state", targetState);
+ /**
+ @private
+ This function is called the first time the `collectObjects`
+ function encounters a promise while converting URL parameters
+ into objects.
- hashes = this.serializeRecursively(targetState, contexts, []);
+ It triggers the `enter` and `setup` methods on the `loading`
+ handler.
- return this.urlFor(targetStateName, hashes);
- },
+ @param {Router} router
+ */
+ function loading(router) {
+ if (!router.isLoading) {
+ router.isLoading = true;
+ var handler = router.getHandler('loading');
- serializeRecursively: function(state, contexts, hashes) {
- var parentState,
- context = get(state, 'hasContext') ? contexts.pop() : null,
- hash = context ? state.serialize(this, context) : null;
+ if (handler) {
+ handler.enter && handler.enter();
+ handler.setup && handler.setup();
+ }
+ }
+ }
- hashes.push(hash);
- parentState = state.get("parentState");
+ /**
+ @private
- if (parentState && parentState instanceof Ember.Route) {
- return this.serializeRecursively(parentState, contexts, hashes);
- } else {
- return hashes;
+ This function is called if a promise was previously
+ encountered once all promises are resolved.
+
+ It triggers the `exit` method on the `loading` handler.
+
+ @param {Router} router
+ */
+ function loaded(router) {
+ router.isLoading = false;
+ var handler = router.getHandler('loading');
+ if (handler) { handler.exit && handler.exit(); }
}
- },
- abortRoutingPromises: function() {
- if (this._routingPromises) {
- this._routingPromises.abort();
- this._routingPromises = null;
+ /**
+ @private
+
+ This function is called if any encountered promise
+ is rejected.
+
+ It triggers the `exit` method on the `loading` handler,
+ the `enter` method on the `failure` handler, and the
+ `setup` method on the `failure` handler with the
+ `error`.
+
+ @param {Router} router
+ @param {Object} error the reason for the promise
+ rejection, to pass into the failure handler's
+ `setup` method.
+ */
+ function failure(router, error) {
+ loaded(router);
+ var handler = router.getHandler('failure');
+ if (handler) { handler.setup && handler.setup(error); }
}
- },
- handleStatePromises: function(states, complete) {
- this.abortRoutingPromises();
+ /**
+ @private
+
+ This function is called after a URL change has been handled
+ by `router.handleURL`.
+
+ Takes an Array of `RecognizedHandler`s, and converts the raw
+ params hashes into deserialized objects by calling deserialize
+ on the handlers. This process builds up an Array of
+ `HandlerInfo`s. It then calls `setupContexts` with the Array.
+
+ If the `deserialize` method on a handler returns a promise
+ (i.e. has a method called `then`), this function will pause
+ building up the `HandlerInfo` Array until the promise is
+ resolved. It will use the resolved value as the context of
+ `HandlerInfo`.
+ */
+ function collectObjects(router, results, index, objects) {
+ if (results.length === index) {
+ loaded(router);
+ setupContexts(router, objects);
+ return;
+ }
- this.set('isLocked', true);
+ var result = results[index];
+ var handler = router.getHandler(result.handler);
+ var object = handler.deserialize && handler.deserialize(result.params);
- var manager = this;
+ if (object && typeof object.then === 'function') {
+ loading(router);
- this._routingPromises = Ember._PromiseChain.create({
- promises: states.slice(),
+ object.then(proceed, function(error) {
+ failure(router, error);
+ });
+ } else {
+ proceed(object);
+ }
- successCallback: function() {
- manager.set('isLocked', false);
- complete();
- },
+ function proceed(value) {
+ var updatedObjects = objects.concat([{ context: value, handler: result.handler }]);
+ collectObjects(router, results, index + 1, updatedObjects);
+ }
+ }
- failureCallback: function() {
- throw "Unable to load object";
- },
+ /**
+ @private
+
+ Takes an Array of `UnresolvedHandlerInfo`s, resolves the handler names
+ into handlers, and then figures out what to do with each of the handlers.
+
+ For example, consider the following tree of handlers. Each handler is
+ followed by the URL segment it handles.
+
+ ```
+ |~index ("/")
+ | |~posts ("/posts")
+ | | |-showPost ("/:id")
+ | | |-newPost ("/new")
+ | | |-editPost ("/edit")
+ | |~about ("/about/:id")
+ ```
+
+ Consider the following transitions:
+
+ 1. A URL transition to `/posts/1`.
+ 1. Triggers the `deserialize` callback on the
+ `index`, `posts`, and `showPost` handlers
+ 2. Triggers the `enter` callback on the same
+ 3. Triggers the `setup` callback on the same
+ 2. A direct transition to `newPost`
+ 1. Triggers the `exit` callback on `showPost`
+ 2. Triggers the `enter` callback on `newPost`
+ 3. Triggers the `setup` callback on `newPost`
+ 3. A direct transition to `about` with a specified
+ context object
+ 1. Triggers the `exit` callback on `newPost`
+ and `posts`
+ 2. Triggers the `serialize` callback on `about`
+ 3. Triggers the `enter` callback on `about`
+ 4. Triggers the `setup` callback on `about`
+
+ @param {Router} router
+ @param {Array[UnresolvedHandlerInfo]} handlerInfos
+ */
+ function setupContexts(router, handlerInfos) {
+ resolveHandlers(router, handlerInfos);
+
+ var partition =
+ partitionHandlers(router.currentHandlerInfos || [], handlerInfos);
+
+ router.currentHandlerInfos = handlerInfos;
+
+ eachHandler(partition.exited, function(handler, context) {
+ delete handler.context;
+ handler.exit && handler.exit();
+ });
- promiseSuccessCallback: function(item, args) {
- set(item, 'object', args[0]);
- },
+ eachHandler(partition.updatedContext, function(handler, context) {
+ handler.context = context;
+ handler.setup && handler.setup(context);
+ });
- abortCallback: function() {
- manager.set('isLocked', false);
- }
- }).start();
- },
+ eachHandler(partition.entered, function(handler, context) {
+ handler.enter && handler.enter();
+ handler.context = context;
+ handler.setup && handler.setup(context);
+ });
+ }
- moveStatesIntoRoot: function() {
- this.root = Ember.Route.extend();
+ /**
+ @private
- for (var name in this) {
- if (name === "constructor") { continue; }
+ Iterates over an array of `HandlerInfo`s, passing the handler
+ and context into the callback.
- var state = this[name];
+ @param {Array[HandlerInfo]} handlerInfos
+ @param {Function(Object, Object)} callback
+ */
+ function eachHandler(handlerInfos, callback) {
+ for (var i=0, l=handlerInfos.length; i<l; i++) {
+ var handlerInfo = handlerInfos[i],
+ handler = handlerInfo.handler,
+ context = handlerInfo.context;
- if (state instanceof Ember.Route || Ember.Route.detect(state)) {
- this.root[name] = state;
- delete this[name];
+ callback(handler, context);
}
}
- },
- init: function() {
- if (!this.root) {
- this.moveStatesIntoRoot();
+ /**
+ @private
+
+ Updates the `handler` field in each element in an Array of
+ `UnresolvedHandlerInfo`s from a handler name to a resolved handler.
+
+ When done, the Array will contain `HandlerInfo` structures.
+
+ @param {Router} router
+ @param {Array[UnresolvedHandlerInfo]} handlerInfos
+ */
+ function resolveHandlers(router, handlerInfos) {
+ for (var i=0, l=handlerInfos.length; i<l; i++) {
+ handlerInfos[i].handler = router.getHandler(handlerInfos[i].handler);
+ }
}
- this._super();
+ /**
+ @private
+
+ This function is called when transitioning from one URL to
+ another to determine which handlers are not longer active,
+ which handlers are newly active, and which handlers remain
+ active but have their context changed.
+
+ Take a list of old handlers and new handlers and partition
+ them into four buckets:
+
+ * unchanged: the handler was active in both the old and
+ new URL, and its context remains the same
+ * updated context: the handler was active in both the
+ old and new URL, but its context changed. The handler's
+ `setup` method, if any, will be called with the new
+ context.
+ * exited: the handler was active in the old URL, but is
+ no longer active.
+ * entered: the handler was not active in the old URL, but
+ is now active.
+
+ The PartitionedHandlers structure has three fields:
+
+ * `updatedContext`: a list of `HandlerInfo` objects that
+ represent handlers that remain active but have a changed
+ context
+ * `entered`: a list of `HandlerInfo` objects that represent
+ handlers that are newly active
+ * `exited`: a list of `HandlerInfo` objects that are no
+ longer active.
+
+ @param {Array[HandlerInfo]} oldHandlers a list of the handler
+ information for the previous URL (or `[]` if this is the
+ first handled transition)
+ @param {Array[HandlerInfo]} newHandlers a list of the handler
+ information for the new URL
+
+ @returns {Partition}
+ */
+ function partitionHandlers(oldHandlers, newHandlers) {
+ var handlers = {
+ updatedContext: [],
+ exited: [],
+ entered: []
+ };
+
+ var handlerChanged, contextChanged;
+
+ for (var i=0, l=newHandlers.length; i<l; i++) {
+ var oldHandler = oldHandlers[i], newHandler = newHandlers[i];
+
+ if (!oldHandler || oldHandler.handler !== newHandler.handler) {
+ handlerChanged = true;
+ }
+
+ if (handlerChanged) {
+ handlers.entered.push(newHandler);
+ if (oldHandler) { handlers.exited.unshift(oldHandler); }
+ } else if (contextChanged || oldHandler.context !== newHandler.context) {
+ contextChanged = true;
+ handlers.updatedContext.push(newHandler);
+ }
+ }
- var location = get(this, 'location'),
- rootURL = get(this, 'rootURL');
+ for (var i=newHandlers.length, l=oldHandlers.length; i<l; i++) {
+ handlers.exited.unshift(oldHandlers[i]);
+ }
- if ('string' === typeof location) {
- set(this, 'location', Ember.Location.create({
- implementation: location,
- rootURL: rootURL
- }));
+ return handlers;
}
- this.assignRouter(this, this);
- },
+ function trigger(router, name) {
+ var currentHandlerInfos = router.currentHandlerInfos;
- assignRouter: function(state, router) {
- state.router = router;
+ if (!currentHandlerInfos) {
+ throw new Error("Could not trigger event. There are no active handlers");
+ }
- var childStates = state.states;
+ for (var i=currentHandlerInfos.length-1; i>=0; i--) {
+ var handlerInfo = currentHandlerInfos[i],
+ handler = handlerInfo.handler;
- if (childStates) {
- for (var stateName in childStates) {
- if (!childStates.hasOwnProperty(stateName)) { continue; }
- this.assignRouter(childStates[stateName], router);
+ if (handler.events && handler.events[name]) {
+ handler.events[name](handler);
+ break;
+ }
}
}
- },
-
- willDestroy: function() {
- get(this, 'location').destroy();
- }
-});
+ return Router;
+ });
View
1  packages/ember-routing/package.json
@@ -13,7 +13,6 @@
"dependencies": {
"spade": "~> 1.0",
"ember-views": "1.0.0-pre.2",
- "ember-states": "1.0.0-pre.2"
},
"dependencies:development": {
Please sign in to comment.
Something went wrong with that request. Please try again.