diff --git a/package.json b/package.json index 33849270..b74ea537 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ ], "license": "MIT", "dependencies": { + "@types/crossroads": "^0.0.29", "@types/js-cookie": "^2.0.28", "@types/mocha": "^2.2.41", "@types/node": "^7.0.22", @@ -70,6 +71,7 @@ "compression": "^1.6.2", "cpx": "^1.5.0", "cross-env": "^5.0.0", + "crossroads": "^0.12.2", "del": "^2.2.2", "del-cli": "^1.0.0", "eslint": "^3.19.0", diff --git a/src/lib/Router/Router.ts b/src/lib/Router/Router.ts new file mode 100644 index 00000000..175646e4 --- /dev/null +++ b/src/lib/Router/Router.ts @@ -0,0 +1,127 @@ +import crossroads from './crossroads'; + +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Subscription, } from 'rxjs/Subscription'; + +import { ViewContext } from '../ViewContext'; + +export type PathString = string; + +type OnRouteFn = (id?: string) => ViewContext; +export type Route = [PathString, OnRouteFn]; +export type RouteConfig = Array; + +export const enum RoutingActionType { + Push = 0, + Replace = 1, + StaticReload = 2, +} + +export interface RoutingAction { + type: RoutingActionType; + path: PathString; +} + +export class Router { + + private _router: typeof crossroads; + private _intent: Observable; + private _nextContext: Subject; + private _changedLocaton: Subject; + private _disposer: Subscription; + + private constructor(channel: Observable, config: RouteConfig) { + this._router = crossroads.create(); + this._intent = channel; + this._nextContext = new Subject(); + this._changedLocaton = new Subject(); + this._disposer = new Subscription(); + + this._setupRouting(config); + } + + static create(channel: Observable, routeConfig: RouteConfig): Router { + const r = new Router(channel, routeConfig); + return r; + } + + private _setupRouting(list: RouteConfig): void { + for (const [path, fn] of list) { + this._router.addRoute(path, (id: string) => { + const ctx: ViewContext = fn(id); + this._nextContext.next(ctx); + this._changedLocaton.next(path); + }); + } + + this._router.bypassed.add((aReq: PathString) => { + const e = new ReferenceError(`tried to route to the non-registered one: ${aReq}`); + this._nextContext.error(e); + console.error(e); + }); + } + + activate(): void { + const disposer = this._disposer; + + const intent = this._intent; + const nextPath = intent.subscribe((action) => { + this._router.parse(action.path); + }); + disposer.add(nextPath); + } + + destroy(): void { + this._disposer.unsubscribe(); + + this._changedLocaton.complete(); + this._changedLocaton.unsubscribe(); + + this._nextContext.complete(); + this._nextContext.unsubscribe(); + + // XXX: for destruction + // tslint:disable:no-any + this._disposer = null as any; + this._changedLocaton = null as any; + this._nextContext = null as any; + // tslint:enable + } + /** + * @returns + * This represents the actual path by + * The component class which displays a url subscribes this to observe + * a url which it should display. + */ + changedLocation(): Observable { + return this._changedLocaton.zip(this._intent, (_, action) => { + return action; + }); + } + + nextContext(): Observable { + return this._nextContext; + } +} + +export function toPathPushAction(path: PathString): RoutingAction { + return { + type: RoutingActionType.Push, + path, + }; +} + +export function toPathReplaceAction(path: PathString): RoutingAction { + return { + type: RoutingActionType.Replace, + path, + }; +} + +export function toStaticReloadAction(path: PathString): RoutingAction { + return { + type: RoutingActionType.StaticReload, + path, + }; +} diff --git a/src/lib/Router/crossroads.d.ts b/src/lib/Router/crossroads.d.ts new file mode 100644 index 00000000..01234968 --- /dev/null +++ b/src/lib/Router/crossroads.d.ts @@ -0,0 +1,7 @@ +// XXX: hack TypeScript module resolution to load d.ts which has legacy default export +// To avoid it, we can also use `allowSyntheticDefaultImports` compiler option, +// But we would not like to use at this moment. So we do this hack. +// See also `./crossroads.d.ts` in the same directory. +import CrossroadsJs = require('crossroads'); // tslint:disable-line:no-require-imports +declare const crossroads: typeof CrossroadsJs; +export default crossroads; // tslint:disable-line:no-default-export diff --git a/src/lib/Router/crossroads.js b/src/lib/Router/crossroads.js new file mode 100644 index 00000000..025fa610 --- /dev/null +++ b/src/lib/Router/crossroads.js @@ -0,0 +1,6 @@ +// XXX: hack TypeScript module resolution to load d.ts which has legacy default export +// To avoid it, we can also use `allowSyntheticDefaultImports` compiler option, +// But we would not like to use at this moment. So we do this hack. +// See also `./crossroads.js` in the same directory. +import crossroads from 'crossroads'; +export default crossroads; diff --git a/src/lib/Router/index.ts b/src/lib/Router/index.ts new file mode 100644 index 00000000..44f7dc8c --- /dev/null +++ b/src/lib/Router/index.ts @@ -0,0 +1 @@ +export * from './Router'; diff --git a/yarn.lock b/yarn.lock index 32f45f33..64af6eb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,12 @@ # yarn lockfile v1 +"@types/crossroads@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/crossroads/-/crossroads-0.0.29.tgz#f2308c106c68c4a84a0b23affa20989abbd38cb1" + dependencies: + "@types/signals" "*" + "@types/js-cookie@^2.0.28": version "2.0.28" resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.0.28.tgz#d6a9136d699257970fc74d899cd7b12a608d14da" @@ -28,6 +34,10 @@ version "15.0.24" resolved "https://registry.yarnpkg.com/@types/react/-/react-15.0.24.tgz#8a75299dc37906df327c18ca918bf97a55e7123b" +"@types/signals@*": + version "0.0.17" + resolved "https://registry.yarnpkg.com/@types/signals/-/signals-0.0.17.tgz#93911b0f09518f314a21805420143aab0c2f7ee4" + "@types/socket.io-client@^1.4.29": version "1.4.29" resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.29.tgz#f8743070cee93175e36e0b6a77a8af73e58ccb32" @@ -1258,6 +1268,12 @@ cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" +crossroads@^0.12.2: + version "0.12.2" + resolved "https://registry.yarnpkg.com/crossroads/-/crossroads-0.12.2.tgz#b1d5f9c1d98af3bdd61f1bda6a86dd1aee4ff8f2" + dependencies: + signals "<2.0" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -4665,6 +4681,10 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +signals@<2.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/signals/-/signals-1.0.0.tgz#65f0c1599352b35372ecaae5a250e6107376ed69" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"