From 9308d5558e818596141d175024c3c5be80c05b0a Mon Sep 17 00:00:00 2001 From: machty Date: Fri, 29 Nov 2013 16:42:20 -0500 Subject: [PATCH] [FEATURE query-params-new] Query params rewrite See here for some examples: https://gist.github.com/machty/8167051 Depends on Handlebars subexpressions: https://github.com/wycats/handlebars.js/pull/690 --- FEATURES.md | 17 +- Gemfile.lock | 4 +- ember-source.gemspec | 2 +- features.json | 2 +- .../ember-handlebars-compiler/lib/main.js | 1 + packages/ember-routing/lib/ext/controller.js | 89 +- packages/ember-routing/lib/helpers/link_to.js | 166 +- .../lib/location/hash_location.js | 2 +- .../lib/location/history_location.js | 2 +- packages/ember-routing/lib/system/dsl.js | 5 - packages/ember-routing/lib/system/route.js | 204 +- packages/ember-routing/lib/system/router.js | 97 +- .../lib/vendor/route-recognizer.js | 111 +- packages/ember-routing/lib/vendor/router.js | 2231 ++++++++++------- .../tests/ext/controller_test.js | 81 + packages/ember/tests/helpers/link_to_test.js | 444 ++-- packages/ember/tests/routing/basic_test.js | 76 +- .../ember/tests/routing/query_params_test.js | 549 ++-- 18 files changed, 2382 insertions(+), 1701 deletions(-) create mode 100644 packages/ember-routing/tests/ext/controller_test.js diff --git a/FEATURES.md b/FEATURES.md index e343c5f7619..8df923e6d3d 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -21,14 +21,6 @@ for a detailed explanation. Added in [#3614](https://github.com/emberjs/ember.js/pull/3614). -* `query-params` - - Add query params support to the ember router. You can now define which query - params your routes respond to, use them in your route hooks to affect model - loading or controller state, and transition query parameters with the link-to - helper and the transitionTo method. - - Added in [#3182](https://github.com/emberjs/ember.js/pull/3182). * `propertyBraceExpansion` Adds support for brace-expansion in dependent keys, observer, and watch properties. @@ -129,3 +121,12 @@ for a detailed explanation. Ember.computed.oneWay('foo').readOnly(). Added in [#3879](https://github.com/emberjs/ember.js/pull/3879) + +* `query-params-new` + + Add query params support to the ember router. This is a rewrite of a + previous attempt at an API for query params. You can define query + param properties on route-driven controllers with the `queryParams` + property, and any changes to those properties will cause the URL + to update, and in the other direction, any URL changes to the query + params will cause those controller properties to update. diff --git a/Gemfile.lock b/Gemfile.lock index a7738fc6447..edc96471039 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -28,7 +28,7 @@ PATH remote: . specs: ember-source (1.4.0.beta.1.canary) - handlebars-source (~> 1.1.2) + handlebars-source (~> 1.2.0) GEM remote: https://rubygems.org/ @@ -45,7 +45,7 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3.6) - handlebars-source (1.1.2) + handlebars-source (1.2.1) json (1.8.1) kicker (3.0.0) listen (~> 1.3.0) diff --git a/ember-source.gemspec b/ember-source.gemspec index 8645ceec4cb..fb20007a66c 100644 --- a/ember-source.gemspec +++ b/ember-source.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |gem| gem.version = Ember.rubygems_version_string - gem.add_dependency "handlebars-source", ["~> 1.1.2"] + gem.add_dependency "handlebars-source", ["~> 1.2.0"] gem.files = %w(VERSION) + Dir['dist/*.js', 'lib/ember/*.rb'] end diff --git a/features.json b/features.json index 29bab17bbea..16b2bdc32ae 100644 --- a/features.json +++ b/features.json @@ -2,7 +2,7 @@ "reduceComputed-non-array-dependencies": true, "ember-testing-lazy-routing": true, "ember-testing-wait-hooks": true, - "query-params": null, + "query-params-new": null, "string-humanize": null, "string-parameterize": null, "propertyBraceExpansion": null, diff --git a/packages/ember-handlebars-compiler/lib/main.js b/packages/ember-handlebars-compiler/lib/main.js index f40699197d0..f643ade298c 100644 --- a/packages/ember-handlebars-compiler/lib/main.js +++ b/packages/ember-handlebars-compiler/lib/main.js @@ -191,6 +191,7 @@ var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { + debugger; var helperInvocation = source[source.length - 1], helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); diff --git a/packages/ember-routing/lib/ext/controller.js b/packages/ember-routing/lib/ext/controller.js index 80317794383..697b5248512 100644 --- a/packages/ember-routing/lib/ext/controller.js +++ b/packages/ember-routing/lib/ext/controller.js @@ -3,7 +3,10 @@ @submodule ember-routing */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, + map = Ember.EnumerableUtils.map; + +var queuedQueryParamChanges = {}; Ember.ControllerMixin.reopen({ /** @@ -61,7 +64,7 @@ Ember.ControllerMixin.reopen({ /** Transition into another route while replacing the current URL, if possible. - This will replace the current history entry instead of adding a new one. + This will replace the current history entry instead of adding a new one. Beside that, it is identical to `transitionToRoute` in all other respects. ```javascript @@ -111,3 +114,85 @@ Ember.ControllerMixin.reopen({ return this.replaceRoute.apply(this, arguments); } }); + +if (Ember.FEATURES.isEnabled("query-params-new")) { + Ember.ControllerMixin.reopen({ + + concatenatedProperties: ['queryParams'], + + queryParams: null, + + _queryParamScope: null, + + _finalizingQueryParams: false, + _queryParamHash: Ember.computed(function computeQueryParamHash() { + + // Given: queryParams: ['foo', 'bar:baz'] on controller:thing + // _queryParamHash should yield: { 'foo': 'thing[foo]' } + + var result = {}; + var queryParams = this.queryParams; + if (!queryParams) { + return result; + } + + for (var i = 0, len = queryParams.length; i < len; ++i) { + var full = queryParams[i]; + var parts = full.split(':'); + var key = parts[0]; + var urlKey = parts[1]; + if (!urlKey) { + if (this._queryParamScope) { + urlKey = this._queryParamScope + '[' + key + ']'; + } else { + urlKey = key; + } + } + result[key] = urlKey; + } + + return result; + }), + + _activateQueryParamObservers: function() { + var queryParams = get(this, '_queryParamHash'); + + for (var k in queryParams) { + if (queryParams.hasOwnProperty(k)) { + this.addObserver(k, this, this._queryParamChanged); + } + } + }, + + _deactivateQueryParamObservers: function() { + var queryParams = get(this, '_queryParamHash'); + + for (var k in queryParams) { + if (queryParams.hasOwnProperty(k)) { + this.removeObserver(k, this, this._queryParamChanged); + } + } + }, + + _queryParamChanged: function(controller, key) { + if (this._finalizingQueryParams) { + var changes = this._queryParamChangesDuringSuspension; + if (changes) { + changes[key] = true; + } + return; + } + + var queryParams = get(this, '_queryParamHash'); + queuedQueryParamChanges[queryParams[key]] = get(this, key); + Ember.run.once(this, this._fireQueryParamTransition); + }, + + _fireQueryParamTransition: function() { + this.transitionToRoute({ queryParams: queuedQueryParamChanges }); + queuedQueryParamChanges = {}; + }, + + _queryParamChangesDuringSuspension: null + }); +} diff --git a/packages/ember-routing/lib/helpers/link_to.js b/packages/ember-routing/lib/helpers/link_to.js index 1733889c693..e5776809a7d 100644 --- a/packages/ember-routing/lib/helpers/link_to.js +++ b/packages/ember-routing/lib/helpers/link_to.js @@ -5,10 +5,16 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; +var slice = Array.prototype.slice; + require('ember-handlebars/helpers/view'); Ember.onLoad('Ember.Handlebars', function(Handlebars) { + var QueryParams = Ember.Object.extend({ + values: null + }); + var resolveParams = Ember.Router.resolveParams, resolvePaths = Ember.Router.resolvePaths, isSimpleClick = Ember.ViewUtils.isSimpleClick; @@ -182,14 +188,6 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { // Map desired event name to invoke function var eventName = get(this, 'eventName'), i; this.on(eventName, this, this._invoke); - - if (Ember.FEATURES.isEnabled("query-params")) { - var queryParams = get(this, '_potentialQueryParams') || []; - - for(i=0; i < queryParams.length; i++) { - this.registerObserver(this, queryParams[i], this, this._queryParamsChanged); - } - } }, /** @@ -232,6 +230,22 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, path, helperParameters.options.data); this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); } + + var queryParamsObject = this.queryParamsObject; + if (queryParamsObject) { + var values = queryParamsObject.values; + + // Install observers for all of the hash options + // provided in the (query-params) subexpression. + for (var k in values) { + if (!values.hasOwnProperty(k)) { continue; } + + if (queryParamsObject.types[k] === 'ID') { + normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, values[k], helperParameters.options.data); + this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); + } + } + } }, afterRender: function(){ @@ -239,17 +253,6 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { this._setupPathObservers(); }, - /** - @private - - This method is invoked by observers installed during `init` that fire - whenever the query params change - */ - _queryParamsChanged: function (object, path) { - this.notifyPropertyChange('queryParams'); - }, - - /** @private @@ -299,7 +302,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { router.isActive.apply(router, [currentWithIndex].concat(contexts)); if (isActive) { return get(this, 'activeClass'); } - }).property('resolvedParams', 'routeArgs', 'router.url'), + }).property('resolvedParams', 'routeArgs'), /** Accessed as a classname binding to apply the `LinkView`'s `loadingClass` @@ -361,7 +364,19 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { /** @private - Computed property that returns the resolved parameters. + Computed property that returns an array of the + resolved parameters passed to the `link-to` helper, + e.g.: + + ```hbs + {{link-to a b '123' c}} + ``` + + will generate a `resolvedParams` of: + + ```js + [aObject, bObject, '123', cObject] + ``` @property @return {Array} @@ -372,18 +387,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { types = options.types, data = options.data; - if (Ember.FEATURES.isEnabled("query-params")) { - if (parameters.params.length === 0) { - var appController = this.container.lookup('controller:application'); - return [get(appController, 'currentRouteName')]; - } else { - return resolveParams(parameters.context, parameters.params, { types: types, data: data }); - } + if (parameters.params.length === 0) { + var appController = this.container.lookup('controller:application'); + return [get(appController, 'currentRouteName')]; + } else { + return resolveParams(parameters.context, parameters.params, { types: types, data: data }); } // Original implementation if query params not enabled return resolveParams(parameters.context, parameters.params, { types: types, data: data }); - }).property(), + }).property('router.url'), /** @private @@ -412,43 +425,68 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { } } - if (Ember.FEATURES.isEnabled("query-params")) { - var queryParams = get(this, 'queryParams'); - - if (queryParams || queryParams === false) { resolvedParams.push({queryParams: queryParams}); } + if (Ember.FEATURES.isEnabled("query-params-new")) { + resolvedParams.push({ queryParams: get(this, 'queryParams') }); } return resolvedParams; - }).property('resolvedParams', 'queryParams', 'router.url'), + }).property('resolvedParams', 'queryParams'), + queryParamsObject: null, + queryParams: Ember.computed(function computeLinkViewQueryParams() { - _potentialQueryParams: Ember.computed(function () { - var namedRoute = get(this, 'resolvedParams')[0]; - if (!namedRoute) { return null; } - var router = get(this, 'router'); + var queryParamsObject = get(this, 'queryParamsObject'), + suppliedParams = {}; - namedRoute = fullRouteName(router, namedRoute); + if (queryParamsObject) { + Ember.merge(suppliedParams, queryParamsObject.values); + } + + var resolvedParams = get(this, 'resolvedParams'), + router = get(this, 'router'), + routeName = resolvedParams[0], + paramsForRoute = router._queryParamNamesFor(routeName), + queryParams = paramsForRoute.queryParams, + translations = paramsForRoute.translations, + paramsForRecognizer = {}, + name; + + // Normalize supplied params into their long-form name + // e.g. 'foo' -> 'controllername:foo' + for (name in suppliedParams) { + if (!suppliedParams.hasOwnProperty(name)) { continue; } + + if (name in translations) { + suppliedParams[translations[name]] = suppliedParams[name]; + delete suppliedParams[name]; + } else { + Ember.assert(fmt("You supplied an unknown query param controller property '%@' for route '%@'. Only the following query param properties can be set for this route: %@", [name, routeName, Ember.keys(translations)]), name in queryParams); + } + } + + var helperParameters = this.parameters; + router._queryParamOverrides(paramsForRecognizer, queryParams, function(name, resultsName) { + if (!(name in suppliedParams)) { return; } - return router.router.queryParamsForHandler(namedRoute); - }).property('resolvedParams'), + var parts = name.split(':'); - queryParams: Ember.computed(function () { - var self = this, - queryParams = null, - allowedQueryParams = get(this, '_potentialQueryParams'); + var type = queryParamsObject.types[parts[1]]; - if (!allowedQueryParams) { return null; } - allowedQueryParams.forEach(function (param) { - var value = get(self, param); - if (typeof value !== 'undefined') { - queryParams = queryParams || {}; - queryParams[param] = value; + var value; + if (type === 'ID') { + var normalizedPath = Ember.Handlebars.normalizePath(helperParameters.context, suppliedParams[name], helperParameters.options.data); + value = Ember.Handlebars.get(normalizedPath.root, normalizedPath.path, helperParameters.options); + } else { + value = suppliedParams[name]; } - }); + delete suppliedParams[name]; + + paramsForRecognizer[resultsName] = value; + }); - return queryParams; - }).property('_potentialQueryParams.[]'), + return paramsForRecognizer; + }).property('resolvedParams.[]'), /** Sets the element's `href` attribute to the url for @@ -744,10 +782,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @see {Ember.LinkView} */ Ember.Handlebars.registerHelper('link-to', function linkToHelper(name) { - var options = [].slice.call(arguments, -1)[0], - params = [].slice.call(arguments, 0, -1), + var options = slice.call(arguments, -1)[0], + params = slice.call(arguments, 0, -1), hash = options.hash; + if (params[params.length - 1] instanceof QueryParams) { + hash.queryParamsObject = params.pop(); + } + hash.disabledBinding = hash.disabledWhen; if (!options.fn) { @@ -775,6 +817,18 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return Ember.Handlebars.helpers.view.call(this, LinkView, options); }); + + if (Ember.FEATURES.isEnabled("query-params-new")) { + Ember.Handlebars.registerHelper('query-params', function queryParamsHelper(options) { + Ember.assert(fmt("The `query-params` helper only accepts hash parameters, e.g. (query-params queryParamPropertyName='%@') as opposed to just (query-params '%@')", [options, options]), arguments.length === 1); + + return QueryParams.create({ + values: options.hash, + types: options.hashTypes + }); + }); + } + /** See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) diff --git a/packages/ember-routing/lib/location/hash_location.js b/packages/ember-routing/lib/location/hash_location.js index 70644b84db8..de8d2b5e33d 100644 --- a/packages/ember-routing/lib/location/hash_location.js +++ b/packages/ember-routing/lib/location/hash_location.js @@ -29,7 +29,7 @@ Ember.HashLocation = Ember.Object.extend({ @method getURL */ getURL: function() { - if (Ember.FEATURES.isEnabled("query-params")) { + if (Ember.FEATURES.isEnabled("query-params-new")) { // location.hash is not used because it is inconsistently // URL-decoded between browsers. var href = get(this, 'location').href, diff --git a/packages/ember-routing/lib/location/history_location.js b/packages/ember-routing/lib/location/history_location.js index d6ca0a51f1a..fa81b0a13f1 100644 --- a/packages/ember-routing/lib/location/history_location.js +++ b/packages/ember-routing/lib/location/history_location.js @@ -58,7 +58,7 @@ Ember.HistoryLocation = Ember.Object.extend({ rootURL = rootURL.replace(/\/$/, ''); var url = path.replace(rootURL, ''); - if (Ember.FEATURES.isEnabled("query-params")) { + if (Ember.FEATURES.isEnabled("query-params-new")) { var search = location.search || ''; url += search; } diff --git a/packages/ember-routing/lib/system/dsl.js b/packages/ember-routing/lib/system/dsl.js index 50d231eb3cc..4115de0f7a3 100644 --- a/packages/ember-routing/lib/system/dsl.js +++ b/packages/ember-routing/lib/system/dsl.js @@ -70,11 +70,6 @@ DSL.prototype = { for (var i=0, l=dslMatches.length; i 1) { + urlKeyName = parts[1]; + } else { + // TODO: use _queryParamScope here? + if (controllerName !== 'application') { + urlKeyName = controllerName + '[' + propName + ']'; + } else { + urlKeyName = propName; + } + } + + var controllerFullname = controllerName + ':' + propName; + + result.queryParams[controllerFullname] = urlKeyName; + result.translations[parts[0]] = controllerFullname; + }); + } +} + /** @private @@ -300,7 +387,7 @@ Ember.Router = Ember.Object.extend(Ember.Evented, { to bubble upward. */ function forEachRouteAbove(originRoute, transition, callback) { - var handlerInfos = transition.handlerInfos, + var handlerInfos = transition.state.handlerInfos, originRouteFound = false; for (var i = handlerInfos.length - 1; i >= 0; --i) { @@ -530,6 +617,4 @@ Ember.Router.reopenClass({ } }); -Router.Transition.prototype.send = Router.Transition.prototype.trigger; - diff --git a/packages/ember-routing/lib/vendor/route-recognizer.js b/packages/ember-routing/lib/vendor/route-recognizer.js index 563c8514cca..f20b1bfc446 100644 --- a/packages/ember-routing/lib/vendor/route-recognizer.js +++ b/packages/ember-routing/lib/vendor/route-recognizer.js @@ -1,6 +1,6 @@ -define("route-recognizer", - [], - function() { +define("route-recognizer", + ["exports"], + function(__exports__) { "use strict"; var specials = [ '/', '.', '*', '+', '?', '|', @@ -29,11 +29,11 @@ define("route-recognizer", function StaticSegment(string) { this.string = string; } StaticSegment.prototype = { eachChar: function(callback) { - var string = this.string, char; + var string = this.string, ch; for (var i=0, l=string.length; i 0) { - currentResult.queryParams = activeQueryParams; - } - result.push(currentResult); + + result.push({ handler: handler.handler, params: params, isDynamic: !!names.length }); } return result; } function addSegment(currentState, segment) { - segment.eachChar(function(char) { + segment.eachChar(function(ch) { var state; - currentState = currentState.put(char); + currentState = currentState.put(ch); }); return currentState; @@ -330,9 +335,6 @@ define("route-recognizer", } var handler = { handler: route.handler, names: names }; - if(route.queryParams) { - handler.queryParams = route.queryParams; - } handlers.push(handler); } @@ -393,20 +395,14 @@ define("route-recognizer", }, generateQueryString: function(params, handlers) { - var pairs = [], allowedParams = []; - for(var i=0; i < handlers.length; i++) { - var currentParamList = handlers[i].queryParams; - if(currentParamList) { - allowedParams.push.apply(allowedParams, currentParamList); - } - } + var pairs = []; for(var key in params) { if (params.hasOwnProperty(key)) { - if(allowedParams.indexOf(key) === -1) { - throw 'Query param "' + key + '" is not specified as a valid param for this route'; - } var value = params[key]; - var pair = encodeURIComponent(key); + if (value === false || value == null) { + continue; + } + var pair = key; if(value !== true) { pair += "=" + encodeURIComponent(value); } @@ -472,6 +468,8 @@ define("route-recognizer", } }; + __exports__["default"] = RouteRecognizer; + function Target(path, matcher, delegate) { this.path = path; this.matcher = matcher; @@ -493,24 +491,12 @@ define("route-recognizer", this.matcher.addChild(this.path, target, callback, this.delegate); } return this; - }, - - withQueryParams: function() { - if (arguments.length === 0) { throw new Error("you must provide arguments to the withQueryParams method"); } - for (var i = 0; i < arguments.length; i++) { - if (typeof arguments[i] !== "string") { - throw new Error('you should call withQueryParams with a list of strings, e.g. withQueryParams("foo", "bar")'); - } - } - var queryParams = [].slice.call(arguments); - this.matcher.addQueryParams(this.path, queryParams); } }; function Matcher(target) { this.routes = {}; this.children = {}; - this.queryParams = {}; this.target = target; } @@ -519,10 +505,6 @@ define("route-recognizer", this.routes[path] = handler; }, - addQueryParams: function(path, params) { - this.queryParams[path] = params; - }, - addChild: function(path, target, callback, delegate) { var matcher = new Matcher(target); this.children[path] = matcher; @@ -549,7 +531,7 @@ define("route-recognizer", }; } - function addRoute(routeArray, path, handler, queryParams) { + function addRoute(routeArray, path, handler) { var len = 0; for (var i=0, l=routeArray.length; i 0) { - var err = 'You supplied the params '; - err += missingParams.map(function(param) { - return '"' + param + "=" + queryParams[param] + '"'; - }).join(' and '); - - err += ' which are not valid for the "' + handlerName + '" handler or its parents'; - - throw new Error(err); + // Construct a TransitionIntent with the provided params + // and apply it to the present state of the router. + var intent = new NamedTransitionIntent({ name: handlerName, contexts: suppliedParams }); + var state = intent.applyToState(this.state, this.recognizer, this.getHandler); + var params = {}; + + for (var i = 0, len = state.handlerInfos.length; i < len; ++i) { + var handlerInfo = state.handlerInfos[i]; + var handlerParams = handlerInfo.params || + serialize(handlerInfo.handler, handlerInfo.context, handlerInfo.names); + merge(params, handlerParams); } + params.queryParams = queryParams; return this.recognizer.generate(handlerName, params); }, isActive: function(handlerName) { + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), contexts = partitionedArgs[0], queryParams = partitionedArgs[1], activeQueryParams = {}, effectiveQueryParams = {}; - var targetHandlerInfos = this.targetHandlerInfos, - found = false, names, object, handlerInfo, handlerObj; + var targetHandlerInfos = this.state.handlerInfos, + found = false, names, object, handlerInfo, handlerObj, i, len; - if (!targetHandlerInfos) { return false; } + if (!targetHandlerInfos.length) { return false; } - var recogHandlers = this.recognizer.handlersFor(targetHandlerInfos[targetHandlerInfos.length - 1].name); - for (var i=targetHandlerInfos.length-1; i>=0; i--) { - handlerInfo = targetHandlerInfos[i]; - if (handlerInfo.name === handlerName) { found = true; } + var targetHandler = targetHandlerInfos[targetHandlerInfos.length - 1].name; + var recogHandlers = this.recognizer.handlersFor(targetHandler); - if (found) { - var recogHandler = recogHandlers[i]; + var index = 0; + for (len = recogHandlers.length; index < len; ++index) { + handlerInfo = targetHandlerInfos[index]; + if (handlerInfo.name === handlerName) { break; } + } - merge(activeQueryParams, handlerInfo.queryParams); - if (queryParams !== false) { - merge(effectiveQueryParams, handlerInfo.queryParams); - mergeSomeKeys(effectiveQueryParams, queryParams, recogHandler.queryParams); - } + if (index === recogHandlers.length) { + // The provided route name isn't even in the route hierarchy. + return false; + } - if (handlerInfo.isDynamic && contexts.length > 0) { - object = contexts.pop(); + var state = new TransitionState(); + state.handlerInfos = targetHandlerInfos.slice(0, index + 1); + recogHandlers = recogHandlers.slice(0, index + 1); - if (isParam(object)) { - var name = recogHandler.names[0]; - if (!this.currentParams || "" + object !== this.currentParams[name]) { return false; } - } else if (handlerInfo.context !== object) { - return false; - } - } - } - } + var intent = new NamedTransitionIntent({ + name: targetHandler, + contexts: contexts + }); + var newState = intent.applyToHandlers(state, recogHandlers, this.getHandler, targetHandler, true, true); - return contexts.length === 0 && found && queryParamsEqual(activeQueryParams, effectiveQueryParams); + return handlerInfosEqual(newState.handlerInfos, state.handlerInfos); }, trigger: function(name) { @@ -423,6 +527,24 @@ define("router", trigger(this, this.currentHandlerInfos, false, args); }, + /** + @private + + Pluggable hook for possibly running route hooks + in a try-catch escaping manner. + + @param {Function} callback the callback that will + be asynchronously called + + @return {Promise} a promise that fulfills with the + value returned from the callback + */ + async: function(callback) { + return new Promise(function(resolve) { + resolve(callback()); + }); + }, + /** Hook point for logging transition status updates. @@ -434,316 +556,23 @@ define("router", /** @private - Used internally for both URL and named transition to determine - a shared pivot parent route and other data necessary to perform - a transition. - */ - function getMatchPoint(router, handlers, objects, inputParams, queryParams) { - - var matchPoint = handlers.length, - providedModels = {}, i, - currentHandlerInfos = router.currentHandlerInfos || [], - params = {}, - oldParams = router.currentParams || {}, - activeTransition = router.activeTransition, - handlerParams = {}, - obj; - - objects = slice.call(objects); - merge(params, inputParams); - - for (i = handlers.length - 1; i >= 0; i--) { - var handlerObj = handlers[i], - handlerName = handlerObj.handler, - oldHandlerInfo = currentHandlerInfos[i], - hasChanged = false; - - // Check if handler names have changed. - if (!oldHandlerInfo || oldHandlerInfo.name !== handlerObj.handler) { hasChanged = true; } - - if (handlerObj.isDynamic) { - // URL transition. - - if (obj = getMatchPointObject(objects, handlerName, activeTransition, true, params)) { - hasChanged = true; - providedModels[handlerName] = obj; - } else { - handlerParams[handlerName] = {}; - for (var prop in handlerObj.params) { - if (!handlerObj.params.hasOwnProperty(prop)) { continue; } - var newParam = handlerObj.params[prop]; - if (oldParams[prop] !== newParam) { hasChanged = true; } - handlerParams[handlerName][prop] = params[prop] = newParam; - } - } - } else if (handlerObj.hasOwnProperty('names')) { - // Named transition. + Takes an Array of `HandlerInfo`s, figures out which ones are + exiting, entering, or changing contexts, and calls the + proper handler hooks. - if (objects.length) { hasChanged = true; } + For example, consider the following tree of handlers. Each handler is + followed by the URL segment it handles. - if (obj = getMatchPointObject(objects, handlerName, activeTransition, handlerObj.names[0], params)) { - providedModels[handlerName] = obj; - } else { - var names = handlerObj.names; - handlerParams[handlerName] = {}; - for (var j = 0, len = names.length; j < len; ++j) { - var name = names[j]; - handlerParams[handlerName][name] = params[name] = params[name] || oldParams[name]; - } - } - } + ``` + |~index ("/") + | |~posts ("/posts") + | | |-showPost ("/:id") + | | |-newPost ("/new") + | | |-editPost ("/edit") + | |~about ("/about/:id") + ``` - // If there is an old handler, see if query params are the same. If there isn't an old handler, - // hasChanged will already be true here - if(oldHandlerInfo && !queryParamsEqual(oldHandlerInfo.queryParams, handlerObj.queryParams)) { - hasChanged = true; - } - - if (hasChanged) { matchPoint = i; } - } - - if (objects.length > 0) { - throw new Error("More context objects were passed than there are dynamic segments for the route: " + handlers[handlers.length - 1].handler); - } - - var pivotHandlerInfo = currentHandlerInfos[matchPoint - 1], - pivotHandler = pivotHandlerInfo && pivotHandlerInfo.handler; - - return { matchPoint: matchPoint, providedModels: providedModels, params: params, handlerParams: handlerParams, pivotHandler: pivotHandler }; - } - - function getMatchPointObject(objects, handlerName, activeTransition, paramName, params) { - - if (objects.length && paramName) { - - var object = objects.pop(); - - // If provided object is string or number, treat as param. - if (isParam(object)) { - params[paramName] = object.toString(); - } else { - return object; - } - } else if (activeTransition) { - // Use model from previous transition attempt, preferably the resolved one. - return activeTransition.resolvedModels[handlerName] || - (paramName && activeTransition.providedModels[handlerName]); - } - } - - function isParam(object) { - return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); - } - - - - /** - @private - - This method takes a handler name and returns a list of query params - that are valid to pass to the handler or its parents - - @param {Router} router - @param {String} handlerName - @return {Array[String]} a list of query parameters - */ - function queryParamsForHandler(router, handlerName) { - var handlers = router.recognizer.handlersFor(handlerName), - queryParams = []; - - for (var i = 0; i < handlers.length; i++) { - queryParams.push.apply(queryParams, handlers[i].queryParams || []); - } - - return queryParams; - } - /** - @private - - This method takes a handler name and a list of contexts and returns - a serialized parameter hash suitable to pass to `recognizer.generate()`. - - @param {Router} router - @param {String} handlerName - @param {Array[Object]} objects - @return {Object} a serialized parameter hash - */ - function paramsForHandler(router, handlerName, objects, queryParams) { - - var handlers = router.recognizer.handlersFor(handlerName), - params = {}, - handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams), - matchPoint = getMatchPoint(router, handlerInfos, objects).matchPoint, - mergedQueryParams = {}, - object, handlerObj, handler, names, i; - - params.queryParams = {}; - - for (i=0; i= matchPoint) { - object = objects.shift(); - // Otherwise use existing context - } else { - object = handler.context; - } - - // Serialize to generate params - merge(params, serialize(handler, object, names)); - } - if (queryParams !== false) { - mergeSomeKeys(params.queryParams, router.currentQueryParams, handlerObj.queryParams); - mergeSomeKeys(params.queryParams, queryParams, handlerObj.queryParams); - } - } - - if (queryParamsEqual(params.queryParams, {})) { delete params.queryParams; } - return params; - } - - function merge(hash, other) { - for (var prop in other) { - if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } - } - } - - function mergeSomeKeys(hash, other, keys) { - if (!other || !keys) { return; } - for(var i = 0; i < keys.length; i++) { - var key = keys[i], value; - if(other.hasOwnProperty(key)) { - value = other[key]; - if(value === null || value === false || typeof value === "undefined") { - delete hash[key]; - } else { - hash[key] = other[key]; - } - } - } - } - - /** - @private - */ - - function generateHandlerInfosWithQueryParams(router, handlers, queryParams) { - var handlerInfos = []; - - for (var i = 0; i < handlers.length; i++) { - var handler = handlers[i], - handlerInfo = { handler: handler.handler, names: handler.names, context: handler.context, isDynamic: handler.isDynamic }, - activeQueryParams = {}; - - if (queryParams !== false) { - mergeSomeKeys(activeQueryParams, router.currentQueryParams, handler.queryParams); - mergeSomeKeys(activeQueryParams, queryParams, handler.queryParams); - } - - if (handler.queryParams && handler.queryParams.length > 0) { - handlerInfo.queryParams = activeQueryParams; - } - - handlerInfos.push(handlerInfo); - } - - return handlerInfos; - } - - /** - @private - */ - function createQueryParamTransition(router, queryParams, isIntermediate) { - var currentHandlers = router.currentHandlerInfos, - currentHandler = currentHandlers[currentHandlers.length - 1], - name = currentHandler.name; - - log(router, "Attempting query param transition"); - - return createNamedTransition(router, [name, queryParams], isIntermediate); - } - - /** - @private - */ - function createNamedTransition(router, args, isIntermediate) { - var partitionedArgs = extractQueryParams(args), - pureArgs = partitionedArgs[0], - queryParams = partitionedArgs[1], - handlers = router.recognizer.handlersFor(pureArgs[0]), - handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams); - - - log(router, "Attempting transition to " + pureArgs[0]); - - return performTransition(router, - handlerInfos, - slice.call(pureArgs, 1), - router.currentParams, - queryParams, - null, - isIntermediate); - } - - /** - @private - */ - function createURLTransition(router, url, isIntermediate) { - var results = router.recognizer.recognize(url), - currentHandlerInfos = router.currentHandlerInfos, - queryParams = {}, - i, len; - - log(router, "Attempting URL transition to " + url); - - if (results) { - // Make sure this route is actually accessible by URL. - for (i = 0, len = results.length; i < len; ++i) { - - if (router.getHandler(results[i].handler).inaccessibleByURL) { - results = null; - break; - } - } - } - - if (!results) { - return errorTransition(router, new Router.UnrecognizedURLError(url)); - } - - for(i = 0, len = results.length; i < len; i++) { - merge(queryParams, results[i].queryParams); - } - - return performTransition(router, results, [], {}, queryParams, null, isIntermediate); - } - - - /** - @private - - Takes an Array of `HandlerInfo`s, figures out which ones are - exiting, entering, or changing contexts, and calls the - proper handler hooks. - - 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: + Consider the following transitions: 1. A URL transition to `/posts/1`. 1. Triggers the `*model` callbacks on the @@ -762,104 +591,70 @@ define("router", 3. Triggers the `enter` callback on `about` 4. Triggers the `setup` callback on `about` - @param {Transition} transition - @param {Array[HandlerInfo]} handlerInfos + @param {Router} transition + @param {TransitionState} newState */ - function setupContexts(transition, handlerInfos) { - var router = transition.router, - partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos); - - router.targetHandlerInfos = handlerInfos; + function setupContexts(router, newState, transition) { + var partition = partitionHandlers(router.state, newState); - eachHandler(partition.exited, function(handlerInfo) { + forEach(partition.exited, function(handlerInfo) { var handler = handlerInfo.handler; delete handler.context; if (handler.exit) { handler.exit(); } }); - var currentHandlerInfos = partition.unchanged.slice(); - router.currentHandlerInfos = currentHandlerInfos; + var oldState = router.oldState = router.state; + router.state = newState; + var currentHandlerInfos = router.currentHandlerInfos = partition.unchanged.slice(); - eachHandler(partition.updatedContext, function(handlerInfo) { - handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, false); - }); + try { + forEach(partition.updatedContext, function(handlerInfo) { + return handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, false, transition); + }); - eachHandler(partition.entered, function(handlerInfo) { - handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); - }); + forEach(partition.entered, function(handlerInfo) { + return handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, true, transition); + }); + } catch(e) { + router.state = oldState; + router.currentHandlerInfos = oldState.handlerInfos; + throw e; + } + + router.state.queryParams = finalizeQueryParamChange(router, currentHandlerInfos, newState.queryParams); } + /** @private Helper method used by setupContexts. Handles errors or redirects that may happen in enter/setup. */ - function handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, enter) { + function handlerEnteredOrUpdated(currentHandlerInfos, handlerInfo, enter, transition) { + var handler = handlerInfo.handler, context = handlerInfo.context; - try { - if (enter && handler.enter) { handler.enter(); } - checkAbort(transition); - - setContext(handler, context); - setQueryParams(handler, handlerInfo.queryParams); - - if (handler.setup) { handler.setup(context, handlerInfo.queryParams); } - checkAbort(transition); - } catch(e) { - if (!(e instanceof Router.TransitionAborted)) { - // Trigger the `error` event starting from this failed handler. - transition.trigger(true, 'error', e, transition, handler); - } - - // Propagate the error so that the transition promise will reject. - throw e; + if (enter && handler.enter) { handler.enter(transition); } + if (transition && transition.isAborted) { + throw new TransitionAborted(); } - currentHandlerInfos.push(handlerInfo); - } - - - /** - @private - - Iterates over an array of `HandlerInfo`s, passing the handler - and context into the callback. + handler.context = context; + if (handler.contextDidChange) { handler.contextDidChange(); } - @param {Array[HandlerInfo]} handlerInfos - @param {Function(Object, Object)} callback - */ - function eachHandler(handlerInfos, callback) { - for (var i=0, l=handlerInfos.length; i= 0; --i) { + var handlerInfo = handlerInfos[i]; + merge(params, handlerInfo.params); + if (handlerInfo.handler.inaccessibleByURL) { + urlMethod = null; + } } - var eventWasHandled = false; - - for (var i=handlerInfos.length-1; i>=0; i--) { - var handlerInfo = handlerInfos[i], - handler = handlerInfo.handler; + if (urlMethod) { + params.queryParams = state.queryParams; + var url = router.recognizer.generate(handlerName, params); - if (handler.events && handler.events[name]) { - if (handler.events[name].apply(handler, args) === true) { - eventWasHandled = true; - } else { - return; + if (urlMethod === 'replaceQuery') { + if (url !== inputUrl) { + router.replaceURL(url); } + } else if (urlMethod === 'replace') { + router.replaceURL(url); + } else { + router.updateURL(url); } } - - if (!eventWasHandled && !ignoreFailure) { - throw new Error("Nothing handled the event '" + name + "'."); - } } - function setContext(handler, context) { - handler.context = context; - if (handler.contextDidChange) { handler.contextDidChange(); } - } + /** + @private - function setQueryParams(handler, queryParams) { - handler.queryParams = queryParams; - if (handler.queryParamsDidChange) { handler.queryParamsDidChange(); } - } + Updates the URL (if necessary) and calls `setupContexts` + to update the router's array of `currentHandlerInfos`. + */ + function finalizeTransition(transition, newState) { + try { + log(transition.router, transition.sequence, "Resolved all models on destination route; finalizing transition."); - /** - @private + var router = transition.router, + handlerInfos = newState.handlerInfos, + seq = transition.sequence; - Extracts query params from the end of an array - **/ + // Run all the necessary enter/setup/exit hooks + setupContexts(router, newState, transition); - function extractQueryParams(array) { - var len = (array && array.length), head, queryParams; + // Check if a redirect occurred in enter/setup + if (transition.isAborted) { + // TODO: cleaner way? distinguish b/w targetHandlerInfos? + router.state.handlerInfos = router.currentHandlerInfos; + return reject(logAbort(transition)); + } - if(len && len > 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { - queryParams = array[len - 1].queryParams; - head = slice.call(array, 0, len - 1); - return [head, queryParams]; - } else { - return [array, null]; - } - } + updateURL(transition, newState, transition.intent.url); - function performIntermediateTransition(router, recogHandlers, matchPointResults) { + transition.isActive = false; + router.activeTransition = null; - var handlerInfos = generateHandlerInfos(router, recogHandlers); - for (var i = 0; i < handlerInfos.length; ++i) { - var handlerInfo = handlerInfos[i]; - handlerInfo.context = matchPointResults.providedModels[handlerInfo.name]; - } + trigger(router, router.currentHandlerInfos, true, ['didTransition']); - var stubbedTransition = { - router: router, - isAborted: false - }; + if (router.didTransition) { + router.didTransition(router.currentHandlerInfos); + } + + log(router, transition.sequence, "TRANSITION COMPLETE."); + + // Resolve with the final handler. + return handlerInfos[handlerInfos.length - 1].handler; + } catch(e) { + if (!(e instanceof TransitionAborted)) { + //var erroneousHandler = handlerInfos.pop(); + var infos = transition.state.handlerInfos; + transition.trigger(true, 'error', e, transition, infos[infos.length-1]); + transition.abort(); + } - setupContexts(stubbedTransition, handlerInfos); + throw e; + } } /** @private - Creates, begins, and returns a Transition. - */ - function performTransition(router, recogHandlers, providedModelsArray, params, queryParams, data, isIntermediate) { + Begins and returns a Transition based on the provided + arguments. Accepts arguments in the form of both URL + transitions and named transitions. - var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params, queryParams), - targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false, - currentHandlerInfos = router.currentHandlerInfos; + @param {Router} router + @param {Array[Object]} args arguments passed to transitionTo, + replaceWith, or handleURL + */ + function doTransition(router, args, isIntermediate) { + // Normalize blank transitions to root URL transitions. + var name = args[0] || '/'; - if (isIntermediate) { - return performIntermediateTransition(router, recogHandlers, matchPointResults); + var lastArg = args[args.length-1]; + var queryParams = {}; + if (lastArg && lastArg.hasOwnProperty('queryParams')) { + queryParams = pop.call(args).queryParams; } - // Check if there's already a transition underway. - if (router.activeTransition) { - if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray, queryParams)) { - return router.activeTransition; - } - router.activeTransition.abort(); - wasTransitioning = true; - } + var intent; + if (args.length === 0) { - var deferred = RSVP.defer(), - transition = new Transition(router, deferred.promise); - - transition.targetName = targetName; - transition.providedModels = matchPointResults.providedModels; - transition.providedModelsArray = providedModelsArray; - transition.params = matchPointResults.params; - transition.data = data || {}; - transition.queryParams = queryParams; - transition.pivotHandler = matchPointResults.pivotHandler; - router.activeTransition = transition; - - var handlerInfos = generateHandlerInfos(router, recogHandlers); - transition.handlerInfos = handlerInfos; - - // Fire 'willTransition' event on current handlers, but don't fire it - // if a transition was already underway. - if (!wasTransitioning) { - trigger(router, currentHandlerInfos, true, ['willTransition', transition]); - } + log(router, "Updating query params"); - log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); - validateEntry(transition, matchPointResults.matchPoint, matchPointResults.handlerParams) - .then(transitionSuccess, transitionFailure); - - return transition; + // A query param update is really just a transition + // into the route you're already on. + var handlerInfos = router.state.handlerInfos; + intent = new NamedTransitionIntent({ + name: handlerInfos[handlerInfos.length - 1].name, + contexts: [], + queryParams: queryParams + }); - function transitionSuccess() { - checkAbort(transition); + } else if (name.charAt(0) === '/') { - try { - finalizeTransition(transition, handlerInfos); + log(router, "Attempting URL transition to " + name); + intent = new URLTransitionIntent({ url: name }); - // currentHandlerInfos was updated in finalizeTransition - trigger(router, router.currentHandlerInfos, true, ['didTransition']); + } else { - if (router.didTransition) { - router.didTransition(handlerInfos); - } + log(router, "Attempting transition to " + name); + intent = new NamedTransitionIntent({ + name: args[0], + contexts: slice.call(args, 1), + queryParams: queryParams + }); + } - log(router, transition.sequence, "TRANSITION COMPLETE."); + return router.transitionByIntent(intent, isIntermediate); + } - // Resolve with the final handler. - transition.isActive = false; - deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); - } catch(e) { - deferred.reject(e); - } + function handlerInfosEqual(handlerInfos, otherHandlerInfos) { + if (handlerInfos.length !== otherHandlerInfos.length) { + return false; + } - // Don't nullify if another transition is underway (meaning - // there was a transition initiated with enter/setup). - if (!transition.isAborted) { - router.activeTransition = null; + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + if (handlerInfos[i] !== otherHandlerInfos[i]) { + return false; } } + return true; + } - function transitionFailure(reason) { - deferred.reject(reason); - } + function finalizeQueryParamChange(router, resolvedHandlers, newQueryParams) { + // We fire a finalizeQueryParamChange event which + // gives the new route hierarchy a chance to tell + // us which query params it's consuming and what + // their final values are. If a query param is + // no longer consumed in the final route hierarchy, + // its serialized segment will be removed + // from the URL. + var finalQueryParams = {}; + trigger(router, resolvedHandlers, true, ['finalizeQueryParamChange', newQueryParams, finalQueryParams]); + return finalQueryParams; } - /** - @private + __exports__.Router = Router; + }); +define("router/transition-intent", + ["./utils","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var merge = __dependency1__.merge; - Accepts handlers in Recognizer format, either returned from - recognize() or handlersFor(), and returns unified - `HandlerInfo`s. - */ - function generateHandlerInfos(router, recogHandlers) { - var handlerInfos = []; - for (var i = 0, len = recogHandlers.length; i < len; ++i) { - var handlerObj = recogHandlers[i], - isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length); - - var handlerInfo = { - isDynamic: !!isDynamic, - name: handlerObj.handler, - handler: router.getHandler(handlerObj.handler) - }; - if(handlerObj.queryParams) { - handlerInfo.queryParams = handlerObj.queryParams; - } - handlerInfos.push(handlerInfo); + function TransitionIntent(props) { + if (props) { + merge(this, props); } - return handlerInfos; + this.data = this.data || {}; } - /** - @private - */ - function transitionsIdentical(oldTransition, targetName, providedModelsArray, queryParams) { + TransitionIntent.prototype.applyToState = function(oldState) { + // Default TransitionIntent is a no-op. + return oldState; + }; - if (oldTransition.targetName !== targetName) { return false; } + __exports__.TransitionIntent = TransitionIntent; + }); +define("router/transition-intent/named-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var UnresolvedHandlerInfoByObject = __dependency3__.UnresolvedHandlerInfoByObject; + var isParam = __dependency4__.isParam; + var forEach = __dependency4__.forEach; + var extractQueryParams = __dependency4__.extractQueryParams; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function NamedTransitionIntent(props) { + TransitionIntent.call(this, props); + } - var oldModels = oldTransition.providedModelsArray; - if (oldModels.length !== providedModelsArray.length) { return false; } + NamedTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + NamedTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler, isIntermediate) { - for (var i = 0, len = oldModels.length; i < len; ++i) { - if (oldModels[i] !== providedModelsArray[i]) { return false; } - } + var partitionedArgs = extractQueryParams([this.name].concat(this.contexts)), + pureArgs = partitionedArgs[0], + queryParams = partitionedArgs[1], + handlers = recognizer.handlersFor(pureArgs[0]); + //handlerInfos = generateHandlerInfosWithQueryParams({}, handlers, queryParams); - if(!queryParamsEqual(oldTransition.queryParams, queryParams)) { - return false; - } + var targetRouteName = handlers[handlers.length-1].handler; - return true; - } + return this.applyToHandlers(oldState, handlers, getHandler, targetRouteName, isIntermediate); + }; - /** - @private + NamedTransitionIntent.prototype.applyToHandlers = function(oldState, handlers, getHandler, targetRouteName, isIntermediate, checkingIfActive) { - Updates the URL (if necessary) and calls `setupContexts` - to update the router's array of `currentHandlerInfos`. - */ - function finalizeTransition(transition, handlerInfos) { + var newState = new TransitionState(); + var objects = this.contexts.slice(0); - log(transition.router, transition.sequence, "Validation succeeded, finalizing transition;"); + var invalidateIndex = handlers.length; + var nonDynamicIndexes = []; - var router = transition.router, - seq = transition.sequence, - handlerName = handlerInfos[handlerInfos.length - 1].name, - urlMethod = transition.urlMethod, - i; + for (var i = handlers.length - 1; i >= 0; --i) { + var result = handlers[i]; + var name = result.handler; + var handler = getHandler(name); - // Collect params for URL. - var objects = [], providedModels = transition.providedModelsArray.slice(); - for (i = handlerInfos.length - 1; i>=0; --i) { - var handlerInfo = handlerInfos[i]; - if (handlerInfo.isDynamic) { - var providedModel = providedModels.pop(); - objects.unshift(isParam(providedModel) ? providedModel.toString() : handlerInfo.context); + var oldHandlerInfo = oldState.handlerInfos[i]; + var newHandlerInfo = null; + + if (result.names.length > 0) { + newHandlerInfo = this.getHandlerInfoForDynamicSegment(name, handler, result.names, objects, oldHandlerInfo, targetRouteName); + } else { + // This route has no dynamic segment. + // Therefore treat as a param-based handlerInfo + // with empty params. This will cause the `model` + // hook to be called with empty params, which is desirable. + newHandlerInfo = this.createParamHandlerInfo(name, handler, result.names, objects, oldHandlerInfo); + nonDynamicIndexes.unshift(i); } - if (handlerInfo.handler.inaccessibleByURL) { - urlMethod = null; + if (checkingIfActive) { + // If we're performing an isActive check, we want to + // serialize URL params with the provided context, but + // ignore mismatches between old and new context. + newHandlerInfo = newHandlerInfo.becomeResolved(null, newHandlerInfo.context); + var oldContext = oldHandlerInfo && oldHandlerInfo.context; + if (result.names.length > 0 && newHandlerInfo.context === oldContext) { + // If contexts match in isActive test, assume params also match. + // This allows for flexibility in not requiring that every last + // handler provide a `serialize` method + newHandlerInfo.params = oldHandlerInfo && oldHandlerInfo.params; + } + newHandlerInfo.context = oldContext; + } + + var handlerToUse = oldHandlerInfo; + if (newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + invalidateIndex = i; + handlerToUse = newHandlerInfo; } + + if (isIntermediate && !checkingIfActive) { + handlerToUse = handlerToUse.becomeResolved(null, handlerToUse.context); + } + + newState.handlerInfos.unshift(handlerToUse); } - var newQueryParams = {}; - for (i = handlerInfos.length - 1; i>=0; --i) { - merge(newQueryParams, handlerInfos[i].queryParams); + if (objects.length > 0) { + throw new Error("More context objects were passed than there are dynamic segments for the route: " + targetRouteName); } - router.currentQueryParams = newQueryParams; + if (!isIntermediate) { + this.invalidateNonDynamicHandlers(newState.handlerInfos, nonDynamicIndexes, invalidateIndex); + } - var params = paramsForHandler(router, handlerName, objects, transition.queryParams); + merge(newState.queryParams, oldState.queryParams); + merge(newState.queryParams, this.queryParams || {}); - router.currentParams = params; + return newState; + }; - if (urlMethod) { - var url = router.recognizer.generate(handlerName, params); + NamedTransitionIntent.prototype.invalidateNonDynamicHandlers = function(handlerInfos, indexes, invalidateIndex) { + forEach(indexes, function(i) { + if (i >= invalidateIndex) { + var handlerInfo = handlerInfos[i]; + handlerInfos[i] = new UnresolvedHandlerInfoByParam({ + name: handlerInfo.name, + handler: handlerInfo.handler, + params: {} + }); + } + }); + }; - if (urlMethod === 'replace') { - router.replaceURL(url); + NamedTransitionIntent.prototype.getHandlerInfoForDynamicSegment = function(name, handler, names, objects, oldHandlerInfo, targetRouteName) { + + var numNames = names.length; + var objectToUse; + if (objects.length > 0) { + + // Use the objects provided for this transition. + objectToUse = objects[objects.length - 1]; + if (isParam(objectToUse)) { + return this.createParamHandlerInfo(name, handler, names, objects, oldHandlerInfo); } else { - // Assume everything else is just a URL update for now. - router.updateURL(url); + objects.pop(); } + } else if (oldHandlerInfo && oldHandlerInfo.name === name) { + // Reuse the matching oldHandlerInfo + return oldHandlerInfo; + } else { + // Ideally we should throw this error to provide maximal + // information to the user that not enough context objects + // were provided, but this proves too cumbersome in Ember + // in cases where inner template helpers are evaluated + // before parent helpers un-render, in which cases this + // error somewhat prematurely fires. + //throw new Error("Not enough context objects were provided to complete a transition to " + targetRouteName + ". Specifically, the " + name + " route needs an object that can be serialized into its dynamic URL segments [" + names.join(', ') + "]"); + return oldHandlerInfo; } - setupContexts(transition, handlerInfos); + return new UnresolvedHandlerInfoByObject({ + name: name, + handler: handler, + context: objectToUse, + names: names + }); + }; + + NamedTransitionIntent.prototype.createParamHandlerInfo = function(name, handler, names, objects, oldHandlerInfo) { + var params = {}; + + // Soak up all the provided string/numbers + var numNames = names.length; + while (numNames--) { + + // Only use old params if the names match with the new handler + var oldParams = (oldHandlerInfo && name === oldHandlerInfo.name && oldHandlerInfo.params) || {}; + + var peek = objects[objects.length - 1]; + var paramName = names[numNames]; + if (isParam(peek)) { + params[paramName] = "" + objects.pop(); + } else { + // If we're here, this means only some of the params + // were string/number params, so try and use a param + // value from a previous handler. + if (oldParams.hasOwnProperty(paramName)) { + params[paramName] = oldParams[paramName]; + } else { + throw new Error("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments for route " + name); + } + } + } + + return new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: params + }); + }; + + __exports__.NamedTransitionIntent = NamedTransitionIntent; + }); +define("router/transition-intent/refresh-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var extractQueryParams = __dependency4__.extractQueryParams; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function RefreshTransitionIntent(props) { + TransitionIntent.call(this, props); } - /** - @private + RefreshTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + RefreshTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler, isIntermediate) { - Internal function used to construct the chain of promises used - to validate a transition. Wraps calls to `beforeModel`, `model`, - and `afterModel` in promises, and checks for redirects/aborts - between each. - */ - function validateEntry(transition, matchPoint, handlerParams) { + var pivotHandlerFound = false; + var newState = new TransitionState(); - var handlerInfos = transition.handlerInfos, - index = transition.resolveIndex; + var oldHandlerInfos = oldState.handlerInfos; + for (var i = 0, len = oldHandlerInfos.length; i < len; ++i) { + var handlerInfo = oldHandlerInfos[i]; + if (handlerInfo.handler === this.pivotHandler) { + pivotHandlerFound = true; + } - if (index === handlerInfos.length) { - // No more contexts to resolve. - return RSVP.resolve(transition.resolvedModels); + if (pivotHandlerFound) { + newState.handlerInfos.push(new UnresolvedHandlerInfoByParam({ + name: handlerInfo.name, + handler: handlerInfo.handler, + params: handlerInfo.params || {} + })); + } else { + newState.handlerInfos.push(handlerInfo); + } } - var router = transition.router, - handlerInfo = handlerInfos[index], - handler = handlerInfo.handler, - handlerName = handlerInfo.name, - seq = transition.sequence; - - if (index < matchPoint) { - log(router, seq, handlerName + ": using context from already-active handler"); - - // We're before the match point, so don't run any hooks, - // just use the already resolved context from the handler. - transition.resolvedModels[handlerInfo.name] = - transition.providedModels[handlerInfo.name] || - handlerInfo.handler.context; - return proceed(); + merge(newState.queryParams, oldState.queryParams); + if (this.queryParams) { + merge(newState.queryParams, this.queryParams); } - transition.trigger(true, 'willResolveModel', transition, handler); + return newState; + }; + + __exports__.RefreshTransitionIntent = RefreshTransitionIntent; + }); +define("router/transition-intent/url-transition-intent", + ["../transition-intent","../transition-state","../handler-info","../utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __exports__) { + "use strict"; + var TransitionIntent = __dependency1__.TransitionIntent; + var TransitionState = __dependency2__.TransitionState; + var UnresolvedHandlerInfoByParam = __dependency3__.UnresolvedHandlerInfoByParam; + var oCreate = __dependency4__.oCreate; + var merge = __dependency4__.merge; + + function URLTransitionIntent(props) { + TransitionIntent.call(this, props); + } + + URLTransitionIntent.prototype = oCreate(TransitionIntent.prototype); + URLTransitionIntent.prototype.applyToState = function(oldState, recognizer, getHandler) { + var newState = new TransitionState(); - return RSVP.resolve().then(handleAbort) - .then(beforeModel) - .then(handleAbort) - .then(model) - .then(handleAbort) - .then(afterModel) - .then(handleAbort) - .then(null, handleError) - .then(proceed); + var results = recognizer.recognize(this.url), + queryParams = {}, + i, len; - function handleAbort(result) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - return RSVP.reject(new Router.TransitionAborted()); + if (!results) { + throw new UnrecognizedURLError(this.url); + } + + var statesDiffer = false; + + for (i = 0, len = results.length; i < len; ++i) { + var result = results[i]; + var name = result.handler; + var handler = getHandler(name); + + if (handler.inaccessibleByURL) { + throw new UnrecognizedURLError(this.url); } - return result; + var newHandlerInfo = new UnresolvedHandlerInfoByParam({ + name: name, + handler: handler, + params: result.params + }); + + var oldHandlerInfo = oldState.handlerInfos[i]; + if (statesDiffer || newHandlerInfo.shouldSupercede(oldHandlerInfo)) { + statesDiffer = true; + newState.handlerInfos[i] = newHandlerInfo; + } else { + newState.handlerInfos[i] = oldHandlerInfo; + } } - function handleError(reason) { - if (reason instanceof Router.TransitionAborted || transition.isAborted) { - // if the transition was aborted and *no additional* error was thrown, - // reject with the Router.TransitionAborted instance - return RSVP.reject(reason); + merge(newState.queryParams, results.queryParams); + + return newState; + }; + + /** + Promise reject reasons passed to promise rejection + handlers for failed transitions. + */ + function UnrecognizedURLError(message) { + this.message = (message || "UnrecognizedURLError"); + this.name = "UnrecognizedURLError"; + } + + __exports__.URLTransitionIntent = URLTransitionIntent; + }); +define("router/transition-state", + ["./handler-info","./utils","rsvp","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var ResolvedHandlerInfo = __dependency1__.ResolvedHandlerInfo; + var forEach = __dependency2__.forEach; + var resolve = __dependency3__.resolve; + + function TransitionState(other) { + this.handlerInfos = []; + this.queryParams = {}; + this.params = {}; + } + + TransitionState.prototype = { + handlerInfos: null, + queryParams: null, + params: null, + + resolve: function(async, shouldContinue, payload) { + + // First, calculate params for this state. This is useful + // information to provide to the various route hooks. + var params = this.params; + forEach(this.handlerInfos, function(handlerInfo) { + params[handlerInfo.name] = handlerInfo.params || {}; + }); + + payload = payload || {}; + payload.resolveIndex = 0; + + var currentState = this; + var wasAborted = false; + + // The prelude RSVP.resolve() asyncs us into the promise land. + return resolve().then(resolveOneHandlerInfo).catch(handleError); + + function innerShouldContinue() { + return resolve(shouldContinue()).catch(function(reason) { + // We distinguish between errors that occurred + // during resolution (e.g. beforeModel/model/afterModel), + // and aborts due to a rejecting promise from shouldContinue(). + wasAborted = true; + throw reason; + }); } - // otherwise, we're here because of a different error - transition.abort(); + function handleError(error) { + // This is the only possible + // reject value of TransitionState#resolve + throw { + error: error, + handlerWithError: currentState.handlerInfos[payload.resolveIndex].handler, + wasAborted: wasAborted, + state: currentState + }; + } - log(router, seq, handlerName + ": handling error: " + reason); + function proceed(resolvedHandlerInfo) { + // Swap the previously unresolved handlerInfo with + // the resolved handlerInfo + currentState.handlerInfos[payload.resolveIndex++] = resolvedHandlerInfo; + + // Call the redirect hook. The reason we call it here + // vs. afterModel is so that redirects into child + // routes don't re-run the model hooks for this + // already-resolved route. + var handler = resolvedHandlerInfo.handler; + if (handler && handler.redirect) { + handler.redirect(resolvedHandlerInfo.context, payload); + } - // An error was thrown / promise rejected, so fire an - // `error` event from this handler info up to root. - transition.trigger(true, 'error', reason, transition, handlerInfo.handler); + // Proceed after ensuring that the redirect hook + // didn't abort this transition by transitioning elsewhere. + return innerShouldContinue().then(resolveOneHandlerInfo); + } - // Propagate the original error. - return RSVP.reject(reason); + function resolveOneHandlerInfo() { + if (payload.resolveIndex === currentState.handlerInfos.length) { + // This is is the only possible + // fulfill value of TransitionState#resolve + return { + error: null, + state: currentState + }; + } + + var handlerInfo = currentState.handlerInfos[payload.resolveIndex]; + + return handlerInfo.resolve(async, innerShouldContinue, payload) + .then(proceed); + } + }, + + getResolvedHandlerInfos: function() { + var resolvedHandlerInfos = []; + var handlerInfos = this.handlerInfos; + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + var handlerInfo = handlerInfos[i]; + if (!(handlerInfo instanceof ResolvedHandlerInfo)) { + break; + } + resolvedHandlerInfos.push(handlerInfo); + } + return resolvedHandlerInfos; } + }; + + __exports__.TransitionState = TransitionState; + }); +define("router/transition", + ["rsvp","./handler-info","./utils","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var reject = __dependency1__.reject; + var resolve = __dependency1__.resolve; + var ResolvedHandlerInfo = __dependency2__.ResolvedHandlerInfo; + var trigger = __dependency3__.trigger; + var slice = __dependency3__.slice; + var log = __dependency3__.log; - function beforeModel() { + /** + @private - log(router, seq, handlerName + ": calling beforeModel hook"); + A Transition is a thennable (a promise-like object) that represents + an attempt to transition to another route. It can be aborted, either + explicitly via `abort` or by attempting another transition while a + previous one is still underway. An aborted transition can also + be `retry()`d later. + */ + function Transition(router, intent, state, error) { + var transition = this; + this.state = state || router.state; + this.intent = intent; + this.router = router; + this.data = this.intent && this.intent.data || {}; + this.resolvedModels = {}; + this.queryParams = {}; - var args; + if (error) { + this.promise = reject(error); + return; + } - if (handlerInfo.queryParams) { - args = [handlerInfo.queryParams, transition]; - } else { - args = [transition]; + if (state) { + this.params = state.params; + this.queryParams = state.queryParams; + + var len = state.handlerInfos.length; + if (len) { + this.targetName = state.handlerInfos[state.handlerInfos.length-1].name; } - var p = handler.beforeModel && handler.beforeModel.apply(handler, args); - return (p instanceof Transition) ? null : p; + for (var i = 0; i < len; ++i) { + var handlerInfo = state.handlerInfos[i]; + if (!(handlerInfo instanceof ResolvedHandlerInfo)) { + break; + } + this.pivotHandler = handlerInfo.handler; + } + + this.sequence = Transition.currentSequence++; + this.promise = state.resolve(router.async, checkForAbort, this).catch(function(result) { + if (result.wasAborted) { + throw logAbort(transition); + } else { + transition.trigger('error', result.error, transition, result.handlerWithError); + transition.abort(); + throw result.error; + } + }); + } else { + this.promise = resolve(this.state); + this.params = {}; } - function model() { - log(router, seq, handlerName + ": resolving model"); - var p = getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); - return (p instanceof Transition) ? null : p; + function checkForAbort() { + if (transition.isAborted) { + return reject(); + } } + } - function afterModel(context) { + Transition.currentSequence = 0; - log(router, seq, handlerName + ": calling afterModel hook"); + Transition.prototype = { + targetName: null, + urlMethod: 'update', + intent: null, + params: null, + pivotHandler: null, + resolveIndex: 0, + handlerInfos: null, + resolvedModels: null, + isActive: true, + state: null, - // Pass the context and resolved parent contexts to afterModel, but we don't - // want to use the value returned from `afterModel` in any way, but rather - // always resolve with the original `context` object. + /** + @public - transition.resolvedModels[handlerInfo.name] = context; + The Transition's internal promise. Calling `.then` on this property + is that same as calling `.then` on the Transition object itself, but + this property is exposed for when you want to pass around a + Transition's promise, but not the Transition object itself, since + Transition object can be externally `abort`ed, while the promise + cannot. + */ + promise: null, - var args; + /** + @public - if (handlerInfo.queryParams) { - args = [context, handlerInfo.queryParams, transition]; + Custom state can be stored on a Transition's `data` object. + This can be useful for decorating a Transition within an earlier + hook and shared with a later hook. Properties set on `data` will + be copied to new transitions generated by calling `retry` on this + transition. + */ + data: null, + + /** + @public + + A standard promise hook that resolves if the transition + succeeds and rejects if it fails/redirects/aborts. + + Forwards to the internal `promise` property which you can + use in situations where you want to pass around a thennable, + but not the Transition itself. + + @param {Function} success + @param {Function} failure + */ + then: function(success, failure) { + return this.promise.then(success, failure); + }, + + /** + @public + + Aborts the Transition. Note you can also implicitly abort a transition + by initiating another transition while a previous one is underway. + */ + abort: function() { + if (this.isAborted) { return this; } + log(this.router, this.sequence, this.targetName + ": transition was aborted"); + this.isAborted = true; + this.isActive = false; + this.router.activeTransition = null; + return this; + }, + + /** + @public + + Retries a previously-aborted transition (making sure to abort the + transition if it's still active). Returns a new transition that + represents the new attempt to transition. + */ + retry: function() { + // TODO: add tests for merged state retry()s + this.abort(); + return this.router.transitionByIntent(this.intent, false); + }, + + /** + @public + + Sets the URL-changing method to be employed at the end of a + successful transition. By default, a new Transition will just + use `updateURL`, but passing 'replace' to this method will + cause the URL to update using 'replaceWith' instead. Omitting + a parameter will disable the URL change, allowing for transitions + that don't update the URL at completion (this is also used for + handleURL, since the URL has already changed before the + transition took place). + + @param {String} method the type of URL-changing method to use + at the end of a transition. Accepted values are 'replace', + falsy values, or any other non-falsy value (which is + interpreted as an updateURL transition). + + @return {Transition} this transition + */ + method: function(method) { + this.urlMethod = method; + return this; + }, + + /** + @public + + Fires an event on the current list of resolved/resolving + handlers within this transition. Useful for firing events + on route hierarchies that haven't fully been entered yet. + + Note: This method is also aliased as `send` + + @param {Boolean} ignoreFailure the name of the event to fire + @param {String} name the name of the event to fire + */ + trigger: function (ignoreFailure) { + var args = slice.call(arguments); + if (typeof ignoreFailure === 'boolean') { + args.shift(); } else { - args = [context, transition]; + // Throw errors on unhandled trigger events by default + ignoreFailure = false; } + trigger(this.router, this.state.handlerInfos.slice(0, this.resolveIndex + 1), ignoreFailure, args); + }, - var p = handler.afterModel && handler.afterModel.apply(handler, args); - return (p instanceof Transition) ? null : p; - } + /** + @public - function proceed() { - log(router, seq, handlerName + ": validation succeeded, proceeding"); + Transitions are aborted and their promises rejected + when redirects occur; this method returns a promise + that will follow any redirects that occur and fulfill + with the value fulfilled by any redirecting transitions + that occur. - handlerInfo.context = transition.resolvedModels[handlerInfo.name]; - transition.resolveIndex++; - return validateEntry(transition, matchPoint, handlerParams); + @return {Promise} a promise that fulfills with the same + value that the final redirecting transition fulfills with + */ + followRedirects: function() { + var router = this.router; + return this.promise.catch(function(reason) { + if (router.activeTransition) { + return router.activeTransition.followRedirects(); + } + throw reason; + }); + }, + + toString: function() { + return "Transition (sequence " + this.sequence + ")"; + }, + + /** + @private + */ + log: function(message) { + log(this.router, this.sequence, message); } - } + }; + + // Alias 'trigger' as 'send' + Transition.prototype.send = Transition.prototype.trigger; /** @private - Throws a TransitionAborted if the provided transition has been aborted. + Logs and returns a TransitionAborted error. */ - function checkAbort(transition) { - if (transition.isAborted) { - log(transition.router, transition.sequence, "detected abort."); - throw new Router.TransitionAborted(); - } + function logAbort(transition) { + log(transition.router, transition.sequence, "detected abort."); + return new TransitionAborted(); } - /** - @private + function TransitionAborted(message) { + this.message = (message || "TransitionAborted"); + this.name = "TransitionAborted"; + } - Encapsulates the logic for whether to call `model` on a route, - or use one of the models provided to `transitionTo`. - */ - function getModel(handlerInfo, transition, handlerParams, needsUpdate) { - var handler = handlerInfo.handler, - handlerName = handlerInfo.name, args; + __exports__.Transition = Transition; + __exports__.logAbort = logAbort; + __exports__.TransitionAborted = TransitionAborted; + }); +define("router/utils", + ["exports"], + function(__exports__) { + "use strict"; + var slice = Array.prototype.slice; - if (!needsUpdate && handler.hasOwnProperty('context')) { - return handler.context; + function merge(hash, other) { + for (var prop in other) { + if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } } + } - if (transition.providedModels.hasOwnProperty(handlerName)) { - var providedModel = transition.providedModels[handlerName]; - return typeof providedModel === 'function' ? providedModel() : providedModel; - } + var oCreate = Object.create || function(proto) { + function F() {} + F.prototype = proto; + return new F(); + }; - if (handlerInfo.queryParams) { - args = [handlerParams || {}, handlerInfo.queryParams, transition]; + /** + @private + + Extracts query params from the end of an array + **/ + function extractQueryParams(array) { + var len = (array && array.length), head, queryParams; + + if(len && len > 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { + queryParams = array[len - 1].queryParams; + head = slice.call(array, 0, len - 1); + return [head, queryParams]; } else { - args = [handlerParams || {}, transition, handlerInfo.queryParams]; + return [array, null]; } - - return handler.model && handler.model.apply(handler, args); } /** @private */ function log(router, sequence, msg) { - if (!router.log) { return; } if (arguments.length === 3) { @@ -1388,28 +1643,22 @@ define("router", } } - /** - @private + function bind(fn, context) { + var boundArgs = arguments; + return function(value) { + var args = slice.call(boundArgs, 2); + args.push(value); + return fn.apply(context, args); + }; + } - Begins and returns a Transition based on the provided - arguments. Accepts arguments in the form of both URL - transitions and named transitions. + function isParam(object) { + return (typeof object === "string" || object instanceof String || typeof object === "number" || object instanceof Number); + } - @param {Router} router - @param {Array[Object]} args arguments passed to transitionTo, - replaceWith, or handleURL - */ - function doTransition(router, args, isIntermediate) { - // Normalize blank transitions to root URL transitions. - var name = args[0] || '/'; - if(args.length === 1 && args[0].hasOwnProperty('queryParams')) { - return createQueryParamTransition(router, args[0], isIntermediate); - } else if (name.charAt(0) === '/') { - return createURLTransition(router, name, isIntermediate); - } else { - return createNamedTransition(router, slice.call(args), isIntermediate); - } + function forEach(array, callback) { + for (var i=0, l=array.length; i=0; i--) { + var handlerInfo = handlerInfos[i], + handler = handlerInfo.handler; + + if (handler.events && handler.events[name]) { + if (handler.events[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; + } + } + } + + if (!eventWasHandled && !ignoreFailure) { + throw new Error("Nothing handled the event '" + name + "'."); + } + } + + + function getChangelist(oldObject, newObject) { + var key; + var results = { + all: {}, + changed: {}, + removed: {} + }; + + merge(results.all, newObject); + + var didChange = false; + + // Calculate removals + for (key in oldObject) { + if (oldObject.hasOwnProperty(key)) { + if (!newObject.hasOwnProperty(key)) { + didChange = true; + results.removed[key] = oldObject[key]; + } + } + } + + // Calculate changes + for (key in newObject) { + if (newObject.hasOwnProperty(key)) { + if (oldObject[key] !== newObject[key]) { + results.changed[key] = newObject[key]; + didChange = true; + } + } + } + + return didChange && results; + } + + __exports__.trigger = trigger; + __exports__.log = log; + __exports__.oCreate = oCreate; + __exports__.merge = merge; + __exports__.extractQueryParams = extractQueryParams; + __exports__.bind = bind; + __exports__.isParam = isParam; + __exports__.forEach = forEach; + __exports__.slice = slice; + __exports__.serialize = serialize; + __exports__.getChangelist = getChangelist; + }); +define("router", + ["./router/router","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Router = __dependency1__.Router; + + __exports__.Router = Router; }); diff --git a/packages/ember-routing/tests/ext/controller_test.js b/packages/ember-routing/tests/ext/controller_test.js new file mode 100644 index 00000000000..0154cefad8b --- /dev/null +++ b/packages/ember-routing/tests/ext/controller_test.js @@ -0,0 +1,81 @@ + +var controller, container; + +if (Ember.FEATURES.isEnabled("query-params-new")) { + + module("Ember.Controller query param support", { + setup: function() { + + container = new Ember.Container(); + + container.register('controller:thing', Ember.Controller.extend({ + _queryParamScope: 'thing', + queryParams: ['foo', 'bar:baz'], + foo: 'imafoo', + bar: 'imabar' + })); + + controller = container.lookup('controller:thing'); + }, + + teardown: function() { + } + }); + + test("setting a query param property on an inactive controller does nothing", function() { + expect(0); + + controller.target = { + transitionTo: function(params) { + ok(false, "should not get here"); + } + }; + + Ember.run(controller, 'set', 'foo', 'newfoo'); + }); + + test("setting a query param property fires off a transition", function() { + expect(1); + + controller.target = { + transitionTo: function(params) { + deepEqual(params, { queryParams: { 'thing[foo]': 'newfoo' } }, "transitionTo is called"); + } + }; + + Ember.run(controller, '_activateQueryParamObservers'); + Ember.run(controller, 'set', 'foo', 'newfoo'); + }); + + test("setting multiple query param properties fires off a single transition", function() { + expect(1); + + controller.target = { + transitionTo: function(params) { + deepEqual(params, { queryParams: { 'thing[foo]': 'newfoo', 'baz': 'newbar' } }, "single transitionTo is called"); + } + }; + + Ember.run(controller, '_activateQueryParamObservers'); + + Ember.run(function() { + controller.set('foo', 'newfoo'); + controller.set('bar', 'newbar'); + }); + }); + + test("changing a prop on a deactivated controller does nothing", function() { + expect(1); + + controller.target = { + transitionTo: function(params) { + deepEqual(params, { queryParams: { 'thing[foo]': 'newfoo' } }, "transitionTo is called"); + } + }; + + Ember.run(controller, '_activateQueryParamObservers'); + Ember.run(controller, 'set', 'foo', 'newfoo'); + Ember.run(controller, '_deactivateQueryParamObservers'); + Ember.run(controller, 'set', 'foo', 'nonono'); + }); +} diff --git a/packages/ember/tests/helpers/link_to_test.js b/packages/ember/tests/helpers/link_to_test.js index 5e934c4302c..31c5d7bcdc1 100644 --- a/packages/ember/tests/helpers/link_to_test.js +++ b/packages/ember/tests/helpers/link_to_test.js @@ -450,10 +450,7 @@ test("The {{link-to}} helper accepts string/numeric arguments", function() { }); test("The {{link-to}} helper unwraps controllers", function() { - // The serialize hook is called thrice: once to generate the href for the - // link, once to generate the URL when the link is clicked, and again - // when the URL changes to check if query params have been updated - expect(3); + expect(2); Router.map(function() { this.route('filter', { path: '/filters/:filter' }); @@ -661,235 +658,6 @@ test("The {{link-to}} helper refreshes href element when one of params changes", equal(Ember.$('#post', '#qunit-fixture').attr('href'), '#', 'href attr becomes # when one of the arguments in nullified'); }); -if (Ember.FEATURES.isEnabled("query-params")) { - test("The {{link-to}} helper supports query params", function() { - expect(66); - - Router.map(function() { - this.route("about", {queryParams: ['section']}); - this.resource("items", { queryParams: ['sort', 'direction'] }); - }); - - Ember.TEMPLATES.about = Ember.Handlebars.compile("

About

{{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'about' section='intro' id='about-link-with-qp'}}Intro{{/link-to}}{{#link-to 'about' section=false id='about-clear-qp'}}Intro{{/link-to}}{{#if isIntro}}

Here is the intro

{{/if}}"); - Ember.TEMPLATES.items = Ember.Handlebars.compile("

Items

{{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'items' id='items-link' direction=otherDirection}}Sort{{/link-to}} {{#link-to 'items' id='items-sort-link' sort='name'}}Sort Ascending{{/link-to}} {{#link-to 'items' id='items-clear-link' queryParams=false}}Clear Query Params{{/link-to}}"); - - App.AboutRoute = Ember.Route.extend({ - setupController: function(controller, context, queryParams) { - controller.set('isIntro', queryParams.section === 'intro'); - } - }); - - App.ItemsRoute = Ember.Route.extend({ - setupController: function (controller, context, queryParams) { - controller.set('currentDirection', queryParams.direction || 'asc'); - } - }); - - var shouldNotHappen = function(error) { - console.error(error.stack); - ok(false, "this .then handler should not be called: " + error.message); - }; - - App.ItemsController = Ember.Controller.extend({ - currentDirection: 'asc', - otherDirection: Ember.computed(function () { - if (get(this, 'currentDirection') === 'asc') { - return 'desc'; - } else { - return 'asc'; - } - }).property('currentDirection') - }); - - bootApplication(); - - Ember.run(function() { - router.handleURL("/about"); - }); - - equal(Ember.$('h1:contains(About)', '#qunit-fixture').length, 1, "The about template was rendered"); - equal(normalizeUrl(Ember.$('#about-link').attr('href')), '/about', "The about link points back at /about"); - shouldBeActive('#about-link'); - equal(normalizeUrl(Ember.$('#about-link-with-qp').attr('href')), '/about?section=intro', "The helper accepts query params"); - shouldNotBeActive('#about-link-with-qp'); - equal(normalizeUrl(Ember.$('#about-clear-qp').attr('href')), '/about', "Falsy query params work"); - shouldBeActive('#about-clear-qp'); - - - Ember.run(function() { - Ember.$('#about-link-with-qp', '#qunit-fixture').click(); - }); - - equal(router.get('url'), "/about?section=intro", "Clicking link-to updates the url"); - equal(Ember.$('p', '#qunit-fixture').text(), "Here is the intro", "Query param is applied to controller"); - equal(normalizeUrl(Ember.$('#about-link').attr('href')), '/about?section=intro', "The params have stuck"); - shouldBeActive('#about-link'); - equal(normalizeUrl(Ember.$('#about-link-with-qp').attr('href')), '/about?section=intro', "The helper accepts query params"); - shouldBeActive('#about-link-with-qp'); - equal(normalizeUrl(Ember.$('#about-clear-qp').attr('href')), '/about', "Falsy query params clear querystring"); - shouldNotBeActive('#about-clear-qp'); - - - Ember.run(function() { - router.transitionTo("/about"); - }); - - equal(router.get('url'), "/about", "handleURL clears query params"); - - Ember.run(function() { - router.transitionTo("/items"); - }); - - var controller = container.lookup('controller:items'); - - equal(controller.get('currentDirection'), 'asc', "Current direction is asc"); - equal(controller.get('otherDirection'), 'desc', "Other direction is desc"); - - equal(Ember.$('h1:contains(Items)', '#qunit-fixture').length, 1, "The items template was rendered"); - equal(normalizeUrl(Ember.$('#about-link').attr('href')), '/about', "The params have not stuck"); - shouldNotBeActive('#about-link'); - equal(normalizeUrl(Ember.$('#items-link').attr('href')), '/items?direction=desc', "Params can come from bindings"); - shouldNotBeActive('#items-link'); - equal(normalizeUrl(Ember.$('#items-clear-link').attr('href')), '/items', "Can clear query params"); - shouldBeActive('#items-clear-link'); - - Ember.run(function() { - Ember.$('#items-link', '#qunit-fixture').click(); - }); - - equal(router.get('url'), "/items?direction=desc", "Clicking link-to should direct to the correct url"); - equal(controller.get('currentDirection'), 'desc', "Current direction is desc"); - equal(controller.get('otherDirection'), 'asc', "Other direction is asc"); - - equal(normalizeUrl(Ember.$('#items-sort-link').attr('href')), '/items?direction=desc&sort=name', "link-to href correctly merges query parmas"); - shouldNotBeActive('#items-sort-link'); - - equal(normalizeUrl(Ember.$('#items-clear-link').attr('href')), '/items', "Can clear query params"); - shouldNotBeActive('#items-clear-link'); - - Ember.run(function() { - Ember.$('#items-sort-link', '#qunit-fixture').click(); - }); - - - equal(router.get('url'), "/items?sort=name&direction=desc", "The params should be merged correctly"); - equal(controller.get('currentDirection'), 'desc', "Current direction is desc"); - equal(controller.get('otherDirection'), 'asc', "Other direction is asc"); - - equal(normalizeUrl(Ember.$('#items-sort-link').attr('href')), "/items?sort=name&direction=desc", "link-to href correctly merges query parmas"); - shouldBeActive('#items-sort-link'); - - equal(normalizeUrl(Ember.$('#items-link').attr('href')), "/items?sort=name&direction=asc", "Params can come from bindings"); - shouldNotBeActive('#items-link'); - - equal(normalizeUrl(Ember.$('#items-clear-link').attr('href')), '/items', "Can clear query params"); - shouldNotBeActive('#items-clear-link'); - - Ember.run(function() { - controller.set('currentDirection', 'asc'); - }); - - equal(controller.get('currentDirection'), 'asc', "Current direction is asc"); - equal(controller.get('otherDirection'), 'desc', "Other direction is desc"); - - equal(normalizeUrl(Ember.$('#items-link').attr('href')), "/items?sort=name&direction=desc", "Params are updated when bindings change"); - shouldBeActive('#items-link'); - equal(normalizeUrl(Ember.$('#items-sort-link').attr('href')), '/items?sort=name&direction=desc', "link-to href correctly merges query params when other params change"); - shouldBeActive('#items-sort-link'); - - Ember.run(function() { - Ember.$('#items-sort-link', '#qunit-fixture').click(); - }); - - equal(router.get('url'), '/items?sort=name&direction=desc', "Clicking the active link should preserve the url"); - shouldBeActive('#items-sort-link'); - - - var promise, next; - - stop(); - - Ember.run(function () { - promise = router.transitionTo({queryParams: {sort: false}}); - }); - - next = function () { - equal(router.get('url'), '/items?direction=desc', "Transitioning updates the url"); - - equal(controller.get('currentDirection'), 'desc', "Current direction is asc"); - equal(controller.get('otherDirection'), 'asc', "Other direction is desc"); - - equal(normalizeUrl(Ember.$('#items-link').attr('href')), "/items?direction=asc", "Params are updated when transitioning"); - shouldNotBeActive('#items-link'); - - equal(normalizeUrl(Ember.$('#items-sort-link').attr('href')), "/items?direction=desc&sort=name", "Params are updated when transitioning"); - shouldNotBeActive('#items-sort-link'); - - return router.transitionTo({queryParams: {sort: 'name'}}); - }; - - Ember.run(function () { - promise.then(next, shouldNotHappen); - }); - - next = function () { - equal(router.get('url'), '/items?sort=name&direction=desc', "Transitioning updates the url"); - - equal(controller.get('currentDirection'), 'desc', "Current direction is asc"); - equal(controller.get('otherDirection'), 'asc', "Other direction is desc"); - - equal(normalizeUrl(Ember.$('#items-link').attr('href')), "/items?sort=name&direction=asc", "Params are updated when transitioning"); - shouldNotBeActive('#items-link'); - - equal(normalizeUrl(Ember.$('#items-sort-link').attr('href')), "/items?sort=name&direction=desc", "Params are updated when transitioning"); - shouldBeActive('#items-sort-link'); - - - Ember.$('#items-clear-link', '#qunit-fixture').click(); - - equal(router.get('url'), '/items', "Link clears the query params"); - equal(normalizeUrl(Ember.$('#items-clear-link').attr('href')), '/items', "Can clear query params"); - shouldBeActive('#items-clear-link'); - - - start(); - }; - - Ember.run(function () { - promise.then(next, shouldNotHappen); - }); - }); - - - - test("The {{link-to}} can work without a route name if query params are supplied", function() { - expect(4); - - Router.map(function() { - this.route("items", { queryParams: ['page'] }); - this.route('about'); - }); - - Ember.TEMPLATES.items = Ember.Handlebars.compile("

Items

{{#link-to page=2 id='next-page'}}Next Page{{/link-to}}"); - - bootApplication(); - - Ember.run(function() { - router.handleURL("/items"); - }); - - equal(normalizeUrl(Ember.$('#next-page').attr('href')), '/items?page=2', "The link-to works without a routename"); - shouldNotBeActive('#next-page'); - - Ember.run(function() { - Ember.$('#next-page', '#qunit-fixture').click(); - }); - - equal(router.get('url'), "/items?page=2", "Clicking the link updates the url"); - shouldBeActive('#next-page'); - }); -} - test("The {{link-to}} helper's bound parameter functionality works as expected in conjunction with an ObjectProxy/Controller", function() { Router.map(function() { this.route('post', { path: '/posts/:post_id' }); @@ -1216,3 +984,213 @@ test("the {{link-to}} helper does not throw an error if its route has exited", f Ember.$('#home-link', '#qunit-fixture').click(); }); }); + +if (Ember.FEATURES.isEnabled("query-params-new")) { + + test("{{link-to}} populates href with default query param values even without query-params object", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: '123' + }); + + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' id='the-link'}}Index{{/link-to}}"); + bootApplication(); + equal(Ember.$('#the-link').attr('href'), "/?index[foo]=123", "link has right href"); + }); + + test("{{link-to}} populates href with default query param values with empty query-params object", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: '123' + }); + + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' (query-params) id='the-link'}}Index{{/link-to}}"); + bootApplication(); + equal(Ember.$('#the-link').attr('href'), "/?index[foo]=123", "link has right href"); + }); + + test("{{link-to}} populates href with supplied query param values", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: '123' + }); + + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' (query-params foo='456') id='the-link'}}Index{{/link-to}}"); + bootApplication(); + equal(Ember.$('#the-link').attr('href'), "/?index[foo]=456", "link has right href"); + }); + + test("{{link-to}} populates href with partially supplied query param values", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo', 'bar'], + foo: '123', + bar: 'yes' + }); + + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' (query-params foo='456') id='the-link'}}Index{{/link-to}}"); + bootApplication(); + equal(Ember.$('#the-link').attr('href'), "/?index[foo]=456&index[bar]=yes", "link has right href"); + }); + + test("{{link-to}} populates href with fully supplied query param values", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo', 'bar'], + foo: '123', + bar: 'yes' + }); + + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' (query-params foo='456' bar='NAW') id='the-link'}}Index{{/link-to}}"); + bootApplication(); + equal(Ember.$('#the-link').attr('href'), "/?index[foo]=456&index[bar]=NAW", "link has right href"); + }); + + module("The {{link-to}} helper: invoking with query params", { + setup: function() { + Ember.run(function() { + App = Ember.Application.create({ + name: "App", + rootElement: '#qunit-fixture' + }); + + App.deferReadiness(); + + App.Router.reopen({ + location: 'none' + }); + + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo', 'bar'], + foo: '123', + bar: 'abc', + boundThing: "OMG" + }); + + App.AboutController = Ember.Controller.extend({ + queryParams: ['baz', 'bat'], + baz: 'alex', + bat: 'borf' + }); + + Router = App.Router; + + container = App.__container__; + + container.register('router:main', Router); + }); + }, + + teardown: function() { + Ember.run(function() { App.destroy(); }); + Ember.TEMPLATES = {}; + } + }); + + test("doesn't update controller QP properties on current route when invoked", function() { + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' id='the-link'}}Index{{/link-to}}"); + bootApplication(); + + Ember.run(Ember.$('#the-link'), 'click'); + var indexController = container.lookup('controller:index'); + deepEqual(indexController.getProperties('foo', 'bar'), { foo: '123', bar: 'abc' }, "controller QP properties not"); + }); + + test("doesn't update controller QP properties on current route when invoked (empty query-params obj)", function() { + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' (query-params) id='the-link'}}Index{{/link-to}}"); + bootApplication(); + + Ember.run(Ember.$('#the-link'), 'click'); + var indexController = container.lookup('controller:index'); + deepEqual(indexController.getProperties('foo', 'bar'), { foo: '123', bar: 'abc' }, "controller QP properties not"); + }); + + test("doesn't update controller QP properties on current route when invoked (inferred route)", function() { + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to id='the-link'}}Index{{/link-to}}"); + bootApplication(); + + Ember.run(Ember.$('#the-link'), 'click'); + var indexController = container.lookup('controller:index'); + deepEqual(indexController.getProperties('foo', 'bar'), { foo: '123', bar: 'abc' }, "controller QP properties not"); + }); + + test("doesn't update controller QP properties on current route when invoked (empty query-params obj, inferred route)", function() { + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to (query-params) id='the-link'}}Index{{/link-to}}"); + bootApplication(); + + Ember.run(Ember.$('#the-link'), 'click'); + var indexController = container.lookup('controller:index'); + deepEqual(indexController.getProperties('foo', 'bar'), { foo: '123', bar: 'abc' }, "controller QP properties not"); + }); + + test("updates controller QP properties on current route when invoked", function() { + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'index' (query-params foo='456') id='the-link'}}Index{{/link-to}}"); + bootApplication(); + + Ember.run(Ember.$('#the-link'), 'click'); + var indexController = container.lookup('controller:index'); + deepEqual(indexController.getProperties('foo', 'bar'), { foo: '456', bar: 'abc' }, "controller QP properties updated"); + }); + + test("updates controller QP properties on current route when invoked (inferred route)", function() { + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to (query-params foo='456') id='the-link'}}Index{{/link-to}}"); + bootApplication(); + + Ember.run(Ember.$('#the-link'), 'click'); + var indexController = container.lookup('controller:index'); + deepEqual(indexController.getProperties('foo', 'bar'), { foo: '456', bar: 'abc' }, "controller QP properties updated"); + }); + + test("updates controller QP properties on other route after transitioning to that route", function() { + Router.map(function() { + this.route('about'); + }); + + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to 'about' (query-params baz='lol') id='the-link'}}About{{/link-to}}"); + bootApplication(); + + equal(Ember.$('#the-link').attr('href'), '/about?about[baz]=lol&about[bat]=borf'); + Ember.run(Ember.$('#the-link'), 'click'); + var aboutController = container.lookup('controller:about'); + deepEqual(aboutController.getProperties('baz', 'bat'), { baz: 'lol', bat: 'borf' }, "about controller QP properties updated"); + + equal(container.lookup('controller:application').get('currentPath'), "about"); + }); + + test("supplied QP properties can be bound", function() { + var indexController = container.lookup('controller:index'); + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to (query-params foo=boundThing) id='the-link'}}Index{{/link-to}}"); + + bootApplication(); + + equal(Ember.$('#the-link').attr('href'), '/?index[foo]=OMG&index[bar]=abc'); + Ember.run(indexController, 'set', 'boundThing', "ASL"); + equal(Ember.$('#the-link').attr('href'), '/?index[foo]=ASL&index[bar]=abc'); + }); + + test("supplied QP properties can be bound (booleans)", function() { + var indexController = container.lookup('controller:index'); + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to (query-params foo=boundThing) id='the-link'}}Index{{/link-to}}"); + + bootApplication(); + + equal(Ember.$('#the-link').attr('href'), '/?index[foo]=OMG&index[bar]=abc'); + Ember.run(indexController, 'set', 'boundThing', false); + equal(Ember.$('#the-link').attr('href'), '/?index[bar]=abc'); + + Ember.run(Ember.$('#the-link'), 'click'); + + deepEqual(indexController.getProperties('foo', 'bar'), { foo: false, bar: 'abc' }); + }); + + test("href updates when unsupplied controller QP props change", function() { + var indexController = container.lookup('controller:index'); + Ember.TEMPLATES.index = Ember.Handlebars.compile("{{#link-to (query-params foo='lol') id='the-link'}}Index{{/link-to}}"); + + bootApplication(); + + equal(Ember.$('#the-link').attr('href'), '/?index[foo]=lol&index[bar]=abc'); + Ember.run(indexController, 'set', 'bar', 'BORF'); + equal(Ember.$('#the-link').attr('href'), '/?index[foo]=lol&index[bar]=BORF'); + Ember.run(indexController, 'set', 'foo', 'YEAH'); + equal(Ember.$('#the-link').attr('href'), '/?index[foo]=lol&index[bar]=BORF'); + }); +} diff --git a/packages/ember/tests/routing/basic_test.js b/packages/ember/tests/routing/basic_test.js index 420733f261d..cfad5b104e0 100644 --- a/packages/ember/tests/routing/basic_test.js +++ b/packages/ember/tests/routing/basic_test.js @@ -734,6 +734,7 @@ test("The loading state doesn't get entered for promises that resolve on the sam equal(Ember.$('p', '#qunit-fixture').text(), "1", "The app is now in the specials state"); }); +/* asyncTest("The Special page returning an error fires the error hook on SpecialRoute", function() { Router.map(function() { this.route("home", { path: "/" }); @@ -767,8 +768,9 @@ asyncTest("The Special page returning an error fires the error hook on SpecialRo handleURLRejectsWith('/specials/1', 'Setup error'); }); +*/ -asyncTest("The Special page returning an error invokes SpecialRoute's error handler", function() { +test("The Special page returning an error invokes SpecialRoute's error handler", function() { Router.map(function() { this.route("home", { path: "/" }); this.resource("special", { path: "/specials/:menu_item_id" }); @@ -780,9 +782,6 @@ asyncTest("The Special page returning an error invokes SpecialRoute's error hand App.MenuItem.reopenClass({ find: function(id) { menuItem = App.MenuItem.create({ id: id }); - Ember.run.later(function() { - menuItem.resolve(menuItem); - }, 1); return menuItem; } }); @@ -794,7 +793,6 @@ asyncTest("The Special page returning an error invokes SpecialRoute's error hand actions: { error: function(reason) { equal(reason, 'Setup error', 'SpecialRoute#error received the error thrown from setup'); - start(); } } }); @@ -802,6 +800,8 @@ asyncTest("The Special page returning an error invokes SpecialRoute's error hand bootApplication(); handleURLRejectsWith('/specials/1', 'Setup error'); + + Ember.run(menuItem, menuItem.resolve, menuItem); }); function testOverridableErrorHandler(handlersName) { @@ -819,9 +819,6 @@ function testOverridableErrorHandler(handlersName) { App.MenuItem.reopenClass({ find: function(id) { menuItem = App.MenuItem.create({ id: id }); - Ember.run.later(function() { - menuItem.resolve(menuItem); - }, 1); return menuItem; } }); @@ -830,7 +827,6 @@ function testOverridableErrorHandler(handlersName) { attrs[handlersName] = { error: function(reason) { equal(reason, 'Setup error', "error was correctly passed to custom ApplicationRoute handler"); - start(); } }; @@ -845,13 +841,15 @@ function testOverridableErrorHandler(handlersName) { bootApplication(); handleURLRejectsWith("/specials/1", "Setup error"); + + Ember.run(menuItem, 'resolve', menuItem); } -asyncTest("ApplicationRoute's default error handler can be overridden", function() { +test("ApplicationRoute's default error handler can be overridden", function() { testOverridableErrorHandler('actions'); }); -asyncTest("ApplicationRoute's default error handler can be overridden (with DEPRECATED `events`)", function() { +test("ApplicationRoute's default error handler can be overridden (with DEPRECATED `events`)", function() { Ember.TESTING_DEPRECATION = true; testOverridableErrorHandler('events'); }); @@ -2388,7 +2386,6 @@ test("Route supports clearing outlet explicitly", function() { }); test("Aborting/redirecting the transition in `willTransition` prevents LoadingRoute from being entered", function() { - expect(8); Router.map(function() { @@ -2632,3 +2629,58 @@ test("Ember.Location.registerImplementation is deprecated", function(){ Ember.ENV.RAISE_ON_DEPRECATION = false; }); + +test("Routes can refresh themselves causing their model hooks to be re-run", function() { + Router.map(function() { + this.resource('parent', { path: '/parent/:parent_id' }, function() { + this.route('child'); + }); + }); + + var appcount = 0; + App.ApplicationRoute = Ember.Route.extend({ + model: function() { + ++appcount; + } + }); + + var parentcount = 0; + App.ParentRoute = Ember.Route.extend({ + model: function(params) { + equal(params.parent_id, '123'); + ++parentcount; + }, + actions: { + refreshParent: function() { + this.refresh(); + } + } + }); + + var childcount = 0; + App.ParentChildRoute = Ember.Route.extend({ + model: function() { + ++childcount; + } + }); + + bootApplication(); + + equal(appcount, 1); + equal(parentcount, 0); + equal(childcount, 0); + + Ember.run(router, 'transitionTo', 'parent.child', '123'); + + equal(appcount, 1); + equal(parentcount, 1); + equal(childcount, 1); + + Ember.run(router, 'send', 'refreshParent'); + + equal(appcount, 1); + equal(parentcount, 2); + equal(childcount, 2); +}); + + diff --git a/packages/ember/tests/routing/query_params_test.js b/packages/ember/tests/routing/query_params_test.js index e66d02b029c..d5ea0284650 100644 --- a/packages/ember/tests/routing/query_params_test.js +++ b/packages/ember/tests/routing/query_params_test.js @@ -1,16 +1,14 @@ -var Router, App, AppView, templates, router, container, originalTemplates; -var get = Ember.get, set = Ember.set; +var Router, App, AppView, templates, router, container; +var get = Ember.get, + set = Ember.set, + compile = Ember.Handlebars.compile, + forEach = Ember.EnumerableUtils.forEach; -function bootApplication(url) { +function bootApplication() { router = container.lookup('router:main'); - if(url) { router.location.setURL(url); } Ember.run(App, 'advanceReadiness'); } -function compile(string) { - return Ember.Handlebars.compile(string); -} - function handleURL(path) { return Ember.run(function() { return router.handleURL(path).then(function(value) { @@ -33,11 +31,6 @@ function handleURLAborts(path) { }); } -function shouldNotHappen(error) { - console.error(error.stack); - ok(false, "this .then handler should not be called: " + error.message); -} - function handleURLRejectsWith(path, expectedReason) { Ember.run(function() { router.handleURL(path).then(function(value) { @@ -47,8 +40,34 @@ function handleURLRejectsWith(path, expectedReason) { }); }); } -if (Ember.FEATURES.isEnabled("query-params")) { - module("Routing with Query Params", { + +var startingURL = '', expectedReplaceURL, expectedPushURL; + +var TestLocation = Ember.NoneLocation.extend({ + initState: function() { + this.set('path', startingURL); + }, + + setURL: function(path) { + if (expectedPushURL) { + equal(path, expectedPushURL, "an expected pushState occurred"); + expectedPushURL = null; + } + this.set('path', path); + }, + + replaceURL: function(path) { + if (expectedReplaceURL) { + equal(path, expectedReplaceURL, "an expected replaceState occurred"); + expectedReplaceURL = null; + } + this.set('path', path); + } +}); + +if (Ember.FEATURES.isEnabled("query-params-new")) { + + module("Routing w/ Query Params", { setup: function() { Ember.run(function() { App = Ember.Application.create({ @@ -58,8 +77,13 @@ if (Ember.FEATURES.isEnabled("query-params")) { App.deferReadiness(); + container = App.__container__; + container.register('location:test', TestLocation); + + startingURL = expectedReplaceURL = expectedPushURL = ''; + App.Router.reopen({ - location: 'none' + location: 'test' }); Router = App.Router; @@ -67,9 +91,7 @@ if (Ember.FEATURES.isEnabled("query-params")) { App.LoadingRoute = Ember.Route.extend({ }); - container = App.__container__; - originalTemplates = Ember.$.extend({}, Ember.TEMPLATES); Ember.TEMPLATES.application = compile("{{outlet}}"); Ember.TEMPLATES.home = compile("

Hours

"); Ember.TEMPLATES.homepage = compile("

Megatroll

{{home}}

"); @@ -82,399 +104,282 @@ if (Ember.FEATURES.isEnabled("query-params")) { App.destroy(); App = null; - Ember.TEMPLATES = originalTemplates; + Ember.TEMPLATES = {}; }); + Ember.TESTING_DEPRECATION = false; } }); - test("The Homepage with Query Params", function() { - expect(5); - + test("Single query params can be set", function() { Router.map(function() { - this.route("index", { path: "/", queryParams: ['foo', 'baz'] }); + this.route("home", { path: '/' }); }); - App.IndexRoute = Ember.Route.extend({ - beforeModel: function(queryParams, transition) { - deepEqual(queryParams, {foo: 'bar', baz: true}, "beforeModel hook is called with query params"); - }, - - model: function(params, queryParams, transition) { - deepEqual(queryParams, {foo: 'bar', baz: true}, "Model hook is called with query params"); - }, - - afterModel: function(resolvedModel, queryParams, transition) { - deepEqual(queryParams, {foo: 'bar', baz: true}, "afterModel hook is called with query params"); - }, + App.HomeController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: "123" + }); - setupController: function (controller, context, queryParams) { - deepEqual(queryParams, {foo: 'bar', baz: true}, "setupController hook is called with query params"); - }, + bootApplication(); - renderTemplate: function (controller, context, queryParams) { - deepEqual(queryParams, {foo: 'bar', baz: true}, "renderTemplates hook is called with query params"); - } + var controller = container.lookup('controller:home'); - }); + Ember.run(controller, '_activateQueryParamObservers'); + Ember.run(controller, 'set', 'foo', '456'); + equal(router.get('location.path'), "/?home[foo]=456"); - bootApplication("/?foo=bar&baz"); + Ember.run(controller, 'set', 'foo', '987'); + equal(router.get('location.path'), "/?home[foo]=987"); }); + test("A replaceURL occurs on startup if QP values aren't already in sync", function() { + expect(1); - asyncTest("Transitioning query params works on the same route", function() { - expect(25); - - var expectedQueryParams; - - Router.map(function() { - this.route("home", { path: "/" }); - this.resource("special", { path: "/specials/:menu_item_id", queryParams: ['view'] }); + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: "123" }); - App.SpecialRoute = Ember.Route.extend({ - beforeModel: function (queryParams, transition) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the beforeModel hook"); - }, - - model: function(params, queryParams, transition) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the model hook"); - return {id: params.menu_item_id}; - }, - - afterModel: function (resolvedModel, queryParams, transition) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the beforeModel hook"); - }, + expectedReplaceURL = "/?index[foo]=123"; - setupController: function (controller, context, queryParams) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the setupController hook"); - }, + bootApplication(); + }); - renderTemplate: function (controller, context, queryParams) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the renderTemplates hook"); - }, + test('can fetch all the query param mappings associated with a route via controller `queryParams`', function() { + Router.map(function() { + this.route("home"); + this.route("parmesan"); + this.route("nothin"); + this.resource("horrible", function() { + this.route("smell"); + this.route("beef"); + this.resource("cesspool", function() { + this.route("stankonia"); + }); + }); + }); - serialize: function (obj) { - return {menu_item_id: obj.id}; - } + App.HomeController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: "1" }); + App.ParmesanController = Ember.Controller.extend({ + queryParams: ['bar'], + bar: "borf" + }); - Ember.TEMPLATES.home = Ember.Handlebars.compile( - "

Home

" - ); + App.HorribleController = Ember.Controller.extend({ + queryParams: ['yes'], + yes: "no" + }); + App.HorribleIndexController = Ember.Controller.extend({ + queryParams: ['nerd', 'dork'], + nerd: "bubbles", + dork: "troubles" + }); - Ember.TEMPLATES.special = Ember.Handlebars.compile( - "

{{content.id}}

" - ); + App.HorribleSmellController = Ember.Controller.extend({ + queryParams: ['lion'], + lion: "king" + }); bootApplication(); - var transition = handleURL('/'); - - Ember.run(function() { - transition.then(function() { - equal(Ember.$('h3', '#qunit-fixture').text(), "Home", "The app is now in the initial state"); - - expectedQueryParams = {}; - return router.transitionTo('special', {id: 1}); - }, shouldNotHappen).then(function(result) { - deepEqual(router.location.path, '/specials/1', "Correct URL after transitioning"); - - expectedQueryParams = {view: 'details'}; - return router.transitionTo('special', {queryParams: {view: 'details'}}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/1?view=details', "Correct URL after transitioning with route name and query params"); - - expectedQueryParams = {view: 'other'}; - return router.transitionTo({queryParams: {view: 'other'}}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/1?view=other', "Correct URL after transitioning with query params only"); - - expectedQueryParams = {view: 'three'}; - return router.transitionTo("/specials/1?view=three"); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/1?view=three', "Correct URL after transitioning with url"); - - start(); - }, shouldNotHappen); - }); + deepEqual(router._queryParamNamesFor('home'), { queryParams: { 'home:foo': 'home[foo]' }, translations: { foo: 'home:foo' } }); + deepEqual(router._queryParamNamesFor('parmesan'), { queryParams: { 'parmesan:bar': 'parmesan[bar]' }, translations: { 'bar': 'parmesan:bar'} }); + deepEqual(router._queryParamNamesFor('nothin'), { queryParams: {}, translations: {} }); }); - - asyncTest("Transitioning query params works on a different route", function() { - expect(46); - - var expectedQueryParams, expectedOtherQueryParams; - - Router.map(function() { - this.route("home", { path: "/" }); - this.resource("special", { path: "/specials/:menu_item_id", queryParams: ['view'] }); - this.resource("other", { path: "/others/:menu_item_id", queryParams: ['view', 'lang'] }); + test("model hooks receives query params", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['omg'], + omg: 'lol' }); - App.SpecialRoute = Ember.Route.extend({ - beforeModel: function (queryParams, transition) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the beforeModel hook"); - }, - - model: function(params, queryParams, transition) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the model hook"); - return {id: params.menu_item_id}; - }, - - afterModel: function (resolvedModel, queryParams, transition) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the beforeModel hook"); - }, - - setupController: function (controller, context, queryParams) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the setupController hook"); - }, - - renderTemplate: function (controller, context, queryParams) { - deepEqual(queryParams, expectedQueryParams, "The query params are correct in the renderTemplates hook"); - }, - - serialize: function (obj) { - return {menu_item_id: obj.id}; + App.IndexRoute = Ember.Route.extend({ + model: function(params) { + deepEqual(params, { omg: 'lol' }); } }); - App.OtherRoute = Ember.Route.extend({ - beforeModel: function (queryParams, transition) { - deepEqual(queryParams, expectedOtherQueryParams, "The query params are correct in the beforeModel hook"); - }, - - model: function(params, queryParams, transition) { - deepEqual(queryParams, expectedOtherQueryParams, "The query params are correct in the model hook"); - return {id: params.menu_item_id}; - }, - - afterModel: function (resolvedModel, queryParams, transition) { - deepEqual(queryParams, expectedOtherQueryParams, "The query params are correct in the beforeModel hook"); - }, + bootApplication(); - setupController: function (controller, context, queryParams) { - deepEqual(queryParams, expectedOtherQueryParams, "The query params are correct in the setupController hook"); - }, + equal(router.get('location.path'), "/?index[omg]=lol"); + }); - renderTemplate: function (controller, context, queryParams) { - deepEqual(queryParams, expectedOtherQueryParams, "The query params are correct in the renderTemplates hook"); - }, + test("model hooks receives query params (overridden by incoming url value)", function() { + App.IndexController = Ember.Controller.extend({ + queryParams: ['omg'], + omg: 'lol' + }); - serialize: function (obj) { - return {menu_item_id: obj.id}; + App.IndexRoute = Ember.Route.extend({ + model: function(params) { + deepEqual(params, { omg: 'yes' }); } }); - - - Ember.TEMPLATES.home = Ember.Handlebars.compile( - "

Home

" - ); - - - Ember.TEMPLATES.special = Ember.Handlebars.compile( - "

{{content.id}}

" - ); - + startingURL = "/?index[omg]=yes"; bootApplication(); - var transition = handleURL('/'); - - Ember.run(function() { - transition.then(function() { - equal(Ember.$('h3', '#qunit-fixture').text(), "Home", "The app is now in the initial state"); - - expectedQueryParams = {}; - - return router.transitionTo('special', {id: 1}); - }, shouldNotHappen).then(function(result) { - deepEqual(router.location.path, '/specials/1', "Correct URL after transitioning"); - - expectedQueryParams = {view: 'details'}; - return router.transitionTo('special', {queryParams: {view: 'details'}}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/1?view=details', "Correct URL after transitioning with route name and query params"); - - expectedOtherQueryParams = {view: 'details'}; - - return router.transitionTo('other', {id: 2}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/others/2?view=details', "Correct URL after transitioning to other route"); - - expectedOtherQueryParams = {view: 'details', lang: 'en'}; - return router.transitionTo({queryParams: {lang: 'en'}}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/others/2?view=details&lang=en', "Correct URL after transitioning to other route"); - - expectedQueryParams = {view: 'details'}; - - return router.transitionTo("special", {id: 2}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/2?view=details', "Correct URL after back to special route"); - - expectedQueryParams = {}; - - return router.transitionTo({queryParams: false}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/2', "queryParams: false clears queryParams"); - - expectedQueryParams = {view: 'details'}; - - return router.transitionTo("special", {id: 2}, {queryParams: {view: 'details'}}); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/2?view=details', "Correct URL after back to special route"); - - expectedQueryParams = {}; - - return router.transitionTo("/specials/2"); - }, shouldNotHappen).then(function (result) { - deepEqual(router.location.path, '/specials/2', "url transition clears queryParams"); - - start(); - }, shouldNotHappen); - }); + equal(router.get('location.path'), "/?index[omg]=yes"); }); + test("Route#paramsFor fetches query params", function() { + expect(1); + Router.map(function() { + this.route('index', { path: '/:something' }); + }); - test("Redirecting to the current target with a different query param aborts the remainder of the routes", function() { - expect(4); + App.IndexController = Ember.Controller.extend({ + queryParams: ['foo'], + foo: 'fooapp' + }); - Router.map(function() { - this.route("home"); - this.resource("foo", function() { - this.resource("bar", { path: "bar/:id" }, function() { - this.route("baz", { queryParams: ['foo']}); - }); - }); + App.IndexRoute = Ember.Route.extend({ + model: function(params, transition) { + deepEqual(this.paramsFor('index'), { something: 'omg', foo: 'fooapp' }, "could retrieve params for index"); + } }); - var model = { id: 2 }; + startingURL = "/omg"; + bootApplication(); + }); - var count = 0; + test("model hook can query prefix-less application params", function() { + App.ApplicationController = Ember.Controller.extend({ + queryParams: ['appomg'], + appomg: 'applol' + }); - App.BarRoute = Ember.Route.extend({ - afterModel: function(context) { - if (count++ > 10) { - ok(false, 'infinite loop'); - } else { - this.transitionTo("bar.baz", {queryParams: {foo: 'bar'}}); - } - }, + App.IndexController = Ember.Controller.extend({ + queryParams: ['omg'], + omg: 'lol' + }); - serialize: function(params) { - return params; + App.ApplicationRoute = Ember.Route.extend({ + model: function(params) { + deepEqual(params, { appomg: 'applol' }); } }); - App.BarBazRoute = Ember.Route.extend({ - setupController: function() { - ok(true, "Should still invoke setupController"); + App.IndexRoute = Ember.Route.extend({ + model: function(params) { + deepEqual(params, { omg: 'lol' }); + deepEqual(this.paramsFor('application'), { appomg: 'applol' }); } }); bootApplication(); - handleURLAborts("/foo/bar/2/baz"); - - equal(router.container.lookup('controller:application').get('currentPath'), 'foo.bar.baz'); - equal(router.get('location').getURL(), "/foo/bar/2/baz?foo=bar"); + equal(router.get('location.path'), "/?index[omg]=lol&appomg=applol"); }); - test("Parent route query params change", function() { - expect(4); - - var editCount = 0, - expectedQueryParams = {}; - - Ember.TEMPLATES.application = compile("{{outlet}}"); - Ember.TEMPLATES.posts = compile("{{outlet}}"); - Ember.TEMPLATES.post = compile("{{outlet}}"); - Ember.TEMPLATES['post/index'] = compile("showing"); - Ember.TEMPLATES['post/edit'] = compile("editing"); - - Router.map(function() { - this.resource("posts", {queryParams: ['sort']}, function() { - this.resource("post", { path: "/:postId" }, function() { - this.route("edit"); - }); - }); + test("model hook can query prefix-less application params (overridden by incoming url value)", function() { + App.ApplicationController = Ember.Controller.extend({ + queryParams: ['appomg'], + appomg: 'applol' }); - App.PostsRoute = Ember.Route.extend({ - actions: { - sort: function(dir) { - this.transitionTo({queryParams: {sort: dir}}); - } - }, - - setupController: function(controller, context, queryParams) { - deepEqual(queryParams, expectedQueryParams, "Posts route has correct query params"); - } + App.IndexController = Ember.Controller.extend({ + queryParams: ['omg'], + omg: 'lol' }); - App.PostRoute = Ember.Route.extend({ - actions: { - editPost: function(context) { - this.transitionTo('post.edit'); - } + App.ApplicationRoute = Ember.Route.extend({ + model: function(params) { + deepEqual(params, { appomg: 'appyes' }); } }); - App.PostEditRoute = Ember.Route.extend({ - setup: function() { - editCount++; + App.IndexRoute = Ember.Route.extend({ + model: function(params) { + deepEqual(params, { omg: 'yes' }); + deepEqual(this.paramsFor('application'), { appomg: 'appyes' }); } }); + startingURL = "/?index[omg]=yes&appomg=appyes"; bootApplication(); - handleURL("/posts/1"); + equal(router.get('location.path'), "/?index[omg]=yes&appomg=appyes"); + }); - Ember.run(function() { - router.send('editPost'); + test("can opt into full transition in response to QP change by calling refresh() inside queryParamsDidChange action", function() { + expect(6); + App.ApplicationController = Ember.Controller.extend({ + queryParams: ['appomg'], + appomg: 'applol' }); - expectedQueryParams = {sort: 'desc'}; + App.IndexController = Ember.Controller.extend({ + queryParams: ['omg'], + omg: 'lol' + }); - Ember.run(function() { - router.send('sort', 'desc'); + var appModelCount = 0; + App.ApplicationRoute = Ember.Route.extend({ + model: function(params) { + appModelCount++; + } }); - equal(editCount, 2, 'set up the edit route twice without failure'); - }); + var indexModelCount = 0; + App.IndexRoute = Ember.Route.extend({ + model: function(params) { + indexModelCount++; + + if (indexModelCount === 1) { + deepEqual(params, { omg: 'lol' }); + } else if (indexModelCount === 2) { + deepEqual(params, { omg: 'lex' }); + } + }, + actions: { + queryParamsDidChange: function() { + this.refresh(); + } + } + }); - test("History location can handle queryparams", function() { - var location = { - pathname: "/foo", - search: "?bar=baz" - }; + bootApplication(); - var history = { - pushState: Ember.K, - replaceState: Ember.K - }; + equal(appModelCount, 1); + equal(indexModelCount, 1); - var historyLocation = Ember.HistoryLocation.create({location: location, history: history}); + var indexController = container.lookup('controller:index'); + Ember.run(indexController, 'set', 'omg', 'lex'); - equal(historyLocation.getURL(), "/foo?bar=baz", "The query params are present"); + equal(appModelCount, 1); + equal(indexModelCount, 2); }); - test("Hash location can handle urlencoded queryparams (issue #3390)", function() { - // Firefox automatically de-url-encodes the hash, however we need it to - // be URL encoded so that encoded data can be used in query params + test("can override incoming QP values in setupController", function() { + expect(2); - var location = { - href: "http://example.com#/testing?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DkfVsfOSbJY0", - hash: "#/testing?url=http://www.youtube.com/watch?v=kfVsfOSbJY0" - }; + App.IndexController = Ember.Controller.extend({ + queryParams: ['omg'], + omg: 'lol' + }); - var hashLocation = Ember.HashLocation.create({location: location}); + App.IndexRoute = Ember.Route.extend({ + setupController: function(controller) { + ok(true, "setupController called"); + controller.set('omg', 'OVERRIDE'); + }, + actions: { + queryParamsDidChange: function() { + ok(false, "queryParamsDidChange shouldn't fire"); + } + } + }); - equal(hashLocation.getURL(), "/testing?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DkfVsfOSbJY0", "The query params are still URL encoded"); + startingURL = "/?index[omg]=borf"; + bootApplication(); + equal(router.get('location.path'), "/?index[omg]=OVERRIDE"); }); - }