Skip to content

Commit 24ee766

Browse files
committed
Improved router
WIP: The idea is to make it so that parameters are transmitted as atoms and the component function is only called when the route changes. If only parameters change, then the changes are propagated via the observables as usual.
1 parent ad9b154 commit 24ee766

File tree

5 files changed

+91
-66
lines changed

5 files changed

+91
-66
lines changed

src/public/components/router.js

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,64 @@
1+
import * as I from 'infestines'
12
import * as L from 'partial.lenses'
2-
import * as R from 'ramda'
33
import * as React from 'karet'
44
import * as U from 'karet.util'
5-
import toRegex from 'path-to-regexp'
6-
7-
const expandRoutes = L.collectAs((Component, route) => {
8-
const params = []
9-
const regex = toRegex(route, params)
10-
return {
11-
route,
12-
regex,
13-
keys: ['path'].concat(params.map(L.get('name'))),
14-
Component
15-
}
16-
}, L.values)
17-
18-
const sortStaticFirst = R.sortBy(({keys}) => (keys.length === 1 ? 0 : 1))
5+
import p2re from 'path-to-regexp'
196

20-
const prepareRoutes = R.o(sortStaticFirst, expandRoutes)
7+
export const prepare = L.collectAs((target, pattern) => {
8+
const {Component, ...props} = I.isFunction(target)
9+
? {Component: target}
10+
: target
11+
const formals = []
12+
const regexp = p2re(pattern, formals)
13+
return {...props, pattern, regexp, formals, Component}
14+
}, L.values)
2115

22-
const router = U.lift((routes, NotFound, path) => {
23-
for (let i = 0; i < routes.length; ++i) {
24-
const {Component, keys, regex} = routes[i]
25-
const match = regex.exec(path)
26-
if (match) return <Component {...R.zipObj(keys, match)} />
16+
export const disassemble = I.curry((routes, path) => {
17+
for (let i = 0, n = routes.length; i < n; ++i) {
18+
const route = routes[i]
19+
const matches = route.regexp.exec(path)
20+
if (null !== matches) {
21+
const match = {}
22+
const {formals} = route
23+
for (let i = 0, n = formals.length; i < n; ++i) {
24+
// XXX Handle optional and repeat parameters?
25+
const {name} = formals[i]
26+
const value = matches[i + 1]
27+
match[name] = undefined === value ? value : decodeURIComponent(value)
28+
}
29+
return {route, match}
30+
}
2731
}
28-
return <NotFound />
2932
})
3033

31-
export const Router = U.withContext(({routes, NotFound}, {path}) =>
32-
U.fromKefir(router(prepareRoutes(routes), NotFound, path))
33-
)
34+
const assemble = ({route: {pattern}, match}) => p2re.compile(pattern)(match)
35+
36+
const routerIso = (unpreparedRoutes, NotFound) =>
37+
L.iso(
38+
L.get([
39+
disassemble(prepare(unpreparedRoutes)),
40+
L.valueOr({route: {Component: NotFound}, match: {}})
41+
]),
42+
assemble
43+
)
44+
45+
export const Router = U.withContext(({routes, NotFound}, {params, path}) => {
46+
const resolved = U.view(routerIso(routes, NotFound), path)
47+
const matches = U.view('match', resolved)
48+
49+
let last, args, rendered
50+
51+
const render = U.lift(({route, match}) => {
52+
if (route !== last) {
53+
last = route
54+
for (const k in args) args[k]._onDeactivation()
55+
args = {}
56+
for (const k in match) args[k] = U.view(k, matches)
57+
const {Component} = route
58+
rendered = <Component {...{params, path}} {...args} />
59+
}
60+
return rendered
61+
})
62+
63+
return U.fromKefir(render(resolved))
64+
})

src/public/pages/examples/page.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import * as React from 'karet'
2-
import * as U from 'karet.util'
32

43
import {PathParams} from './path-params'
54
import {QuerystringParams} from './querystring-params'
65

7-
export const Examples = U.withContext((props, {params, path}) => (
6+
export const Examples = ({path, params, ...props}) => (
87
<div>
98
<h1>Examples</h1>
10-
<div>
11-
Routing / Path Params:
12-
<PathParams path={path} props={props} />
13-
</div>
14-
<div>
15-
Querystring Params:
16-
<QuerystringParams path={path} params={params} />
17-
</div>
9+
<PathParams {...{path, props}} />
10+
<QuerystringParams {...{path, params}} />
1811
</div>
19-
))
12+
)

src/public/pages/examples/path-params.js

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,35 @@ import * as L from 'partial.lenses'
22
import * as React from 'karet'
33
import * as U from 'karet.util'
44

5-
import {PathInput} from '../../components/restricted-input'
6-
import {PrettyStringify} from '../../components/pretty-stringify'
5+
import {TextInput} from '../../components/text-input'
76

8-
const getPagePathRoot = U.pipe(x => U.match(/(^\/[^/]+\/?).*/, x)[1])
9-
10-
const subPathGetter = pagePathRoot =>
11-
U.pipe(U.replace(pagePathRoot, ''), decodeURIComponent)
12-
13-
const subPathSetter = pagePathRoot =>
14-
U.pipe(
15-
U.map(U.when(x => x !== '/', encodeURIComponent)),
16-
U.join(''),
17-
U.concat(pagePathRoot),
18-
U.replace(pagePathRoot + '/', pagePathRoot)
19-
)
20-
21-
const subPathL = U.lift(pagePathRoot =>
22-
L.iso(subPathGetter(pagePathRoot), subPathSetter(pagePathRoot))
23-
)
24-
25-
const decodeProps = U.mapObjIndexed(decodeURIComponent)
26-
27-
export const PathParams = ({props, path}) => (
7+
export const PathParams = ({path, props}) => (
288
<div>
29-
<PathInput
30-
type="text"
31-
label="Path"
32-
value={U.view(subPathL(getPagePathRoot(path)), path)}
33-
/>
34-
<PrettyStringify value={decodeProps(props)} />
9+
<h2>Path params</h2>
10+
<pre className="pretty-stringify">{path}</pre>
11+
<table>
12+
<thead>
13+
<tr>
14+
<td>Key</td>
15+
<td>Value</td>
16+
</tr>
17+
</thead>
18+
<tbody>
19+
{L.collectAs(
20+
(value, key) => (
21+
<tr key={key}>
22+
<td>
23+
<code>{key}</code>
24+
</td>
25+
<td>
26+
<TextInput value={U.view(L.defaults(''), value)} />
27+
</td>
28+
</tr>
29+
),
30+
L.values,
31+
props
32+
)}
33+
</tbody>
34+
</table>
3535
</div>
3636
)

src/public/pages/examples/querystring-params.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const QuerystringParams = ({params, path, copy = U.atom()}) => {
4545
)
4646
return (
4747
<div>
48+
<h2>Querystring params</h2>
49+
<PrettyStringify value={params} />
4850
<table>
4951
<thead>
5052
<tr>
@@ -66,7 +68,6 @@ export const QuerystringParams = ({params, path, copy = U.atom()}) => {
6668
{U.scope((href = newPathString(path, copied)) => (
6769
<Link href={href}>{href}</Link>
6870
))}
69-
<PrettyStringify value={params} />
7071
</div>
7172
)
7273
}

src/public/routes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {Main} from './pages/main'
55
export const routes = {
66
'/': Main,
77
'/another-page': Main,
8-
'/examples/:pathParam1?/:pathParam2?/:pathParam3?/:splat*': Examples,
8+
'/examples/:pathParam1/:pathParam2/:pathParam3?/:splat*': Examples,
99
'/contacts': Contacts.Browse,
1010
'/contacts/new': Contacts.New,
1111
'/contacts/:id': Contacts.Edit

0 commit comments

Comments
 (0)