Skip to content

Commit

Permalink
feat(ui-router): Convert to TypeScript and implement ui-router builder
Browse files Browse the repository at this point in the history
  • Loading branch information
wms committed Feb 22, 2017
1 parent bfd9799 commit c4251b5
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 138 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
**/*.js
**/*.d.ts
node_modules
5 changes: 5 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**/*.ts
**/*.tsx
**/*.spec.*
**/__mocks__/**
**/__snapshots__/**
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,72 @@ Without any custom options or mutations, the webpack configuration will provide
- Hot Module Reloading (using React Hot Loader 3.0 Beta 6)
- TypeScript 2.0 (using Awesome TypeScript Loader)
- Entry point: `src/index.tsx`
- Babel compilation step (required by Ant Design's homebrew tree-shaking strategy)
- Less compilation
- .less files ending in `.inline.less` will be embedded into `index.html`, useful for styling the IPL
- Dev Server configuration
- Sets up a proxy to a backend API running at `localhost:8080` at `/api`

# UI-Router

Create a UI-Router instance pre-configured with all of our favourite things using `basalplatten/ui-router`:

```javascript
// AppEntryPoint.jsx
const {UIRouter} = require('ui-router-react');
const {buildRouter} = require('basalplatten/ui-router');

var router = buildRouter();
router.stateRegistry.register({
// ... your state definitions
});

<UIRouter router={router}>
<UIView/>
</UIRouter>
```

The `buildRouter` factory provides you with a UI-Router instance preconfigured with our favourite things:

- Reactive Extensions plugin so that you can `.observe()` state parameter changes directly from components
- A default error handler that displays an [Error Notification](https://ant.design/components/notification/) when a state transition fails
- A URL routing handler that display a Notification and redirects to `/` when attempting to access a URL which does not match a state
- Custom parameter types

# UI-Router Parameter Types

A handful of custom parameter types that are useful for serializing state such as filter and order criteria from data tables into the URL. We use these types over the built-on `json` type as they're a little friendlier on the (human) eye.

If you use the `buildRouter` factory function in `basalplatten/ui-router`, then these types are already registered with the router instance and ready to use. Otherwise, you'll need to register them manually:

```javascript
const {where} = require('basalplatten/ui-router/paramTypes');

router.urlMatcherFactory.type('where', where);
```

### `where`

A parameter with the `where` type and following value:

```json
{
"user_id": "13",
"owner_id": "37",
"status": ["completed", "failed"]
}
```

Will be encoded into the URL as `?user_id:13!owner_id:37!~status:completed,failed`.

### `order`

A parameter with the `order` type and following value:

```json
{
"created_at": "desc"
}
```

Will be encoded into the URL as `?created_at:desc`.
4 changes: 4 additions & 0 deletions __mocks__/antd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const notification = {
error: jest.fn(),
info: jest.fn()
}
12 changes: 12 additions & 0 deletions __snapshots__/ui-router.spec.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`adds a URL redirection handler 1`] = `"The URL blank does not exist. You have been redirected to the home page."`;

exports[`adds a default error handler 1`] = `
<div>
<p>
The following error was encountered during a UI state transition:
</p>
<code />
</div>
`;
18 changes: 17 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
"description": "Core components for building UIs the Fountainhead way",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "$(npm bin)/tsc --declaration"
},
"author": "Warren Seymour",
"license": "MIT",
"dependencies": {
"@types/jest": "^18.1.1",
"@types/node": "^7.0.5",
"@types/ramda": "0.0.3",
"@types/react": "^15.0.11",
"@types/react-dom": "^0.14.23",
Expand All @@ -23,6 +26,7 @@
"callsite": "^1.0.0",
"css-loader": "^0.26.1",
"html-webpack-plugin": "^2.28.0",
"jest": "^19.0.0",
"less-loader": "^2.2.3",
"ramda": "^0.23.0",
"react": "^15.4.2",
Expand All @@ -32,6 +36,7 @@
"semantic-release-gitlab": "^3.0.2",
"style-ext-html-webpack-plugin": "^2.0.6",
"style-loader": "^0.13.1",
"ts-jest": "^19.0.0",
"typescript": "^2.1.6",
"ui-router-react": "^0.4.0",
"ui-router-rx": "^0.2.1",
Expand Down Expand Up @@ -62,5 +67,16 @@
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"jest": {
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
]
}
}
13 changes: 13 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"sourceMap": false,
"jsx": "react",
"skipLibCheck": true
},
"exclude": [
"node_modules"
]
}
50 changes: 50 additions & 0 deletions ui-router.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {buildRouter} from './ui-router';
import {notification} from 'antd';
import * as paramTypes from './ui-router/paramTypes';

it('adds a default error handler', async () => {
var router = buildRouter();

router.stateRegistry.register({
name: 'badState',
resolve: {
willThrow: () => {
throw 'whoops';
}
}
});

try {
await router.stateService.go('badState');
} catch (e) {}

expect(notification.error).toHaveBeenCalled();
var options = (notification.error as jest.Mock<any>).mock.calls[0][0];

expect(options.message).toBe('State Transition Error');
expect(options.description).toMatchSnapshot();
});

it('adds a URL redirection handler', () => {
var router = buildRouter();

try {
window.location.pathname = '/test';
router.urlRouter.sync();
} catch (e) {}

expect(notification.info).toHaveBeenCalled();
var options = (notification.info as jest.Mock<any>).mock.calls[0][0];

expect(options.message).toBe('Unknown URL');
expect(options.description).toMatchSnapshot();
});

it('registers custom parameter type definitions', () => {
var router = buildRouter();

var registeredTypes = Object.keys(router.urlMatcherFactory.paramTypes.types);
var definedTypes = Object.keys(paramTypes);

expect(registeredTypes).toEqual(definedTypes);
});
45 changes: 45 additions & 0 deletions ui-router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react';
import {UIRouterReact, UIRouter, UIView, servicesPlugin, pushStateLocationPlugin} from 'ui-router-react';
import {ParamTypeDefinition, UrlParts} from 'ui-router-core';
import {UIRouterRx} from 'ui-router-rx';
import {notification} from 'antd';
import * as paramTypes from './ui-router/paramTypes';

export const buildRouter = (): UIRouterReact => {
var router = new UIRouterReact();

router.plugin(servicesPlugin);
router.plugin(pushStateLocationPlugin);
router.plugin(UIRouterRx);

router.stateService.defaultErrorHandler(err => {
notification.error({
message: 'State Transition Error',

description: (
<div>
<p>The following error was encountered during a UI state transition:</p>
<code>{err.stack}</code>
</div>
),

duration: null
});
});

router.urlRouter.otherwise((matchValue, url, router) => {
notification.info({
message: 'Unknown URL',
description: `The URL ${url.path} does not exist. You have been redirected to the home page.`
});

return '/';
});

Object.keys(paramTypes).forEach(name => {
router.urlMatcherFactory.type(name, paramTypes[name]);
});

return router;
}

45 changes: 45 additions & 0 deletions ui-router/paramTypes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as paramTypes from './paramTypes';

describe('where', () => {
var obj = {
user_id: '13',
owner_id: '37',
status: ['completed', 'failed']
};

var str = 'user_id:13!owner_id:37!~status:completed,failed';

it('encodes an object to a string', () => {
expect(paramTypes.where.encode(obj)).toBe(str);
});

it('decodes a string to an object', () => {
expect(paramTypes.where.decode(str)).toEqual(obj);
});

it('matches the regex pattern', () => {
expect(str).toMatch(paramTypes.where.pattern);
expect('test').not.toMatch(paramTypes.where.pattern);
});
});

describe('order', () => {
var obj = {
created_at: 'desc'
};

var str = 'created_at:desc';

it('encodes an object to a string', () => {
expect(paramTypes.order.encode(obj)).toBe(str);
});

it('decodes a string to an object', () => {
expect(paramTypes.order.decode(str)).toEqual(obj);
});

it('matches the regex pattern', () => {
expect(str).toMatch(paramTypes.order.pattern);
expect('test').not.toMatch(paramTypes.order.pattern);
});
});
Loading

0 comments on commit c4251b5

Please sign in to comment.