Skip to content
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
18 changes: 18 additions & 0 deletions docs/en/routing/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ export default factory(function App() {

If the browser is pointed to the URL path `/home/page?queryOne=modern&queryTwo=dojo`, then the query parameters are injected into the matching `Route`'s `renderer` method as an object of type `MatchDetails` and accessed via that object's `queryParams` property. Using this URL, the page would show "Hello modern-dojo". If a query parameter is not provided, then its value will be set to `undefined`.

### Optional query parameters

Path parameters are always required as they are part of the routes path, however query params can sometimes be optional. To define an optional query parameter add a `?` before the closing brace of the query param. This instructs the router to be able to generate links even when no value for this param has been provided.

> src/routes.ts

```tsx
export default [
{
id: 'home',
path: 'home?{page?}',
outlet: 'home'
}
];
```

In the example, `page` is now optional meaning that the router can generate a link without the page value and the query param will simply not be added to the URL.

### Default route and parameters

- Specify a default route by updating the routing configuration to include `defaultRoute: true` for the preferred route. The default route is used to redirect the application on initial load if no route has been provided or the requested route has not been registered.
Expand Down
42 changes: 23 additions & 19 deletions src/routing/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,30 +124,28 @@ export class Router extends Evented<{ nav: NavEvent; route: RouteEvent; outlet:
}

let linkPath: string | undefined = route.fullPath;
if (route.fullQueryParams.length > 0) {
let queryString = route.fullQueryParams.reduce((queryParamString, param, index) => {
if (index > 0) {
return `${queryParamString}&${param}={${param}}`;
}
return `?${param}={${param}}`;
}, '');
linkPath = `${linkPath}${queryString}`;
}
params = { ...route.defaultParams, ...this._currentQueryParams, ...this._currentParams, ...params };

if (Object.keys(params).length === 0 && route.fullParams.length > 0) {
return undefined;
}

const fullParams = [...route.fullParams, ...route.fullQueryParams];
for (let i = 0; i < fullParams.length; i++) {
const param = fullParams[i];
for (let i = 0; i < route.fullParams.length; i++) {
const param = route.fullParams[i];
if (params[param]) {
linkPath = linkPath.replace(`{${param}}`, params[param]);
} else {
return undefined;
}
}

let paramSeparator = '?';
for (let i = 0; i < route.fullQueryParams.length; i++) {
const { param, optional } = route.fullQueryParams[i];
if (params[param]) {
linkPath = `${linkPath}${paramSeparator}${param}=${params[param]}`;
paramSeparator = '&';
} else if (!optional) {
return undefined;
}
}

return this._history.prefix(linkPath);
}

Expand Down Expand Up @@ -198,8 +196,12 @@ export class Router extends Evented<{ nav: NavEvent; route: RouteEvent; outlet:
routes = routes ? routes : this._routes;
for (let i = 0; i < config.length; i++) {
let { path, outlet, children, defaultRoute = false, defaultParams = {}, id, title, redirect } = config[i];
let [parsedPath, queryParamString] = path.split('?');
let queryParams: string[] = [];
let [parsedPath, ...queryParamParts] = path.split('?');
let queryParamString = queryParamParts.join('?');
let queryParams: {
param: string;
optional: boolean;
}[] = [];
parsedPath = this._stripLeadingSlash(parsedPath);

const segments: string[] = parsedPath.split('/');
Expand Down Expand Up @@ -239,7 +241,9 @@ export class Router extends Evented<{ nav: NavEvent; route: RouteEvent; outlet:
}
if (queryParamString) {
queryParams = queryParamString.split('&').map((queryParam) => {
return queryParam.replace('{', '').replace('}', '');
const param = queryParam.replace('{', '').replace(/\??}$/, '');
const optional = /\?}/.test(queryParam);
return { param, optional };
});
}
route.fullQueryParams = parentRoute ? [...parentRoute.fullQueryParams, ...queryParams] : queryParams;
Expand Down
5 changes: 4 additions & 1 deletion src/routing/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export interface Route {
redirect?: string;
fullPath: string;
fullParams: string[];
fullQueryParams: string[];
fullQueryParams: {
param: string;
optional: boolean;
}[];
defaultParams: Params;
score: number;
}
Expand Down
48 changes: 46 additions & 2 deletions tests/routing/unit/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as sinon from 'sinon';
import global from '../../../src/shim/global';
import { Router } from '../../../src/routing/Router';
import { MemoryHistory as HistoryManager } from '../../../src/routing/history/MemoryHistory';
import { RouteConfig } from '../../../src/routing/interfaces';

const routeConfig = [
{
Expand Down Expand Up @@ -105,7 +106,7 @@ const routeWithChildrenAndMultipleParams = [
}
];

const routeConfigWithParamsAndQueryParams = [
const routeConfigWithParamsAndQueryParams: RouteConfig[] = [
{
path: '/foo/{foo}?{fooQuery}',
outlet: 'foo',
Expand All @@ -116,13 +117,21 @@ const routeConfigWithParamsAndQueryParams = [
},
children: [
{
path: '/bar/{bar}?{barQuery}',
path: '/bar/{bar}?{barQuery}&{optionalQuery?}',
outlet: 'bar',
id: 'bar',
defaultParams: {
bar: 'bar',
barQuery: 'barQuery'
}
},
{
path: '/bar/{bar}?{barQuery}&{optionalQuery?}',
outlet: 'bar',
id: 'qux',
defaultParams: {
bar: 'bar'
}
}
]
}
Expand Down Expand Up @@ -578,6 +587,8 @@ describe('Router', () => {
router.setPath('foo/bar/bar/foo?fooQuery=bar&barQuery=foo');
assert.strictEqual(router.link('foo'), 'foo/bar?fooQuery=bar');
assert.strictEqual(router.link('bar'), 'foo/bar/bar/foo?fooQuery=bar&barQuery=foo');
router.setPath('foo/bar/bar/foo?fooQuery=bar&barQuery=foo&optionalQuery=optional');
assert.strictEqual(router.link('bar'), 'foo/bar/bar/foo?fooQuery=bar&barQuery=foo&optionalQuery=optional');
});

it('Should create link with params and query params with specified params', () => {
Expand All @@ -587,6 +598,17 @@ describe('Router', () => {
router.link('bar', { foo: 'qux', bar: 'baz', fooQuery: 'quxQuery', barQuery: 'bazQuery' }),
'foo/qux/bar/baz?fooQuery=quxQuery&barQuery=bazQuery'
);
assert.strictEqual(router.link('foo', { foo: 'qux', fooQuery: 'quxQuery' }), 'foo/qux?fooQuery=quxQuery');
assert.strictEqual(
router.link('bar', {
foo: 'qux',
bar: 'baz',
fooQuery: 'quxQuery',
barQuery: 'bazQuery',
optionalQuery: 'optional'
}),
'foo/qux/bar/baz?fooQuery=quxQuery&barQuery=bazQuery&optionalQuery=optional'
);
});

it('Cannot generate link for an unknown route', () => {
Expand All @@ -595,6 +617,28 @@ describe('Router', () => {
assert.isUndefined(link);
});

it('Cannot generate link when missing required query params', () => {
const router = new Router(routeConfigWithParamsAndQueryParams, { HistoryManager });
assert.strictEqual(
router.link('qux', {
foo: 'qux',
bar: 'baz',
fooQuery: 'quxQuery',
barQuery: 'barQuery',
optionalQuery: 'optional'
}),
'foo/qux/bar/baz?fooQuery=quxQuery&barQuery=barQuery&optionalQuery=optional'
);
assert.isUndefined(
router.link('qux', {
foo: 'qux',
bar: 'baz',
fooQuery: 'quxQuery',
optionalQuery: 'optional'
})
);
});

it('The router will not start automatically if autostart is set to false', () => {
let initialNavEvent = false;
let historyManagerCount = 0;
Expand Down