├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── examples ├── package.json ├── recursive │ ├── index.html │ ├── src │ │ ├── app.ts │ │ └── index.ts │ └── tsconfig.json ├── simple │ ├── index.html │ ├── src │ │ ├── app.ts │ │ └── index.ts │ └── tsconfig.json └── tsconfig.json ├── jest.config.js ├── package.json ├── src └── router.ts ├── test └── router.spec.ts ├── tsconfig-release.json ├── tsconfig-test.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .cache -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint", "jest"], 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier", 9 | "prettier/@typescript-eslint", 10 | "plugin:jest/recommended", 11 | ], 12 | rules: { 13 | "@typescript-eslint/explicit-module-boundary-types": "off", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | bundle.js 3 | *.map 4 | node_modules/ 5 | *~ 6 | .tern-port 7 | .#* 8 | dist/ 9 | generated/ 10 | coverage/ 11 | .vscode 12 | yarn.lock 13 | .cache 14 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | docs 4 | coverage 5 | yarn.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | after_success: 5 | - npm run codecov 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Rudolph 4 | A pure and functional router using classic FRP. Written in TypeScript. 5 | Experimental. 6 | 7 | [![Build Status](https://travis-ci.org/funkia/rudolph.svg?branch=master)](https://travis-ci.org/funkia/rudolph) 8 | [![codecov](https://codecov.io/gh/funkia/rudolph/branch/master/graph/badge.svg)](https://codecov.io/gh/funkia/rudolph) 9 | 10 | 11 | ## Install 12 | ``` 13 | npm install --save @funkia/rudolph @funkia/hareactive 14 | ``` 15 | 16 | ## API 17 | 18 | ### Router 19 | 20 | ```ts 21 | type Router = { 22 | prefixPath: string; 23 | path: Behavior; 24 | useHash: boolean; 25 | }; 26 | ``` 27 | 28 | ### createRouter 29 | 30 | Takes a configuration Object describing how to handle the routing: 31 | 32 | * `useHash: boolean` - whether to use hash-routing 33 | * `path: Behavior` - defaults to `locationHashB` or `locationB` 34 | 35 | It errors if `useHash = true` but hash-routing is unsupported in that browser, or if there is no support for the history API. 36 | 37 | The returned Router object is identical to its input, augmented with `prefixPath: ""`, which is used to nest routers. 38 | 39 | Usage: 40 | 41 | ```ts 42 | const router = createRouter({ 43 | useHash: false 44 | }); 45 | 46 | runComponent("#mount", main({ router })); 47 | ``` 48 | 49 | ### navigate 50 | 51 | ```ts 52 | navigate(router: Router, pathStream: Stream): Now> 53 | ``` 54 | 55 | `navigate` takes a stream of paths. Whenever the stream has an occurence, it is navigated to. 56 | 57 | Usage: 58 | 59 | ```ts 60 | const navs: Stream = userIds 61 | .map(prefix("/user/")) 62 | .combine(on.homeClicks.mapTo("/")); 63 | 64 | start(navigate(props.router, navs)); 65 | ``` 66 | 67 | ### routePath 68 | 69 | `routePath(routes: Routes, router: Router): Behavior` 70 | 71 | Takes a description of the routes and a router, and returns a behavior with the result of parsing the router's location according to the routes' pattern. 72 | 73 | The first parameter, `routes: Routes`, is a description of the routes, in the form: 74 | 75 | ```ts 76 | {"/route/:urlParam"; (restUrl, params) => result} 77 | ``` 78 | 79 | Usage: 80 | 81 | ```ts 82 | E.section( 83 | routePath( 84 | { 85 | "/user/:userId": (_subrouter, { userId }) => user(userId), 86 | "/": () => home, 87 | "*": () => notFound, 88 | }, 89 | props.router 90 | ) 91 | ) 92 | ``` 93 | 94 | ### Routes 95 | 96 | ```ts 97 | type Routes = Record> 98 | ``` 99 | 100 | Example: 101 | 102 | ```ts 103 | { 104 | "/user/:userId": (_subrouter, { userId }) => user(userId), 105 | "/": () => home, 106 | "*": () => notFound, 107 | } 108 | ``` 109 | 110 | ### RouteHandler 111 | 112 | ```ts 113 | type RouteHandler = ( 114 | router: Router, 115 | params: Record 116 | ) => A; 117 | ``` 118 | 119 | ### locationHashB 120 | 121 | `locationHashB: Behavior` represents the current values of the URL hash. 122 | 123 | ### locationB 124 | 125 | `locationHashB: Behavior` represents the current values of the URL pathname. 126 | 127 | ### navigateHashIO 128 | 129 | `navigateHashIO: (path: string) => IO` is an `IO` effect that updates the URL hash to the supplied argument. 130 | 131 | ### navigateIO 132 | 133 | `navigateIO: (path: string) => IO` is an `IO` effect that updates the URL pathname to the supplied argument. 134 | 135 | ### warnNavigation 136 | 137 | Takes a behavior of a boolean, if true the user will have to confirm before unloading page. 138 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "funnel-simple-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "simple": "parcel simple/index.html", 8 | "recursive": "parcel recursive/index.html" 9 | }, 10 | "author": "Funkia", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@funkia/turbine": "^0.4.0" 14 | }, 15 | "devDependencies": { 16 | "parcel-bundler": "^1.12.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/recursive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/recursive/src/app.ts: -------------------------------------------------------------------------------- 1 | import { combine } from "@funkia/jabz"; 2 | import { Stream } from "@funkia/hareactive"; 3 | import { 4 | elements as E, 5 | dynamic, 6 | Component, 7 | toComponent, 8 | component, 9 | } from "@funkia/turbine"; 10 | import { navigate, routePath, Router } from "../../../src/router"; 11 | 12 | const file = (filename: string) => 13 | toComponent([ 14 | E.h1("File: " + filename), 15 | E.span( 16 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed lacinia libero id massa semper, sed maximus diam venenatis." 17 | ), 18 | ]); 19 | 20 | const style: Partial = { 21 | border: "1px solid black", 22 | padding: "15px", 23 | }; 24 | 25 | type On = { 26 | A: Stream; 27 | B: Stream; 28 | C: Stream; 29 | D: Stream; 30 | }; 31 | 32 | type Props = { 33 | directoryName: string; 34 | router: Router; 35 | }; 36 | 37 | const directory = (props: Props): Component => 38 | component((on, start) => { 39 | const navs = combine( 40 | on.A.mapTo("/d/A"), 41 | on.B.mapTo("/d/B"), 42 | on.C.mapTo("/d/C"), 43 | on.D.mapTo("/f/D") 44 | ); 45 | start(navigate(props.router, navs)); 46 | 47 | return E.div([ 48 | E.span(`Directory: ${props.directoryName} is containing:`), 49 | E.div([ 50 | E.button("dir A").use({ A: "click" }), 51 | E.button("dir B").use({ B: "click" }), 52 | E.button("dir C").use({ C: "click" }), 53 | E.button("file D").use({ D: "click" }), 54 | ]), 55 | E.div( 56 | { style }, 57 | dynamic( 58 | routePath( 59 | { 60 | "/d/:dirname": (subrouter, { dirname }) => 61 | directory({ router: subrouter, directoryName: dirname }), 62 | "/f/:filename": (_, { filename }) => file(filename), 63 | "*": () => Component.of({}), 64 | }, 65 | props.router 66 | ) 67 | ) 68 | ), 69 | ]); 70 | }); 71 | 72 | export const main = ({ router }: Props) => 73 | directory({ router, directoryName: "root" }); 74 | -------------------------------------------------------------------------------- /examples/recursive/src/index.ts: -------------------------------------------------------------------------------- 1 | import { runComponent } from "@funkia/turbine"; 2 | import { main } from "./app"; 3 | import { createRouter } from "../../../src/router"; 4 | 5 | const router = createRouter({ 6 | useHash: true 7 | }); 8 | runComponent("#mount", main({ router })); 9 | -------------------------------------------------------------------------------- /examples/recursive/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include":[ 4 | "src/**/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/simple/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Behavior, Stream, snapshot } from "@funkia/hareactive"; 2 | import { elements as E, toComponent, component } from "@funkia/turbine"; 3 | import { navigate, routePath, Router } from "../../../src/router"; 4 | 5 | const prefix = (pre: string) => (str: string) => pre + str; 6 | 7 | const user = (userId: string) => 8 | toComponent([ 9 | E.h1("User"), 10 | E.span(`Here you see the profile of user: ${userId}`), 11 | ]); 12 | 13 | const home = toComponent([E.h1("Home"), E.span("Here is your home screen.")]); 14 | 15 | const notFound = toComponent([ 16 | E.h1("404: Page not found"), 17 | E.span("Nothing to find here..."), 18 | ]); 19 | 20 | type On = { 21 | userClicks: Stream; 22 | homeClicks: Stream; 23 | userId: Behavior; 24 | }; 25 | 26 | type Props = { 27 | router: Router; 28 | }; 29 | 30 | const menu = (props: Props) => 31 | component((on, start) => { 32 | const userIds = snapshot(on.userId, on.userClicks).log("userIds"); 33 | const navs: Stream = userIds 34 | .map(prefix("/user/")) 35 | .combine(on.homeClicks.mapTo("/")); 36 | start(navigate(props.router, navs)); 37 | 38 | return E.div([ 39 | E.div([ 40 | E.button("Home").use({ homeClicks: "click" }), 41 | E.button("Find User:").use({ userClicks: "click" }), 42 | E.input().use({ userId: "value" }), 43 | ]), 44 | E.section( 45 | routePath( 46 | { 47 | "/user/:userId": (_subrouter, { userId }) => user(userId), 48 | "/": () => home, 49 | "*": () => notFound, 50 | }, 51 | props.router 52 | ) 53 | ), 54 | ]); 55 | }); 56 | 57 | export const main = menu; 58 | -------------------------------------------------------------------------------- /examples/simple/src/index.ts: -------------------------------------------------------------------------------- 1 | import { runComponent } from "@funkia/turbine"; 2 | import { main } from "./app"; 3 | import { createRouter } from "../../../src/router"; 4 | 5 | const router = createRouter({ 6 | useHash: false 7 | }); 8 | 9 | runComponent("#mount", main({ router })); 10 | -------------------------------------------------------------------------------- /examples/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include":[ 4 | "src/**/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "lib": ["dom", "es5", "es2015.core", "es2015.promise", "es2015.iterable", "es2015.proxy"] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | testMatch: [ "**/test/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" ], 5 | testPathIgnorePatterns: [ "/node_modules/", "/test/helpers.ts" ], 6 | coveragePathIgnorePatterns: [ "/node_modules/", "/test/helpers.ts" ] 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funkia/rudolph", 3 | "version": "0.1.0", 4 | "description": "A purely functional router based on Hareactive", 5 | "main": "dist/router.js", 6 | "module": "dist/es2015/router.js", 7 | "typings": "dist/defs/router.d.ts", 8 | "directories": { 9 | "dist": "dist" 10 | }, 11 | "scripts": { 12 | "test": "jest --coverage", 13 | "lint": "eslint 'src/**/*.ts'", 14 | "typecheck": "tsc --noEmit", 15 | "codecov": "codecov -f coverage/**/json", 16 | "build": "npm run build-cmjs; npm run build-es2015", 17 | "build-es2015": "tsc -P ./tsconfig-release.json --outDir 'dist/es2015' --module es6 --moduleResolution node", 18 | "build-cmjs": "tsc -P ./tsconfig-release.json", 19 | "clean": "rm -rf coverage dist", 20 | "format": "prettier --write \"{src,test,examples}/**/*.ts\"", 21 | "formatcheck": "prettier --check {src,test,examples}/**/*", 22 | "prepublishOnly": "npm run clean; npm run build", 23 | "release": "np" 24 | }, 25 | "author": "Funkia", 26 | "license": "MIT", 27 | "homepage": "https://github.com/funkia/rudolph#readme", 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/funkia/rudolph.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/funkia/rudolph/issues" 34 | }, 35 | "keywords": [ 36 | "pure", 37 | "functional", 38 | "router", 39 | "frp", 40 | "functional reactive programming", 41 | "typescript" 42 | ], 43 | "dependencies": { 44 | "@funkia/io": "0.0.5" 45 | }, 46 | "peerDependencies": { 47 | "@funkia/hareactive": "0.3.2" 48 | }, 49 | "devDependencies": { 50 | "@funkia/hareactive": "^0.4.0", 51 | "@types/chai": "^4.2.14", 52 | "@types/jest": "^26.0.20", 53 | "@types/sinon": "^9.0.10", 54 | "@typescript-eslint/eslint-plugin": "^4.14.0", 55 | "@typescript-eslint/parser": "^4.14.0", 56 | "chai": "^4.2.0", 57 | "codecov": "^3.8.1", 58 | "eslint": "^7.18.0", 59 | "eslint-config-prettier": "^7.2.0", 60 | "eslint-plugin-jest": "^24.1.3", 61 | "jest": "^26.6.3", 62 | "np": "^7.2.0", 63 | "prettier": "^2.2.1", 64 | "sinon": "^9.2.3", 65 | "ts-jest": "^26.4.4", 66 | "typescript": "^4.1.3" 67 | }, 68 | "prettier": { 69 | "arrowParens": "always" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import { withEffects } from "@funkia/io"; 2 | import { 3 | Behavior, 4 | Now, 5 | Stream, 6 | performStream, 7 | snapshotWith, 8 | SinkBehavior, 9 | flatFuturesOrdered 10 | } from "@funkia/hareactive"; 11 | import { streamFromEvent, behaviorFromEvent } from "@funkia/hareactive/dom"; 12 | 13 | export type ParamBehavior = Behavior>; 14 | 15 | function fst
(arr: A[]): A; 16 | function fst(arr: string): string; 17 | function fst(arr: A[] | string): A | string { 18 | return arr[0]; 19 | } 20 | 21 | function takeUntilRight(stop: string, str: string): string { 22 | return str.substr(str.indexOf(stop) + 1); 23 | } 24 | 25 | function isEqual(obj1: any, obj2: any): boolean { 26 | return Object.keys(obj1).every( 27 | (key) => key in obj2 && obj1[key] === obj2[key] 28 | ); 29 | } 30 | 31 | export type Router = { 32 | prefixPath: string; 33 | path: Behavior; 34 | useHash: boolean; 35 | }; 36 | 37 | /** 38 | * Takes a configuration Object describing how to handle the routing. 39 | * @param config An Object containing the router basic router 40 | * configurations. 41 | */ 42 | export function createRouter({ 43 | useHash = false, 44 | path = useHash ? locationHashB : locationB 45 | }: Partial): Router { 46 | const supportHistory = "history" in window && "pushState" in window.history; 47 | const supportHash = "onhashchange" in window; 48 | if (useHash && !supportHash) { 49 | throw new Error("No support for hash-routing."); 50 | } else if (!supportHistory) { 51 | throw new Error("No support for history API."); 52 | } 53 | return { 54 | prefixPath: "", 55 | path, 56 | useHash 57 | }; 58 | } 59 | 60 | export const locationHashB = behaviorFromEvent( 61 | window, 62 | "hashchange", 63 | (w) => takeUntilRight("#", w.location.hash) || "/", 64 | (evt) => takeUntilRight("#", evt.newURL) 65 | ); 66 | 67 | export const locationB = behaviorFromEvent( 68 | window, 69 | "popstate", 70 | (w) => w.location.pathname, 71 | (_e) => window.location.pathname 72 | ); 73 | 74 | const navigateHashIO = withEffects((path: string) => { 75 | window.location.hash = path; 76 | }); 77 | const navigateIO = withEffects((path: string) => { 78 | (>locationB).newValue(path); 79 | window.history.pushState({}, "", path); 80 | }); 81 | 82 | /** 83 | * Takes a stream of paths. Whenever the stream has an occurrence it 84 | * is navigated to. 85 | * @param pathStream A stream of paths. 86 | */ 87 | export function navigate( 88 | router: Router, 89 | pathStream: Stream 90 | ): Now> { 91 | const newUrl = pathStream.map((path) => router.prefixPath + path); 92 | const navigateFn = router.useHash ? navigateHashIO : navigateIO; 93 | return performStream(newUrl.map(navigateFn)); 94 | } 95 | 96 | type ParsedPathPattern = { 97 | path: string[]; 98 | params: Record; 99 | length: number; 100 | handler: RouteHandler; 101 | }; 102 | 103 | export type RouteHandler = ( 104 | router: Router, 105 | params: Record 106 | ) => A; 107 | 108 | function parsePathPattern( 109 | pattern: string, 110 | handler: RouteHandler 111 | ): ParsedPathPattern { 112 | const patternParts = pattern.split("/"); 113 | const p: ParsedPathPattern = { 114 | path: [], 115 | params: {}, 116 | length: patternParts.length, 117 | handler 118 | }; 119 | if (pattern === "*") { 120 | return p; 121 | } 122 | for (let i = 0; i < patternParts.length; i++) { 123 | const part = patternParts[i]; 124 | if (fst(part) === ":") { 125 | p.params[part.substr(1)] = i; 126 | } else { 127 | p.path[i] = part; 128 | } 129 | } 130 | return p; 131 | } 132 | 133 | export type Routes = Record>; 134 | 135 | /** 136 | * Takes a description of the routes, and a router 137 | * and returns a behavior with the result of parsing the 138 | * location according to the pattern. 139 | * @param routes A description of the routes, in the form 140 | * {"/route/:urlParam"; (restUrl, params) => result} 141 | */ 142 | export function routePath(routes: Routes, router: Router): Behavior { 143 | const parsedRoutes = Object.keys(routes).map((path) => 144 | parsePathPattern(path, routes[path]) 145 | ); 146 | let lastMatch: ParsedPathPattern | undefined; 147 | let result: A; 148 | let lastParams: Record; 149 | let lastRouter: Router; 150 | return router.path.map((location) => { 151 | const locationParts = location.split("/"); 152 | const match = parsedRoutes.find(({ path }: ParsedPathPattern) => 153 | path.every((part, index) => { 154 | return part === locationParts[index]; 155 | }) 156 | )!; 157 | 158 | 159 | const params = Object.keys(match.params).reduce>((paramsAcc, key) => { 160 | paramsAcc[key] = locationParts[match.params[key]]; 161 | return paramsAcc; 162 | }, {}); 163 | 164 | if (match !== lastMatch) { 165 | lastMatch = match; 166 | // const rest = "/" + locationParts.slice(match.length).join("/"); 167 | const matchedPath = locationParts.slice(0, match.length).join("/"); 168 | 169 | const newRouter: Router = { 170 | prefixPath: router.prefixPath + matchedPath, 171 | path: router.path.map((l) => l.slice(matchedPath.length)), 172 | useHash: router.useHash 173 | }; 174 | 175 | lastParams = params; 176 | lastRouter = newRouter; 177 | result = match.handler(newRouter, params); 178 | } else if (!isEqual(lastParams, params)) { 179 | lastParams = params; 180 | result = match.handler(lastRouter, params); 181 | } 182 | return result; 183 | }); 184 | } 185 | 186 | export const beforeUnload = streamFromEvent(window, "beforeunload"); 187 | 188 | const preventNavigationIO = withEffects( 189 | (event: WindowEventMap["beforeunload"], shouldWarn: boolean) => { 190 | if (shouldWarn) { 191 | event.returnValue = "o/"; 192 | return "o/"; 193 | } 194 | } 195 | ); 196 | 197 | /** 198 | * Takes a behavior of a boolean, if true the user will have to confirm before unloading page. 199 | * @param shouldWarnB A behavior of a boolean 200 | */ 201 | export function warnNavigation( 202 | shouldWarnB: Behavior 203 | ): Now> { 204 | const a = snapshotWith(preventNavigationIO, shouldWarnB, beforeUnload); 205 | return performStream(a).chain(flatFuturesOrdered); 206 | } 207 | -------------------------------------------------------------------------------- /test/router.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { spy } from "sinon"; 3 | import * as H from "@funkia/hareactive"; 4 | import * as R from "../src/router"; 5 | 6 | describe("routePath", () => { 7 | it("should change the behavior according to the path", () => { 8 | const path = H.sinkBehavior("/admin"); 9 | const router = R.createRouter({ path }); 10 | const content = R.routePath( 11 | { 12 | "/": () => 1, 13 | "/user": () => 2, 14 | "/admin": () => 3 15 | }, 16 | router 17 | ); 18 | 19 | assert.strictEqual(content.at(), 3); 20 | path.push("/user"); 21 | assert.strictEqual(content.at(), 2); 22 | path.push("/"); 23 | assert.strictEqual(content.at(), 1); 24 | }); 25 | 26 | it('should use the "*" route as fallback', () => { 27 | const path = H.sinkBehavior("/cats"); 28 | const router = R.createRouter({ path }); 29 | const content = R.routePath( 30 | { 31 | "/": () => 1, 32 | "/user": () => 2, 33 | "/admin": () => 3, 34 | "*": () => 404 35 | }, 36 | router 37 | ); 38 | assert.strictEqual(content.at(), 404); 39 | path.push("/user"); 40 | assert.strictEqual(content.at(), 2); 41 | path.push("/dogs"); 42 | assert.strictEqual(content.at(), 404); 43 | }); 44 | 45 | it("should support subrouting", () => { 46 | const path = H.sinkBehavior("/"); 47 | const router = R.createRouter({ path }); 48 | 49 | const subRoute = (subrouter: R.Router) => 50 | R.routePath( 51 | { 52 | "/admin": () => 2, 53 | "/user": () => 3, 54 | "*": () => 0 55 | }, 56 | subrouter 57 | ); 58 | 59 | const content = R.routePath( 60 | { 61 | "/": () => H.Behavior.of(1), 62 | "/company": subRoute, 63 | "*": () => H.Behavior.of(404) 64 | }, 65 | router 66 | ); 67 | 68 | assert.strictEqual(content.flat().at(), 1); 69 | path.push("/company"); 70 | assert.strictEqual(content.flat().at(), 0); 71 | path.push("/company/admin"); 72 | assert.strictEqual(content.flat().at(), 2); 73 | }); 74 | 75 | it("should only call the subRouteHandler if only subroute changed", () => { 76 | const path = H.sinkBehavior("/company/admin"); 77 | const router = R.createRouter({ path }); 78 | const topRender = spy(); 79 | const subRender = spy(); 80 | 81 | const subRoute = (subrouter: R.Router) => 82 | R.routePath( 83 | { 84 | "/admin": () => { 85 | subRender(); 86 | return 1; 87 | }, 88 | "/user": () => { 89 | subRender(); 90 | return 2; 91 | } 92 | }, 93 | subrouter 94 | ); 95 | 96 | const content = R.routePath( 97 | { 98 | "/company": (r) => { 99 | topRender(); 100 | return subRoute(r); 101 | } 102 | }, 103 | router 104 | ); 105 | 106 | content.flat().at(); 107 | assert.strictEqual(topRender.callCount, 1); 108 | assert.strictEqual(subRender.callCount, 1); 109 | path.push("/company/user"); 110 | content.flat().at(); 111 | assert.strictEqual(topRender.callCount, 1); 112 | assert.strictEqual(subRender.callCount, 2); 113 | }); 114 | 115 | it('should parse "/:name" and pass as route parameters', () => { 116 | const path = H.sinkBehavior("/user/john123"); 117 | const router = R.createRouter({ path }); 118 | 119 | let params: any; 120 | 121 | const content = R.routePath( 122 | { 123 | "/user/:userId": (_, p) => { 124 | params = p; 125 | return 1; 126 | }, 127 | "/admin": (_, p) => { 128 | params = p; 129 | return 2; 130 | }, 131 | "*": (_, p) => { 132 | params = p; 133 | return 0; 134 | } 135 | }, 136 | router 137 | ); 138 | 139 | content.subscribe(() => ""); // Needed to activate the reactive 140 | assert.deepEqual(params, { userId: "john123" }); 141 | path.push("/admin"); 142 | assert.deepEqual(params, {}); 143 | path.push("/user/jack321/profile"); 144 | assert.deepEqual(params, { userId: "jack321" }); 145 | }); 146 | 147 | it('should recall the RouteHandler if "/:name" changes with new value', () => { 148 | const path = H.sinkBehavior("/user/john123"); 149 | const router = R.createRouter({ path }); 150 | 151 | let params: any; 152 | 153 | const content = R.routePath( 154 | { 155 | "/user/:userId": (_, p) => { 156 | params = p; 157 | return 1; 158 | }, 159 | "/admin": (_, p) => { 160 | params = p; 161 | return 2; 162 | }, 163 | "*": (_, p) => { 164 | params = p; 165 | return 0; 166 | } 167 | }, 168 | router 169 | ); 170 | 171 | content.subscribe(() => ""); // Needed to activate the reactive 172 | assert.deepEqual(params, { userId: "john123" }); 173 | path.push("/user/jack321/profile"); 174 | assert.deepEqual(params, { userId: "jack321" }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /tsconfig-release.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationDir": "./dist/defs", 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "noImplicitAny": false, 10 | "sourceMap": true, 11 | "experimentalDecorators": true, 12 | "lib": ["dom", "es5", "es2015.core", "es2015.promise", "es2015.iterable"] 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } -------------------------------------------------------------------------------- /tsconfig-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "outDir": "tmp", 7 | "rootDir": ".", 8 | "target": "ES2015", 9 | "types" : [ 10 | "mocha" 11 | ] 12 | }, 13 | "include": [ 14 | "src/**/*.ts", 15 | "test/**/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "strict": true, 7 | "experimentalDecorators": true, 8 | "lib": ["dom"] 9 | }, 10 | "include":[ 11 | "src/**/*" 12 | ] 13 | } 14 | --------------------------------------------------------------------------------