diff --git a/packages/ember-routing-htmlbars/tests/helpers/link-to_test.js b/packages/ember-routing-htmlbars/tests/helpers/link-to_test.js index a13e48db319..e9f1a643ef1 100644 --- a/packages/ember-routing-htmlbars/tests/helpers/link-to_test.js +++ b/packages/ember-routing-htmlbars/tests/helpers/link-to_test.js @@ -4,11 +4,32 @@ import EmberView from "ember-views/views/view"; import compile from "ember-template-compiler/system/compile"; import { set } from "ember-metal/property_set"; import Controller from "ember-runtime/controllers/controller"; +import { Registry } from "ember-runtime/system/container"; import { runAppend, runDestroy } from "ember-runtime/tests/utils"; +import EmberObject from "ember-runtime/system/object"; var view; +var container; +var registry = new Registry(); + +// These tests don't rely on the routing service, but LinkView makes +// some assumptions that it will exist. This small stub service ensures +// that the LinkView can render without raising an exception. +// +// TODO: Add tests that test actual behavior. Currently, all behavior +// is tested integration-style in the `ember` package. +registry.register('service:-routing', EmberObject.extend({ + availableRoutes: function() { return ['index']; }, + hasRoute: function(name) { return name === 'index'; }, + isActiveForRoute: function() { return true; }, + generateURL: function() { return "/"; } +})); QUnit.module("ember-routing-htmlbars: link-to helper", { + setup: function() { + container = registry.container(); + }, + teardown: function() { runDestroy(view); } @@ -18,7 +39,8 @@ QUnit.module("ember-routing-htmlbars: link-to helper", { QUnit.test("should be able to be inserted in DOM when the router is not present", function() { var template = "{{#link-to 'index'}}Go to Index{{/link-to}}"; view = EmberView.create({ - template: compile(template) + template: compile(template), + container: container }); runAppend(view); @@ -33,7 +55,8 @@ QUnit.test("re-renders when title changes", function() { title: 'foo', routeName: 'index' }, - template: compile(template) + template: compile(template), + container: container }); runAppend(view); @@ -54,7 +77,8 @@ QUnit.test("can read bound title", function() { title: 'foo', routeName: 'index' }, - template: compile(template) + template: compile(template), + container: container }); runAppend(view); @@ -65,7 +89,8 @@ QUnit.test("can read bound title", function() { QUnit.test("escaped inline form (double curlies) escapes link title", function() { view = EmberView.create({ title: "blah", - template: compile("{{link-to view.title}}") + template: compile("{{link-to view.title}}"), + container: container }); runAppend(view); @@ -76,7 +101,8 @@ QUnit.test("escaped inline form (double curlies) escapes link title", function() QUnit.test("unescaped inline form (triple curlies) does not escape link title", function() { view = EmberView.create({ title: "blah", - template: compile("{{{link-to view.title}}}") + template: compile("{{{link-to view.title}}}"), + container: container }); runAppend(view); @@ -92,7 +118,8 @@ QUnit.test("unwraps controllers", function() { model: 'foo' }), - template: compile(template) + template: compile(template), + container: container }); expectDeprecation(function() { diff --git a/packages/ember-routing-views/lib/views/link.js b/packages/ember-routing-views/lib/views/link.js index d9ab390b8ba..f0b16b1a5aa 100644 --- a/packages/ember-routing-views/lib/views/link.js +++ b/packages/ember-routing-views/lib/views/link.js @@ -6,27 +6,12 @@ import Ember from "ember-metal/core"; // FEATURES, Logger, assert import { get } from "ember-metal/property_get"; -import merge from "ember-metal/merge"; -import run from "ember-metal/run_loop"; import { computed } from "ember-metal/computed"; import { fmt } from "ember-runtime/system/string"; -import keys from "ember-metal/keys"; import { isSimpleClick } from "ember-views/system/utils"; import EmberComponent from "ember-views/views/component"; -import { routeArgs } from "ember-routing/utils"; import { read, subscribe } from "ember-metal/streams/utils"; - -var numberOfContextsAcceptedByHandler = function(handler, handlerInfos) { - var req = 0; - for (var i = 0, l = handlerInfos.length; i < l; i++) { - req = req + handlerInfos[i].names.length; - if (handlerInfos[i].handler === handler) { - break; - } - } - - return req; -}; +import inject from "ember-runtime/inject"; var linkViewClassNameBindings = ['active', 'loading', 'disabled']; if (Ember.FEATURES.isEnabled('ember-routing-transitioning-classes')) { @@ -216,6 +201,8 @@ var LinkView = EmberComponent.extend({ this.on(eventName, this, this._invoke); }, + _routing: inject.service('-routing'), + /** This method is invoked by observers installed during `init` that fire whenever the params change @@ -290,16 +277,14 @@ var LinkView = EmberComponent.extend({ @property active **/ active: computed('loadedParams', function computeLinkViewActive() { - var router = get(this, 'router'); - if (!router) { return; } - return computeActive(this, router.currentState); + var currentState = get(this, '_routing.currentState'); + return computeActive(this, currentState); }), - willBeActive: computed('router.targetState', function() { - var router = get(this, 'router'); - if (!router) { return; } - var targetState = router.targetState; - if (router.currentState === targetState) { return; } + willBeActive: computed('_routing.targetState', function() { + var routing = get(this, '_routing'); + var targetState = get(routing, 'targetState'); + if (get(routing, 'currentState') === targetState) { return; } return !!computeActive(this, targetState); }), @@ -333,19 +318,6 @@ var LinkView = EmberComponent.extend({ if (!get(this, 'loadedParams')) { return get(this, 'loadingClass'); } }), - /** - Returns the application's main router from the container. - - @private - @property router - **/ - router: computed(function() { - var controller = get(this, 'controller'); - if (controller && controller.container) { - return controller.container.lookup('router:main'); - } - }), - /** Event handler that invokes the link, activating the associated route. @@ -377,57 +349,8 @@ var LinkView = EmberComponent.extend({ return false; } - var router = get(this, 'router'); - var loadedParams = get(this, 'loadedParams'); - - var transition = router._doTransition(loadedParams.targetRouteName, loadedParams.models, loadedParams.queryParams); - if (get(this, 'replace')) { - transition.method('replace'); - } - - if (Ember.FEATURES.isEnabled('ember-routing-transitioning-classes')) { - return; - } - - // Schedule eager URL update, but after we've given the transition - // a chance to synchronously redirect. - // We need to always generate the URL instead of using the href because - // the href will include any rootURL set, but the router expects a URL - // without it! Note that we don't use the first level router because it - // calls location.formatURL(), which also would add the rootURL! - var args = routeArgs(loadedParams.targetRouteName, loadedParams.models, transition.state.queryParams); - var url = router.router.generate.apply(router.router, args); - - run.scheduleOnce('routerTransitions', this, this._eagerUpdateUrl, transition, url); - }, - - /** - @private - @method _eagerUpdateUrl - @param transition - @param href - */ - _eagerUpdateUrl: function(transition, href) { - if (!transition.isActive || !transition.urlMethod) { - // transition was aborted, already ran to completion, - // or it has a null url-updated method. - return; - } - - if (href.indexOf('#') === 0) { - href = href.slice(1); - } - - // Re-use the routerjs hooks set up by the Ember router. - var routerjs = get(this, 'router.router'); - if (transition.urlMethod === 'update') { - routerjs.updateURL(href); - } else if (transition.urlMethod === 'replace') { - routerjs.replaceURL(href); - } - - // Prevent later update url refire. - transition.method(null); + var params = get(this, 'loadedParams'); + get(this, '_routing').transitionTo(params.targetRouteName, params.models, params.queryParams, get(this, 'replace')); }, /** @@ -449,15 +372,14 @@ var LinkView = EmberComponent.extend({ @property @return {Array} */ - resolvedParams: computed('router.url', function() { + resolvedParams: computed('_routing.currentState', function() { var params = this.params; var targetRouteName; var models = []; var onlyQueryParamsSupplied = (params.length === 0); if (onlyQueryParamsSupplied) { - var appController = this.container.lookup('controller:application'); - targetRouteName = get(appController, 'currentRouteName'); + targetRouteName = get(this, '_routing.currentRouteName'); } else { targetRouteName = read(params[0]); @@ -486,8 +408,8 @@ var LinkView = EmberComponent.extend({ @return {Array} An array with the route name and any dynamic segments **/ loadedParams: computed('resolvedParams', function computeLinkViewRouteArgs() { - var router = get(this, 'router'); - if (!router) { return; } + var routing = get(this, '_routing'); + if (!routing) { return; } var resolvedParams = get(this, 'resolvedParams'); var namedRoute = resolvedParams.targetRouteName; @@ -496,8 +418,8 @@ var LinkView = EmberComponent.extend({ Ember.assert(fmt("The attempt to link-to route '%@' failed. " + "The router did not find '%@' in its possible routes: '%@'", - [namedRoute, namedRoute, keys(router.router.recognizer.names).join("', '")]), - router.hasRoute(namedRoute)); + [namedRoute, namedRoute, routing.availableRoutes().join("', '")]), + routing.hasRoute(namedRoute)); if (!paramsAreLoaded(resolvedParams.models)) { return; } @@ -518,20 +440,14 @@ var LinkView = EmberComponent.extend({ href: computed('loadedParams', function computeLinkViewHref() { if (get(this, 'tagName') !== 'a') { return; } - var router = get(this, 'router'); - var loadedParams = get(this, 'loadedParams'); + var routing = get(this, '_routing'); + var params = get(this, 'loadedParams'); - if (!loadedParams) { + if (!params) { return get(this, 'loadingHref'); } - var visibleQueryParams = {}; - merge(visibleQueryParams, loadedParams.queryParams); - router._prepareQueryParams(loadedParams.targetRouteName, loadedParams.models, visibleQueryParams); - - var args = routeArgs(loadedParams.targetRouteName, loadedParams.models, visibleQueryParams); - var result = router.generate.apply(router, args); - return result; + return routing.generateURL(params.targetRouteName, params.models, params.queryParams); }), /** @@ -572,46 +488,26 @@ function paramsAreLoaded(params) { return true; } -function computeActive(route, routerState) { - if (get(route, 'loading')) { return false; } +function computeActive(view, routerState) { + if (get(view, 'loading')) { return false; } - var currentWhen = route['current-when'] || route.currentWhen; + var currentWhen = view['current-when'] || view.currentWhen; var isCurrentWhenSpecified = !!currentWhen; - currentWhen = currentWhen || get(route, 'loadedParams').targetRouteName; + currentWhen = currentWhen || get(view, 'loadedParams').targetRouteName; currentWhen = currentWhen.split(' '); for (var i = 0, len = currentWhen.length; i < len; i++) { - if (isActiveForRoute(route, currentWhen[i], isCurrentWhenSpecified, routerState)) { - return get(route, 'activeClass'); + if (isActiveForRoute(view, currentWhen[i], isCurrentWhenSpecified, routerState)) { + return get(view, 'activeClass'); } } return false; } -function isActiveForRoute(route, routeName, isCurrentWhenSpecified, routerState) { - var router = get(route, 'router'); - var loadedParams = get(route, 'loadedParams'); - var contexts = loadedParams.models; - - var handlers = router.router.recognizer.handlersFor(routeName); - var leafName = handlers[handlers.length-1].handler; - var maximumContexts = numberOfContextsAcceptedByHandler(routeName, handlers); - - // NOTE: any ugliness in the calculation of activeness is largely - // due to the fact that we support automatic normalizing of - // `resource` -> `resource.index`, even though there might be - // dynamic segments / query params defined on `resource.index` - // which complicates (and makes somewhat ambiguous) the calculation - // of activeness for links that link to `resource` instead of - // directly to `resource.index`. - - // if we don't have enough contexts revert back to full route name - // this is because the leaf route will use one of the contexts - if (contexts.length > maximumContexts) { - routeName = leafName; - } - - return routerState.isActiveIntent(routeName, contexts, loadedParams.queryParams, !isCurrentWhenSpecified); +function isActiveForRoute(view, routeName, isCurrentWhenSpecified, routerState) { + var params = get(view, 'loadedParams'); + var service = get(view, '_routing'); + return service.isActiveForRoute(params, routeName, routerState, isCurrentWhenSpecified); } export { diff --git a/packages/ember-routing/lib/initializers/routing-service.js b/packages/ember-routing/lib/initializers/routing-service.js new file mode 100644 index 00000000000..b9e9f3837da --- /dev/null +++ b/packages/ember-routing/lib/initializers/routing-service.js @@ -0,0 +1,14 @@ +import { onLoad } from "ember-runtime/system/lazy_load"; +import RoutingService from "ember-routing/services/routing"; + +onLoad('Ember.Application', function(Application) { + Application.initializer({ + name: 'routing-service', + initialize: function(registry) { + // Register the routing service... + registry.register('service:-routing', RoutingService); + // Then inject the app router into it + registry.injection('service:-routing', 'router', 'router:main'); + } + }); +}); diff --git a/packages/ember-routing/lib/main.js b/packages/ember-routing/lib/main.js index 93e620bf82d..bc448775837 100644 --- a/packages/ember-routing/lib/main.js +++ b/packages/ember-routing/lib/main.js @@ -27,6 +27,8 @@ import RouterDSL from "ember-routing/system/dsl"; import Router from "ember-routing/system/router"; import Route from "ember-routing/system/route"; +import "ember-routing/initializers/routing-service"; + Ember.Location = EmberLocation; Ember.AutoLocation = AutoLocation; Ember.HashLocation = HashLocation; diff --git a/packages/ember-routing/lib/services/routing.js b/packages/ember-routing/lib/services/routing.js new file mode 100644 index 00000000000..613551c4399 --- /dev/null +++ b/packages/ember-routing/lib/services/routing.js @@ -0,0 +1,102 @@ +/** +@module ember +@submodule ember-routing +*/ + +import Service from "ember-runtime/system/service"; + +import { get } from "ember-metal/property_get"; +import { readOnly } from "ember-metal/computed_macros"; +import { routeArgs } from "ember-routing/utils"; +import keys from "ember-metal/keys"; +import merge from "ember-metal/merge"; + +/** @private + The Routing service is used by LinkView, and provides facilities for + the component/view layer to interact with the router. + + While still private, this service can eventually be opened up, and provides + the set of API needed for components to control routing without interacting + with router internals. +*/ + +var RoutingService = Service.extend({ + router: null, + + targetState: readOnly('router.targetState'), + currentState: readOnly('router.currentState'), + currentRouteName: readOnly('router.currentRouteName'), + + availableRoutes: function() { + return keys(get(this, 'router').router.recognizer.names); + }, + + hasRoute: function(routeName) { + return get(this, 'router').hasRoute(routeName); + }, + + transitionTo: function(routeName, models, queryParams, shouldReplace) { + var router = get(this, 'router'); + + var transition = router._doTransition(routeName, models, queryParams); + + if (shouldReplace) { + transition.method('replace'); + } + }, + + normalizeQueryParams: function(routeName, models, queryParams) { + get(this, 'router')._prepareQueryParams(routeName, models, queryParams); + }, + + generateURL: function(routeName, models, queryParams) { + var router = get(this, 'router'); + + var visibleQueryParams = {}; + merge(visibleQueryParams, queryParams); + + this.normalizeQueryParams(routeName, models, visibleQueryParams); + + var args = routeArgs(routeName, models, visibleQueryParams); + return router.generate.apply(router, args); + }, + + isActiveForRoute: function(params, routeName, routerState, isCurrentWhenSpecified) { + var router = get(this, 'router'); + var contexts = params.models; + + var handlers = router.router.recognizer.handlersFor(routeName); + var leafName = handlers[handlers.length-1].handler; + var maximumContexts = numberOfContextsAcceptedByHandler(routeName, handlers); + + // NOTE: any ugliness in the calculation of activeness is largely + // due to the fact that we support automatic normalizing of + // `resource` -> `resource.index`, even though there might be + // dynamic segments / query params defined on `resource.index` + // which complicates (and makes somewhat ambiguous) the calculation + // of activeness for links that link to `resource` instead of + // directly to `resource.index`. + + // if we don't have enough contexts revert back to full route name + // this is because the leaf route will use one of the contexts + if (contexts.length > maximumContexts) { + routeName = leafName; + } + + return routerState.isActiveIntent(routeName, contexts, params.queryParams, !isCurrentWhenSpecified); + } +}); + +var numberOfContextsAcceptedByHandler = function(handler, handlerInfos) { + var req = 0; + for (var i = 0, l = handlerInfos.length; i < l; i++) { + req = req + handlerInfos[i].names.length; + if (handlerInfos[i].handler === handler) { + break; + } + } + + return req; +}; + +export default RoutingService; diff --git a/packages/ember-routing/lib/system/router.js b/packages/ember-routing/lib/system/router.js index 4863c41c52b..32688ef03a5 100644 --- a/packages/ember-routing/lib/system/router.js +++ b/packages/ember-routing/lib/system/router.js @@ -875,12 +875,14 @@ function updatePaths(router) { } set(appController, 'currentPath', path); + set(router, 'currentPath', path); if (!('currentRouteName' in appController)) { defineProperty(appController, 'currentRouteName'); } set(appController, 'currentRouteName', infos[infos.length - 1].name); + set(router, 'currentRouteName', infos[infos.length - 1].name); } EmberRouter.reopenClass({