├── .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 | [](https://travis-ci.org/funkia/rudolph)
8 | [](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 |
--------------------------------------------------------------------------------