Skip to content

Commit ba204ac

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 8b3981a commit ba204ac

17 files changed

+215
-134
lines changed

src/client/request.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as Kefir from 'kefir'
22
import * as L from 'kefir.partial.lenses'
33
import * as R from 'ramda'
44

5-
import paramsI from '../shared/search-params'
5+
import {paramsI} from '../shared/search-params'
66

77
export const withParams = /*#__PURE__*/ R.curry(
88
(base, params) => `${base}${L.getInverse(paramsI, params)}`

src/client/scroll.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import smoothScroll from 'smoothscroll'
22

3-
const scroll = (target, ...args) => {
4-
if (typeof target === 'string') target = document.querySelector(target)
3+
export const scroll =
4+
typeof window === 'undefined'
5+
? () => {}
6+
: (target, ...args) => {
7+
if (typeof target === 'string') target = document.querySelector(target)
58

6-
target !== undefined && target !== null && smoothScroll(target, ...args)
7-
}
8-
9-
export default (typeof window === 'undefined' ? () => {} : scroll)
9+
target !== undefined && target !== null && smoothScroll(target, ...args)
10+
}

src/public/app.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as L from 'partial.lenses'
12
import * as React from 'karet'
23
import * as ReactDOM from 'react-dom/server'
34
import * as U from 'karet.util'
@@ -25,14 +26,16 @@ export const router = express.Router()
2526

2627
router.use(contactsApp.router)
2728

28-
router.get(Object.keys(routes), ({path, url, headers: {host}}, res) => {
29+
const patterns = L.collect(L.flat('pattern'), routes)
30+
31+
router.get(patterns, ({path, url, headers: {host}}, res) => {
2932
const location = {
3033
path,
3134
search: url.slice(path.length),
3235
hash: ''
3336
}
3437
const state = U.atom(State.initial)
35-
const context = State.context(location, host, state)
38+
const context = State.context(location, host, state, routes)
3639

3740
// XXX Inject state here.
3841

src/public/client.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {Page} from './components/page'
1616
import * as Meta from './meta'
1717
import * as State from './state'
1818

19+
import {routes} from './routes'
20+
1921
//
2022

2123
const state = MaybeSessionStored({
@@ -27,7 +29,7 @@ const state = MaybeSessionStored({
2729

2830
//
2931

30-
const context = State.context(location, window.location.host, state)
32+
const context = State.context(location, window.location.host, state, routes)
3133

3234
//
3335

src/public/components/_.less

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
@import './header.less';
2+
@import './link.less';
23
@import './pretty-stringify';
34
@import './restricted-input';

src/public/components/header.js

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import * as L from 'partial.lenses'
12
import * as React from 'karet'
3+
import * as U from 'karet.util'
24

35
import {Link} from './link'
46

5-
export const Header = () => (
7+
export const Header = U.withContext((_, {routes}) => (
68
<div className="header">
79
<div className="content">
810
<div className="links">
9-
<Link href="/">Main</Link>
10-
<Link href="/another-page">Another page</Link>
11-
<Link href="/examples/keep/calmm/and/curry/on/?hello=world">
12-
Examples
13-
</Link>
14-
<Link href="/contacts">Contacts</Link>
11+
{L.collectAs(
12+
({name, href, pattern}, i) => (
13+
<Link key={i} href={href || pattern}>
14+
{name}
15+
</Link>
16+
),
17+
L.flat(L.when(L.get('name'))),
18+
routes
19+
)}
1520
</div>
1621
</div>
1722
</div>
18-
)
23+
))

src/public/components/link.js

+44-18
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,78 @@
1+
import * as L from 'kefir.partial.lenses'
12
import * as R from 'kefir.ramda'
23
import * as React from 'karet'
34
import * as U from 'karet.util'
45

5-
import scroll from '../../client/scroll'
6+
import {disassemble} from '../../shared/routing'
7+
import {parse} from '../../shared/search-params'
68

7-
const special = U.actions(U.preventDefault, U.stopPropagation)
8-
9-
let onsite
9+
import {scroll} from '../../client/scroll'
1010

1111
const isExt = R.test(/^https?:\/\//)
1212

13+
const dropHash = U.lift(href => /^([^#]*)/.exec(href)[1])
14+
15+
const hrefParse = U.lift(href =>
16+
/^((\/[^?#]*)([?][^#]*)?)?([#].*)?$/.exec(href)
17+
)
18+
19+
const hrefLocation = R.pipe(
20+
hrefParse,
21+
L.get(
22+
L.ifElse(
23+
Array.isArray,
24+
L.pick({
25+
path: [2, L.valueOr('/')],
26+
params: [3, parse],
27+
hash: [4, L.valueOr('')]
28+
}),
29+
[]
30+
)
31+
)
32+
)
33+
1334
export const Link = U.withContext(
1435
(
1536
{href, onClick: outerOnClick, onThere, mount, className, ...props},
16-
{location}
37+
{location, locationPrime, routes, route}
1738
) => {
18-
const onClick = U.liftRec(href => e => {
19-
const internal = /^((\/[^?#]*)([?][^#]*)?)?([#].*)?$/.exec(href)
39+
const onClick = U.lift(href => e => {
40+
const internal = hrefParse(href)
2041

2142
if (internal) {
2243
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return
44+
e.preventDefault()
2345

2446
const [, , path = '', search = '', hash = ''] = internal
2547

48+
location.set({path, search, hash})
49+
2650
if (!path && !search) {
27-
if (!hash && onsite) {
28-
special(e)
29-
window.history.back()
30-
} else {
31-
window.setTimeout(() => scroll(hash, 200, onThere), 10)
32-
}
51+
window.setTimeout(() => scroll(hash, 200, onThere), 10)
3352
} else {
34-
special(e)
35-
location.set({path, search, hash})
36-
3753
if (!hash) scroll(0, 1, onThere)
3854
else window.setTimeout(() => scroll(hash, onThere), 100)
3955
}
4056
}
41-
onsite = 1
4257
})
4358

59+
const thisRoute = L.get('route', disassemble(routes, dropHash(href)))
60+
const sameRoute = R.equals(thisRoute, route)
61+
62+
const thisLocation = hrefLocation(href)
63+
const sameLocation = R.equals(thisLocation, locationPrime)
64+
4465
return (
4566
<a
4667
href={href}
4768
ref={mount}
4869
onClick={U.actions(outerOnClick, onClick(href))}
49-
className={U.cns(className, U.when(isExt(href), 'ext-link'))}
70+
className={U.cns(
71+
className,
72+
U.when(isExt(href), 'ext-link'),
73+
U.when(sameRoute, 'same-route'),
74+
U.when(sameLocation, 'same-location')
75+
)}
5076
{...props}
5177
/>
5278
)

src/public/components/link.less

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.same-location {
2+
text-decoration: none;
3+
}

src/public/components/page.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import * as React from 'karet'
22

33
import {NotFound} from '../pages/not-found'
44

5-
import {routes} from '../routes'
6-
75
import {Header} from './header'
86
import {Router} from './router'
97

108
export const Page = () => (
119
<div>
1210
<Header />
13-
<Router {...{routes, NotFound}} />
11+
<Router {...{NotFound}} />
1412
</div>
1513
)

src/public/components/router.js

+18-28
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,25 @@
1-
import * as L from 'kefir.partial.lenses'
2-
import * as R from 'kefir.ramda'
31
import * as React from 'karet'
42
import * as U from 'karet.util'
5-
import toRegex from 'path-to-regexp'
63

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)
4+
export const Router = U.withContext(
5+
({NotFound}, {resolved, matches, params, path}) => {
6+
const notFound = {route: {Component: NotFound}, match: {}}
177

18-
const sortStaticFirst = R.sortBy(({keys}) => (keys.length === 1 ? 0 : 1))
8+
let last, args, rendered
199

20-
const prepareRoutes = R.o(sortStaticFirst, expandRoutes)
10+
const render = U.lift(resolved => {
11+
const {route, match} = resolved || notFound
12+
if (route !== last) {
13+
last = route
14+
for (const k in args) args[k]._onDeactivation()
15+
args = {}
16+
for (const k in match) args[k] = U.view(k, matches)
17+
const {Component} = route
18+
rendered = <Component {...{params, path}} {...args} />
19+
}
20+
return rendered
21+
})
2122

22-
const router = U.liftRec((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)} />
23+
return <React.Fragment>{render(resolved)}</React.Fragment>
2724
}
28-
return <NotFound />
29-
})
30-
31-
export const Router = U.withContext(({routes, NotFound}, {path}) => (
32-
<React.Fragment>
33-
{router(prepareRoutes(routes), NotFound, path)}
34-
</React.Fragment>
35-
))
25+
)

src/public/pages/examples/page.js

+4-11
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+
)
+29-29
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
1-
import * as L from 'kefir.partial.lenses'
2-
import * as R from 'kefir.ramda'
1+
import * as L from 'partial.lenses'
32
import * as React from 'karet'
43
import * as U from 'karet.util'
54

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

9-
const getPagePathRoot = R.pipe(x => R.match(/(^\/[^/]+\/?).*/, x)[1])
10-
11-
const subPathGetter = pagePathRoot =>
12-
R.pipe(R.replace(pagePathRoot, ''), decodeURIComponent)
13-
14-
const subPathSetter = pagePathRoot =>
15-
R.pipe(
16-
R.map(R.when(x => x !== '/', encodeURIComponent)),
17-
R.join(''),
18-
R.concat(pagePathRoot),
19-
R.replace(pagePathRoot + '/', pagePathRoot)
20-
)
21-
22-
const subPathL = pagePathRoot =>
23-
L.iso(subPathGetter(pagePathRoot), subPathSetter(pagePathRoot))
24-
25-
const decodeProps = R.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

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const QuerystringParams = ({params, path, copy = U.atom()}) => {
4444
const href = newPathString(path, copied)
4545
return (
4646
<div>
47+
<h2>Querystring params</h2>
48+
<PrettyStringify value={params} />
4749
<table>
4850
<thead>
4951
<tr>
@@ -62,7 +64,6 @@ export const QuerystringParams = ({params, path, copy = U.atom()}) => {
6264
</tbody>
6365
</table>
6466
Navigate to: <Link href={href}>{href}</Link>
65-
<PrettyStringify value={params} />
6667
</div>
6768
)
6869
}

0 commit comments

Comments
 (0)