From 557e5ec0b401bfebafc639afe28dd90b5d003b05 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 1 Dec 2017 15:44:38 +0000 Subject: [PATCH] Add basic routing. --- src/App.tsx | 47 +++++++++++++++++++++++++++++++++- src/routes.test.tsx | 61 +++++++++++++++++++++++++++++++++++++++++++++ src/routes.tsx | 47 ++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/routes.test.tsx create mode 100644 src/routes.tsx diff --git a/src/App.tsx b/src/App.tsx index b64b8dc..ea00d29 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,59 @@ import * as React from 'react'; import { HashRouter } from 'react-router-dom'; +import { Switch, Route } from 'react-router'; import './App.css'; import { JournalList } from './JournalList'; +import { RouteResolver, RouteKeysType } from './routes'; + +interface RouteItem { + path: string; + exact?: boolean; + keys?: RouteKeysType; + name: string; + component: React.ComponentType; +} + +const navigationRoutes: RouteItem[] = [ + { + path: 'home', + exact: true, + name: 'Home', + component: JournalList, + }, + { + path: 'journals._id', + exact: true, + name: 'Journal', + keys: {journalUid: 1}, + component: JournalList, + }, +]; + +const routeResolver = new RouteResolver({ + home: '', + journals: { + _base: 'journals', + _id: { + _base: ':journalUid', + }, + }, +}); class App extends React.Component { render() { return ( - + + {navigationRoutes.map((route, index) => ( + + ))} + ); } diff --git a/src/routes.test.tsx b/src/routes.test.tsx new file mode 100644 index 0000000..10e703a --- /dev/null +++ b/src/routes.test.tsx @@ -0,0 +1,61 @@ +import { RouteResolver } from './routes'; + +const routes = { + home: '', + post: { + _base: 'post', + _id: { + _base: ':postId', + comment: 'comment/:commentId', + revision: 'history/:revisionId/:someOtherVar/test', + }, + }, +}; + +const routeResolver = new RouteResolver(routes); + +it('translating routes', () => { + // Working basic resolves + expect(routeResolver.getRoute('home')).toBe('/'); + expect(routeResolver.getRoute('post')).toBe('/post'); + expect(routeResolver.getRoute('post._id')).toBe('/post/:postId'); + expect(routeResolver.getRoute('post._id.comment')).toBe('/post/:postId/comment/:commentId'); + + // Working translation resolves + expect(routeResolver.getRoute('home')).toBe('/'); + expect(routeResolver.getRoute('post')).toBe('/post'); + expect(routeResolver.getRoute('post._id', { postId: 3 })).toBe('/post/3'); + expect(routeResolver.getRoute('post._id.comment', + { postId: 3, commentId: 5 })).toBe('/post/3/comment/5'); + expect(routeResolver.getRoute('post._id.revision', + { postId: 3, revisionId: 5, someOtherVar: 'a' })).toBe('/post/3/history/5/a/test'); + + // Failing basic resolves + expect(() => { + routeResolver.getRoute('bad'); + }).toThrow(); + expect(() => { + routeResolver.getRoute('home.bad'); + }).toThrow(); + expect(() => { + routeResolver.getRoute('post._id.bad'); + }).toThrow(); + + // Failing translations + expect(() => { + routeResolver.getRoute('home', { test: 4 }); + }).toThrow(); + expect(() => { + routeResolver.getRoute('post._id', { test: 4 }); + }).toThrow(); + expect(() => { + routeResolver.getRoute('post._id', { postId: 3, test: 4 }); + }).toThrow(); + expect(() => { + routeResolver.getRoute('post._id.comment', { postId: 3, commentId: 5, test: 4 }); + }).toThrow(); + expect(() => { + routeResolver.getRoute('post._id.comment', { postId: 3 }); + }).toThrow(); +}); + diff --git a/src/routes.tsx b/src/routes.tsx new file mode 100644 index 0000000..c27093a --- /dev/null +++ b/src/routes.tsx @@ -0,0 +1,47 @@ +export type RouteKeysType = { [Identifier: string]: any }; + +export class RouteResolver { + routes: {}; + + constructor(routes: {}) { + this.routes = routes; + } + + getRoute(name: string, _keys?: RouteKeysType): string { + let dict = this.routes; + + let path: string[] = []; + name.split('.').forEach((key) => { + const val = (typeof dict[key] === 'string') ? dict[key] : dict[key]._base; + path.push(val); + + dict = dict[key]; + }); + + if (_keys) { + let keys = Object.assign({}, _keys); + + path = path.map((pathComponent) => { + return pathComponent.split('/').map((val) => { + if (val[0] === ':') { + const ret = keys[val.slice(1)]; + if (ret === undefined) { + throw new Error('Missing key: ' + val.slice(1)); + } + + delete keys[val.slice(1)]; + return ret; + } + + return val; + }).join('/'); + }); + + if (Object.keys(keys).length !== 0) { + throw new Error('Too many keys for route.'); + } + } + + return '/' + path.join('/'); + } +}