From d6e899c77991bb83977c9aac4c4afe69f4528450 Mon Sep 17 00:00:00 2001 From: Chad Hietala Date: Wed, 3 Oct 2018 11:54:17 -0400 Subject: [PATCH] [FEATURE Router Service] recognize and recognizeAndLoad --- package.json | 2 +- .../-internals/routing/lib/services/router.ts | 55 ++++- .../-internals/routing/lib/system/route.ts | 14 +- .../router_service_test/recognize_test.js | 229 ++++++++++++++++++ yarn.lock | 7 +- 5 files changed, 297 insertions(+), 10 deletions(-) create mode 100644 packages/ember/tests/routing/router_service_test/recognize_test.js diff --git a/package.json b/package.json index fee4b90d213..3233196c5fd 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "puppeteer": "^1.3.0", "qunit": "^2.5.0", "route-recognizer": "^0.3.4", - "router_js": "^5.1.1", + "router_js": "^5.2.0", "rsvp": "^4.8.2", "semver": "^5.5.0", "serve-static": "^1.12.2", diff --git a/packages/@ember/-internals/routing/lib/services/router.ts b/packages/@ember/-internals/routing/lib/services/router.ts index 34379cabb6e..4de2952a3b0 100644 --- a/packages/@ember/-internals/routing/lib/services/router.ts +++ b/packages/@ember/-internals/routing/lib/services/router.ts @@ -1,5 +1,6 @@ import { Evented } from '@ember/-internals/runtime'; import { EMBER_ROUTING_ROUTER_SERVICE } from '@ember/canary-features'; +import { assert } from '@ember/debug'; import { readOnly } from '@ember/object/computed'; import Service from '@ember/service'; import { Transition } from 'router_js'; @@ -261,6 +262,46 @@ if (EMBER_ROUTING_ROUTER_SERVICE) { this.trigger('routeDidChange', transition); }); }, + + /** + Takes a string URL and returns a `RouteInfo` for the leafmost route represented + by the URL. Returns `null` if the URL is not recognized. This method expects to + receive the actual URL as seen by the browser including the app's `rootURL`. + + @method recognize + @param {String} url + @category ember-routing-router-service + @public + */ + recognize(url: string) { + assert( + `You must pass a url that begins with the application's rootURL "${this.rootURL}"`, + url.indexOf(this.rootURL) === 0 + ); + let internalURL = cleanURL(url, this.rootURL); + return this._router._routerMicrolib.recognize(internalURL); + }, + + /** + Takes a string URL and returns a promise that resolves to a + `RouteInfoWithAttributes` for the leafmost route represented by the URL. + The promise rejects if the URL is not recognized or an unhandled exception + is encountered. This method expects to receive the actual URL as seen by + the browser including the app's `rootURL`. + + @method recognizeAndLoad + @param {String} url + @category ember-routing-router-service + @public + */ + recognizeAndLoad(url: string) { + assert( + `You must pass a url that begins with the application's rootURL "${this.rootURL}"`, + url.indexOf(this.rootURL) === 0 + ); + let internalURL = cleanURL(url, this.rootURL); + return this._router._routerMicrolib.recognizeAndLoad(internalURL); + }, /** The `routeWillChange` event is fired at the beginning of any attempted transition with a `Transition` object as the sole @@ -287,11 +328,7 @@ if (EMBER_ROUTING_ROUTER_SERVICE) { }); ``` - The `routeWillChange` event fires whenever a new route is chosen - as the desired target of a transition. This includes `transitionTo`, - `replaceWith`, all redirection for any reason including error handling, - and abort. Aborting implies changing the desired target back to where - you already were. Once a transition has completed, `routeDidChange` fires. + The `routeWillChange` event fires whenever a new route is chosen as the desired target of a transition. This includes `transitionTo`, `replaceWith`, all redirection for any reason including error handling, and abort. Aborting implies changing the desired target back to where you already were. Once a transition has completed, `routeDidChange` fires. @event routeWillChange @param {Transition} transition @@ -329,4 +366,12 @@ if (EMBER_ROUTING_ROUTER_SERVICE) { @public */ }); + + function cleanURL(url: string, rootURL: string) { + if (rootURL === '/') { + return url; + } + + return url.substr(rootURL.length, url.length); + } } diff --git a/packages/@ember/-internals/routing/lib/system/route.ts b/packages/@ember/-internals/routing/lib/system/route.ts index fdce44eb433..e3b3b96091e 100644 --- a/packages/@ember/-internals/routing/lib/system/route.ts +++ b/packages/@ember/-internals/routing/lib/system/route.ts @@ -1169,6 +1169,9 @@ class Route extends EmberObject implements IRoute { Router.js hook. */ deserialize(_params: {}, transition: Transition) { + if (EMBER_ROUTING_ROUTER_SERVICE) { + return this.model(this._paramsFor(this.routeName, _params), transition); + } return this.model(this.paramsFor(this.routeName), transition); } @@ -2544,7 +2547,16 @@ if (EMBER_ROUTING_ROUTER_SERVICE && ROUTER_EVENTS) { }, }; - Route.reopen(ROUTER_EVENT_DEPRECATIONS); + Route.reopen(ROUTER_EVENT_DEPRECATIONS, { + _paramsFor(routeName: string, params: {}) { + let transition = this._router._routerMicrolib.activeTransition; + if (transition !== undefined) { + return this.paramsFor(routeName); + } + + return params; + }, + }); } export default Route; diff --git a/packages/ember/tests/routing/router_service_test/recognize_test.js b/packages/ember/tests/routing/router_service_test/recognize_test.js new file mode 100644 index 00000000000..5e5494970dc --- /dev/null +++ b/packages/ember/tests/routing/router_service_test/recognize_test.js @@ -0,0 +1,229 @@ +import { RouterTestCase, moduleFor } from 'internal-test-helpers'; +import { Route } from '@ember/-internals/routing'; +import { EMBER_ROUTING_ROUTER_SERVICE } from '@ember/canary-features'; + +if (EMBER_ROUTING_ROUTER_SERVICE) { + moduleFor( + 'Router Service - recognize', + class extends RouterTestCase { + '@test returns a RouteInfo for recognized URL'(assert) { + return this.visit('/').then(() => { + let routeInfo = this.routerService.recognize('/dynamic-with-child/123/1?a=b'); + assert.ok(routeInfo); + let { name, localName, parent, child, params, queryParams, paramNames } = routeInfo; + assert.equal(name, 'dynamicWithChild.child'); + assert.equal(localName, 'child'); + assert.ok(parent); + assert.equal(parent.name, 'dynamicWithChild'); + assert.notOk(child); + assert.deepEqual(params, { child_id: '1' }); + assert.deepEqual(queryParams, { a: 'b' }); + assert.deepEqual(paramNames, ['child_id']); + }); + } + + '@test does not transition'(assert) { + this.addTemplate('parent', 'Parent'); + this.addTemplate('dynamic-with-child.child', 'Dynamic Child'); + + return this.visit('/').then(() => { + this.routerService.recognize('/dynamic-with-child/123/1?a=b'); + this.assertText('Parent', 'Did not transition and cause render'); + assert.equal(this.routerService.currentURL, '/', 'Did not transition'); + }); + } + + '@test respects the usage of a different rootURL'(assert) { + this.router.reopen({ + rootURL: '/app/', + }); + + return this.visit('/app').then(() => { + let routeInfo = this.routerService.recognize('/app/child/'); + assert.ok(routeInfo); + let { name, localName, parent } = routeInfo; + assert.equal(name, 'parent.child'); + assert.equal(localName, 'child'); + assert.equal(parent.name, 'parent'); + }); + } + + '@test must include rootURL'() { + this.addTemplate('parent', 'Parent'); + this.addTemplate('dynamic-with-child.child', 'Dynamic Child'); + + this.router.reopen({ + rootURL: '/app/', + }); + + return this.visit('/app').then(() => { + expectAssertion(() => { + this.routerService.recognize('/dynamic-with-child/123/1?a=b'); + }, 'You must pass a url that begins with the application\'s rootURL "/app/"'); + }); + } + + '@test returns `null` if URL is not recognized'(assert) { + return this.visit('/').then(() => { + let routeInfo = this.routerService.recognize('/foo'); + assert.equal(routeInfo, null); + }); + } + } + ); + + moduleFor( + 'Router Service - recognizeAndLoad', + class extends RouterTestCase { + '@test returns a RouteInfoWithAttributes for recognized URL'(assert) { + this.add( + 'route:dynamicWithChild', + Route.extend({ + model(params) { + return { name: 'dynamicWithChild', data: params.dynamic_id }; + }, + }) + ); + this.add( + 'route:dynamicWithChild.child', + Route.extend({ + model(params) { + return { name: 'dynamicWithChild.child', data: params.child_id }; + }, + }) + ); + + return this.visit('/') + .then(() => { + return this.routerService.recognizeAndLoad('/dynamic-with-child/123/1?a=b'); + }) + .then(routeInfoWithAttributes => { + assert.ok(routeInfoWithAttributes); + let { + name, + localName, + parent, + attributes, + paramNames, + params, + queryParams, + } = routeInfoWithAttributes; + assert.equal(name, 'dynamicWithChild.child'); + assert.equal(localName, 'child'); + assert.equal(parent.name, 'dynamicWithChild'); + assert.deepEqual(params, { child_id: '1' }); + assert.deepEqual(queryParams, { a: 'b' }); + assert.deepEqual(paramNames, ['child_id']); + assert.deepEqual(attributes, { name: 'dynamicWithChild.child', data: '1' }); + assert.deepEqual(parent.attributes, { name: 'dynamicWithChild', data: '123' }); + assert.deepEqual(parent.paramNames, ['dynamic_id']); + assert.deepEqual(parent.params, { dynamic_id: '123' }); + }); + } + + '@test does not transition'(assert) { + this.addTemplate('parent', 'Parent{{outlet}}'); + this.addTemplate('parent.child', 'Child'); + + this.add( + 'route:parent.child', + Route.extend({ + model() { + return { name: 'child', data: ['stuff'] }; + }, + }) + ); + return this.visit('/') + .then(() => { + return this.routerService.recognizeAndLoad('/child'); + }) + .then(() => { + assert.equal(this.routerService.currentURL, '/'); + this.assertText('Parent'); + }); + } + + '@test respects the usage of a different rootURL'(assert) { + this.router.reopen({ + rootURL: '/app/', + }); + + return this.visit('/app') + .then(() => { + return this.routerService.recognizeAndLoad('/app/child/'); + }) + .then(routeInfoWithAttributes => { + assert.ok(routeInfoWithAttributes); + let { name, localName, parent } = routeInfoWithAttributes; + assert.equal(name, 'parent.child'); + assert.equal(localName, 'child'); + assert.equal(parent.name, 'parent'); + }); + } + + '@test must include rootURL'() { + this.router.reopen({ + rootURL: '/app/', + }); + + return this.visit('/app').then(() => { + expectAssertion(() => { + this.routerService.recognizeAndLoad('/dynamic-with-child/123/1?a=b'); + }, 'You must pass a url that begins with the application\'s rootURL "/app/"'); + }); + } + + '@test rejects if url is not recognized'(assert) { + this.addTemplate('parent', 'Parent{{outlet}}'); + this.addTemplate('parent.child', 'Child'); + + this.add( + 'route:parent.child', + Route.extend({ + model() { + return { name: 'child', data: ['stuff'] }; + }, + }) + ); + return this.visit('/') + .then(() => { + return this.routerService.recognizeAndLoad('/foo'); + }) + .then( + () => { + assert.ok(false, 'never'); + }, + reason => { + assert.equal(reason, 'URL /foo was not recognized'); + } + ); + } + + '@test rejects if there is an unhandled error'(assert) { + this.addTemplate('parent', 'Parent{{outlet}}'); + this.addTemplate('parent.child', 'Child'); + + this.add( + 'route:parent.child', + Route.extend({ + model() { + throw Error('Unhandled'); + }, + }) + ); + return this.visit('/') + .then(() => { + return this.routerService.recognizeAndLoad('/child'); + }) + .then( + () => { + assert.ok(false, 'never'); + }, + err => { + assert.equal(err.message, 'Unhandled'); + } + ); + } + } + ); +} diff --git a/yarn.lock b/yarn.lock index 634bf9239a4..5b5b1cc3818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7385,9 +7385,10 @@ route-recognizer@^0.3.4: resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g== -router_js@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/router_js/-/router_js-5.1.1.tgz#3a285264132040ea4aa4a6ce2d9b7a05d40176fb" +router_js@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/router_js/-/router_js-5.2.0.tgz#8796d0ad7ab8a9d0ffbf5b02e5e00d2472a53e7d" + integrity sha512-v+gjYRwDWJpJW0jPB9tFphbcp0pD7R/ZRqu/tno9TXgQxanRArw/weyGFZnbpR95tY9B5SpFonAZk5opPNQUvQ== dependencies: "@types/node" "^10.5.5"