diff --git a/features.json b/features.json index 6312a5ef204..62b0837ae71 100644 --- a/features.json +++ b/features.json @@ -7,6 +7,7 @@ "ember-glimmer-allow-backtracking-rerender": null, "ember-testing-resume-test": null, "ember-factory-for": true, - "ember-no-double-extend": null + "ember-no-double-extend": null, + "ember-routing-router-service": null } } diff --git a/packages/ember-application/lib/system/application.js b/packages/ember-application/lib/system/application.js index 7d6e0a81832..69ecf325f7a 100644 --- a/packages/ember-application/lib/system/application.js +++ b/packages/ember-application/lib/system/application.js @@ -34,6 +34,8 @@ import ApplicationInstance from './application-instance'; import { privatize as P } from 'container'; import Engine from './engine'; import { setupApplicationRegistry } from 'ember-glimmer'; +import { RouterService } from 'ember-routing'; +import { isFeatureEnabled } from 'ember-metal'; let librariesRegistered = false; @@ -1037,6 +1039,11 @@ function commonSetupRegistry(registry) { registry.register('location:none', NoneLocation); registry.register(P`-bucket-cache:main`, BucketCache); + + if (isFeatureEnabled('ember-routing-router-service')) { + registry.register('service:router', RouterService); + registry.injection('service:router', 'router', 'router:main'); + } } function registerLibraries() { diff --git a/packages/ember-routing/lib/index.js b/packages/ember-routing/lib/index.js index 2a66c1c5c2c..76789c31057 100644 --- a/packages/ember-routing/lib/index.js +++ b/packages/ember-routing/lib/index.js @@ -23,4 +23,5 @@ export { default as Router } from './system/router'; export { default as Route } from './system/route'; export { default as QueryParams } from './system/query_params'; export { default as RoutingService } from './services/routing'; +export { default as RouterService } from './services/router'; export { default as BucketCache } from './system/cache'; diff --git a/packages/ember-routing/lib/services/router.js b/packages/ember-routing/lib/services/router.js new file mode 100644 index 00000000000..17c54cd8873 --- /dev/null +++ b/packages/ember-routing/lib/services/router.js @@ -0,0 +1,49 @@ +/** +@module ember +@submodule ember-routing +*/ + +import { + Service, + readOnly +} from 'ember-runtime'; +import { get } from 'ember-metal'; +import RouterDSL from '../system/dsl'; + +/** + The Router service is the public API that provides component/view layer + access to the router. + + @public + @class RouterService + @category ember-routing-router-service + */ +const RouterService = Service.extend({ + currentRouteName: readOnly('router.currentRouteName'), + currentURL: readOnly('router.currentURL'), + location: readOnly('router.location'), + rootURL: readOnly('router.rootURL'), + + /** + Transition the application into another route. The route may + be either a single route or route path: + + See [Route.transitionTo](http://emberjs.com/api/classes/Ember.Route.html#method_transitionTo) for more info. + + @method transitionTo + @category ember-routing-router-service + @param {String} name the name of the route or a URL + @param {...Object} models the model(s) or identifier(s) to be used while + transitioning to the route. + @param {Object} [options] optional hash with a queryParams property + containing a mapping of query parameters + @return {Transition} the transition object associated with this + attempted transition + @public + */ + transitionTo() { + this.router.transitionTo(...arguments); + } +}); + +export default RouterService; diff --git a/packages/ember-routing/lib/system/router.js b/packages/ember-routing/lib/system/router.js index 00794ace15e..e0fcc6a579d 100644 --- a/packages/ember-routing/lib/system/router.js +++ b/packages/ember-routing/lib/system/router.js @@ -138,6 +138,10 @@ const EmberRouter = EmberObject.extend(Evented, { init() { this._super(...arguments); + this.currentURL = null; + this.currentRouteName = null; + this.currentPath = null; + this._qpCache = new EmptyObject(); this._resetQueuedQueryParameterChanges(); this._handledErrors = dictionary(null); @@ -629,6 +633,7 @@ const EmberRouter = EmberObject.extend(Evented, { let doUpdateURL = function() { location.setURL(lastURL); + set(emberRouter, 'currentURL', lastURL); }; router.updateURL = function(path) { @@ -639,6 +644,7 @@ const EmberRouter = EmberObject.extend(Evented, { if (location.replaceURL) { let doReplaceURL = function() { location.replaceURL(lastURL); + set(emberRouter, 'currentURL', lastURL); }; router.replaceURL = function(path) { @@ -1291,9 +1297,11 @@ function updatePaths(router) { let path = EmberRouter._routePath(infos); let currentRouteName = infos[infos.length - 1].name; + let currentURL = router.get('location').getURL(); set(router, 'currentPath', path); set(router, 'currentRouteName', currentRouteName); + set(router, 'currentURL', currentURL); let appController = getOwner(router).lookup('controller:application'); diff --git a/packages/ember/tests/routing/router_service_test/basic_test.js b/packages/ember/tests/routing/router_service_test/basic_test.js new file mode 100644 index 00000000000..93cd1720a2b --- /dev/null +++ b/packages/ember/tests/routing/router_service_test/basic_test.js @@ -0,0 +1,317 @@ +import Logger from 'ember-console'; +import { + Controller, + inject +} from 'ember-runtime'; +import { Component } from 'ember-glimmer'; +import { Route, NoneLocation } from 'ember-routing'; +import { + run, + get, + set +} from 'ember-metal'; +import { jQuery } from 'ember-views'; +import { + ApplicationTestCase, + moduleFor +} from 'internal-test-helpers'; + +import { isFeatureEnabled } from 'ember-metal'; + +if (isFeatureEnabled('ember-routing-router-service')) { + moduleFor('Router Service - main', class extends ApplicationTestCase { + constructor() { + super(); + + this.router.map(function() { + this.route('parent', { path: '/' }, function() { + this.route('child'); + this.route('sister'); + this.route('brother'); + }); + this.route('dynamic', { path: '/dynamic/:post_id' }); + }); + } + + ['@test RouterService#currentRouteName is correctly set for top level route'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/').then(() => { + assert.equal(routerService.get('currentRouteName'), 'parent.index'); + }); + } + + ['@test RouterService#currentRouteName is correctly set for child route'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.child', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/child').then(() => { + assert.equal(routerService.get('currentRouteName'), 'parent.child'); + }); + } + + ['@test RouterService#currentRouteName is correctly set after transition'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.child', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + }, + + afterModel() { + this.transitionTo('parent.sister'); + } + })); + + return this.visit('/child').then(() => { + assert.equal(routerService.get('currentRouteName'), 'parent.sister'); + }); + } + + ['@test RouterService#currentRouteName is correctly set on each transition'](assert) { + assert.expect(3); + + let routerService; + + this.registerRoute('parent.child', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/child') + .then(() => { + assert.equal(routerService.get('currentRouteName'), 'parent.child'); + + return this.visit('/sister'); + }) + .then(() => { + assert.equal(routerService.get('currentRouteName'), 'parent.sister'); + + return this.visit('/brother'); + }) + .then(() => { + assert.equal(routerService.get('currentRouteName'), 'parent.brother'); + }); + } + + ['@test RouterService#rootURL is correctly set to the default value'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/').then(() => { + assert.equal(routerService.get('rootURL'), '/'); + }); + } + + ['@test RouterService#rootURL is correctly set to a custom value'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + set(this.router, 'rootURL', '/homepage'); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/').then(() => { + assert.equal(routerService.get('rootURL'), '/homepage'); + }); + } + + ['@test RouterService#location is correctly delegated from router:main'](assert) { + assert.expect(2); + + let routerService; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/').then(() => { + let location = routerService.get('location'); + assert.ok(location); + assert.ok(location instanceof NoneLocation); + }); + } + + ['@test RouterService#transitionTo with basic route'](assert) { + assert.expect(1); + + let routerService; + let componentInstance; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + this.registerTemplate('parent.index', '{{foo-bar}}'); + + this.registerComponent('foo-bar', { + ComponentClass: Component.extend({ + routerService: inject.service('router'), + init() { + this._super(); + componentInstance = this; + }, + actions: { + transitionToSister() { + get(this, 'routerService').transitionTo('parent.sister'); + } + } + }), + template: `foo-bar` + }); + + return this.visit('/').then(() => { + run(function() { + componentInstance.send('transitionToSister'); + }); + + assert.equal(routerService.get('currentRouteName'), 'parent.sister'); + }); + } + + ['@test RouterService#transitionTo with dynamic segment'](assert) { + assert.expect(3); + + let routerService; + let componentInstance; + let dynamicModel = { id: 1, contents: 'much dynamicism' }; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + this.registerTemplate('parent.index', '{{foo-bar}}'); + this.registerTemplate('dynamic', '{{model.contents}}'); + + this.registerComponent('foo-bar', { + ComponentClass: Component.extend({ + routerService: inject.service('router'), + init() { + this._super(); + componentInstance = this; + }, + actions: { + transitionToDynamic() { + get(this, 'routerService').transitionTo('dynamic', dynamicModel); + } + } + }), + template: `foo-bar` + }); + + return this.visit('/').then(() => { + run(function() { + componentInstance.send('transitionToDynamic'); + }); + + assert.equal(routerService.get('currentRouteName'), 'dynamic'); + assert.equal(routerService.get('currentURL'), '/dynamic/1'); + this.assertText('much dynamicism'); + }); + } + + ['@test RouterService#transitionTo with dynamic segment and model hook'](assert) { + assert.expect(3); + + let routerService; + let componentInstance; + let dynamicModel = { id: 1, contents: 'much dynamicism' }; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + this.registerRoute('dynamic', Route.extend({ + model() { + return dynamicModel; + } + })); + + this.registerTemplate('parent.index', '{{foo-bar}}'); + this.registerTemplate('dynamic', '{{model.contents}}'); + + this.registerComponent('foo-bar', { + ComponentClass: Component.extend({ + routerService: inject.service('router'), + init() { + this._super(); + componentInstance = this; + }, + actions: { + transitionToDynamic() { + get(this, 'routerService').transitionTo('dynamic', 1); + } + } + }), + template: `foo-bar` + }); + + return this.visit('/').then(() => { + run(function() { + componentInstance.send('transitionToDynamic'); + }); + + assert.equal(routerService.get('currentRouteName'), 'dynamic'); + assert.equal(routerService.get('currentURL'), '/dynamic/1'); + this.assertText('much dynamicism'); + }); + } + }); +} diff --git a/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js b/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js new file mode 100644 index 00000000000..3e9b03203aa --- /dev/null +++ b/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js @@ -0,0 +1,206 @@ +import Logger from 'ember-console'; +import { + Controller, + inject, + readOnly +} from 'ember-runtime'; +import { Component } from 'ember-glimmer'; +import { Route, NoneLocation } from 'ember-routing'; +import { + run, + get, + set +} from 'ember-metal'; +import { jQuery } from 'ember-views'; +import { + ApplicationTestCase, + moduleFor +} from 'internal-test-helpers'; + +import { isFeatureEnabled } from 'ember-metal'; + +if (isFeatureEnabled('ember-routing-router-service')) { + let results = []; + let ROUTE_NAMES = ['index', 'child', 'sister', 'brother']; + + let InstrumentedRoute = Route.extend({ + routerService: inject.service('router'), + + beforeModel() { + let service = get(this, 'routerService'); + results.push([service.get('currentRouteName'), 'beforeModel', service.get('currentURL')]); + }, + + model() { + let service = get(this, 'routerService'); + results.push([service.get('currentRouteName'), 'model', service.get('currentURL')]); + }, + + afterModel() { + let service = get(this, 'routerService'); + results.push([service.get('currentRouteName'), 'afterModel', service.get('currentURL')]); + } + }); + + moduleFor('Router Service - currentURL lifecycle', class extends ApplicationTestCase { + constructor() { + super(); + + results = []; + + this.router.map(function() { + this.route('parent', { path: '/' }, function() { + this.route('child'); + this.route('sister'); + this.route('brother'); + this.route('stepsister'); + }); + this.route('dynamic', { path: '/dynamic/:post_id' }); + }); + + ROUTE_NAMES.forEach((name) => { + let routeName = `parent.${name}`; + this.registerRoute(routeName, InstrumentedRoute.extend()); + this.registerTemplate(routeName, '{{current-url}}'); + }); + + this.registerComponent('current-url', { + ComponentClass: Component.extend({ + routerService: inject.service('router'), + currentURL: readOnly('routerService.currentURL') + }), + template: '{{currentURL}}' + }); + } + + ['@test RouterService#currentURL is correctly set for top level route'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.index', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/').then(() => { + assert.equal(routerService.get('currentURL'), '/'); + }); + } + + ['@test RouterService#currentURL is correctly set for child route'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.child', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/child').then(() => { + assert.equal(routerService.get('currentURL'), '/child'); + }); + } + + ['@test RouterService#currentURL is correctly set after transition'](assert) { + assert.expect(1); + + let routerService; + + this.registerRoute('parent.child', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + }, + + afterModel() { + this.transitionTo('parent.sister'); + } + })); + + return this.visit('/child').then(() => { + assert.equal(routerService.get('currentURL'), '/sister'); + }); + } + + ['@test RouterService#currentURL is correctly set on each transition'](assert) { + assert.expect(3); + + let routerService; + + this.registerRoute('parent.child', Route.extend({ + routerService: inject.service('router'), + init() { + this._super(); + routerService = get(this, 'routerService'); + } + })); + + return this.visit('/child') + .then(() => { + assert.equal(routerService.get('currentURL'), '/child'); + + return this.visit('/sister'); + }) + .then(() => { + assert.equal(routerService.get('currentURL'), '/sister'); + + return this.visit('/brother'); + }) + .then(() => { + assert.equal(routerService.get('currentURL'), '/brother'); + }); + } + + ['@test RouterService#currentURL is not set during lifecycle hooks'](assert) { + assert.expect(2); + + return this.visit('/') + .then(() => { + assert.deepEqual(results, [ + [null, 'beforeModel', null], + [null, 'model', null], + [null, 'afterModel', null] + ]); + + results = []; + + return this.visit('/child'); + }) + .then(() => { + assert.deepEqual(results, [ + ['parent.index', 'beforeModel', '/'], + ['parent.index', 'model', '/'], + ['parent.index', 'afterModel', '/'] + ]); + }); + } + + ['@test RouterService#currentURL is correctly set with component after consecutive visits'](assert) { + assert.expect(3); + + return this.visit('/') + .then(() => { + this.assertText('/'); + + return this.visit('/child'); + }) + .then(() => { + this.assertText('/child'); + + return this.visit('/'); + }) + .then(() => { + this.assertText('/'); + }); + } + }); +}