Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE Router Service] recognize and recognizeAndLoad #17034

Merged
merged 1 commit into from
Oct 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
55 changes: 50 additions & 5 deletions packages/@ember/-internals/routing/lib/services/router.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
14 changes: 13 additions & 1 deletion packages/@ember/-internals/routing/lib/system/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
229 changes: 229 additions & 0 deletions packages/ember/tests/routing/router_service_test/recognize_test.js
Original file line number Diff line number Diff line change
@@ -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');
}
);
}
}
);
}
7 changes: 4 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down