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

Add array syntax for route definitions. #21

Merged
merged 1 commit into from
Feb 10, 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
71 changes: 52 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ dispatch Redux actions in response to route changes.
- [Install](#install)
- [Usage](#usage)
- [Behavior](#behavior)
- [Parameters](#parameters)
- [Route Matching](#route-matching)
- [Routes](#routes)
- [Options](#options)
- [Navigation](#navigation)
- [Hash History](#hash-history)
Expand Down Expand Up @@ -103,7 +102,35 @@ your application will continue to function when you hit other routes. That also
means you should ensure you handle any potential errors that could occur in your
route sagas.

## Parameters
## Routes

Routes may be expressed as either an object or an array with the main difference
being that the array form preserves order and, therefore, the precedence of
routes.

```js
const objectFormRoutes = {
'/foo': fooHandler,
'/bar': barHandler,
};

const arrayFormRoutes = [
{ pattern: '/foo', handler: fooHandler },
{ pattern: '/bar', handler: barHandler },
];
```

### Exact Matching

This route will only match `/foo` exactly.

```js
const routes = {
'/foo': saga,
};
```

### Path Parameters

You can capture dynamic path parameters by prepending them with the `:` symbol.
The name you use will be assigned to a property of the same name on a parameters
Expand All @@ -125,22 +152,6 @@ const routes = {
};
```

## Route Matching

Here are some examples of how route matching works.

### Exact Matching

This route will only match `/foo` exactly.

```js
const routes = {
'/foo': saga,
};
```

### Path Parameters

If you specify a dynamic path parameter, then it will be required. This route
will match `/bar/42` but NOT `/bar`.

Expand Down Expand Up @@ -184,6 +195,28 @@ const routes = {
};
```

### Route Precedence

It is often desirable to have some routes take higher precedence over others.
Consider the case where one route accepts a parameter but for a particular value
of that parameter, we want to use a separate handler:

```js
const routes = [
{ pattern: '/foo/baz', handler: bazHandler },
{ pattern: '/foo/:arg', handler: genericHandler },
]
```

By using the array form to define routes, and placing the specific pattern
before the general pattern (higher precedence), we can have `bazHandler` invoked
whenever the route `/foo/baz` is visited, and `genericHandler` for
anything else under `/foo`.

Note that if we were to reverse the order of the above routes, `genericHandler`
would be invoked for both `/foo/baz` and anything else under `/foo`, leaving
`bazHandler` unreachable.

## Options

As mentioned earlier, the `router` saga may also take a third argument, an
Expand Down
51 changes: 50 additions & 1 deletion __tests__/buildRouteMatcher.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import buildRouteMatcher from '../src/buildRouteMatcher';

test('creates a route matcher', () => {
test('creates a route matcher by object syntax', () => {
const rootRoute = () => 'root route';
const fooRoute = () => 'foo route';

Expand All @@ -19,6 +19,55 @@ test('creates a route matcher', () => {
expect(fooMatch.action).toBe(fooRoute);
});

test('creates a route matcher by array syntax, honouring route order', () => {
const fooRoute = () => 'foo route';
const fooBarRoute = () => 'foo bar route';

const descendingSpecificityMatcher = buildRouteMatcher([
{ pattern: '/foo/bar', handler: fooBarRoute },
{ pattern: '/foo/:arg', handler: fooRoute },
]);
const ascendingSpecificityMatcher = buildRouteMatcher([
{ pattern: '/foo/:arg', handler: fooRoute },
{ pattern: '/foo/bar', handler: fooBarRoute },
]);

const descendingFooMatch = descendingSpecificityMatcher.match('/foo/baz');
const descendingFooBarMatch = descendingSpecificityMatcher.match('/foo/bar');
const ascendingFooMatch = ascendingSpecificityMatcher.match('/foo/baz');
const ascendingFooBarMatch = ascendingSpecificityMatcher.match('/foo/bar');

expect(descendingFooMatch).not.toBe(null);
expect(descendingFooBarMatch).not.toBe(null);
expect(ascendingFooMatch).not.toBe(null);
expect(ascendingFooBarMatch).not.toBe(null);

expect(descendingFooMatch.action).toBe(fooRoute);
expect(descendingFooBarMatch.action).toBe(fooBarRoute);
expect(ascendingFooMatch.action).toBe(fooRoute);
expect(ascendingFooBarMatch.action).toBe(fooRoute);
expect(ascendingFooMatch.params.arg).toBe('baz');
expect(ascendingFooBarMatch.params.arg).toBe('bar');
});

test('throws an error when given incompatible values for routes', () => {
expect(() => {
buildRouteMatcher(null);
}).toThrow();

expect(() => {
buildRouteMatcher(undefined);
}).toThrow();

expect(() => {
buildRouteMatcher('string');
}).toThrow();

expect(() => {
buildRouteMatcher(1234);
}).toThrow();
});

test('recognizes fall-through pattern', () => {
const rootRoute = () => 'root route';
const fooRoute = () => 'foo route';
Expand Down
18 changes: 16 additions & 2 deletions src/buildRouteMatcher.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import ruta3 from 'ruta3';

function normalizeRoutes(routes) {
if (Array.isArray(routes)) {
return routes;
} else if (routes !== null && typeof routes === 'object') {
return Object.keys(routes).map((pattern) => ({ pattern, handler: routes[pattern] }));
}

throw new Error(
'Provided routes must either be an object in the form ' +
'{ [pattern]: handler }, or an array whose elements are objects in the ' +
'form { pattern: string, handler: function }.'
);
}

export default function buildRouteMatcher(routes) {
const routeMatcher = ruta3();

Object.keys(routes).forEach((route) => {
routeMatcher.addRoute(route, routes[route]);
normalizeRoutes(routes).forEach(({ pattern, handler }) => {
routeMatcher.addRoute(pattern, handler);
});

return routeMatcher;
Expand Down