Skip to content

Commit 35dea54

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 c8bf210 commit 35dea54

File tree

17 files changed

+213
-132
lines changed

17 files changed

+213
-132
lines changed

src/client/request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as Kefir from 'kefir'
22
import * as L from '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

Lines changed: 7 additions & 6 deletions
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

Lines changed: 5 additions & 2 deletions
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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 13 additions & 8 deletions
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.elems, L.when(L.get('name'))],
18+
routes
19+
)}
1520
</div>
1621
</div>
1722
</div>
18-
)
23+
))

src/public/components/link.js

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,78 @@
1+
import * as L from 'partial.lenses'
2+
import * as R from 'ramda'
13
import * as React from 'karet'
24
import * as U from 'karet.util'
35

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

6-
function special(e) {
7-
e.preventDefault()
8-
e.stopPropagation()
9-
}
10-
11-
let onsite
9+
import {scroll} from '../../client/scroll'
1210

1311
const isExt = U.lift1Shallow(href => /^https?:\/\//.test(href))
1412

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+
U.view(
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+
1534
export const Link = U.withContext(
1635
(
1736
{href, onClick: outerOnClick, onThere, mount, className, ...props},
18-
{location}
37+
{location, locationPrime, routes, route}
1938
) => {
2039
const onClick = U.lift(href => e => {
21-
const internal = /^((\/[^?#]*)([?][^#]*)?)?([#].*)?$/.exec(href)
40+
const internal = hrefParse(href)
2241

2342
if (internal) {
2443
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return
44+
e.preventDefault()
2545

2646
const [, , path = '', search = '', hash = ''] = internal
2747

48+
location.set({path, search, hash})
49+
2850
if (!path && !search) {
29-
if (!hash && onsite) {
30-
special(e)
31-
window.history.back()
32-
} else {
33-
window.setTimeout(() => scroll(hash, 200, onThere), 10)
34-
}
51+
window.setTimeout(() => scroll(hash, 200, onThere), 10)
3552
} else {
36-
special(e)
37-
location.set({path, search, hash})
38-
3953
if (!hash) scroll(0, 1, onThere)
4054
else window.setTimeout(() => scroll(hash, onThere), 100)
4155
}
4256
}
43-
onsite = 1
4457
})
4558

59+
const thisRoute = U.view('route', disassemble(routes, dropHash(href)))
60+
const sameRoute = U.equals(thisRoute, route)
61+
62+
const thisLocation = hrefLocation(href)
63+
const sameLocation = U.equals(thisLocation, locationPrime)
64+
4665
return (
4766
<a
4867
href={href}
4968
ref={mount}
5069
onClick={U.actions(outerOnClick, onClick(href))}
51-
className={U.cns(className, U.ift(isExt(href), 'ext-link'))}
70+
className={U.cns(
71+
className,
72+
U.ift(isExt(href), 'ext-link'),
73+
U.ift(sameRoute, 'same-route'),
74+
U.ift(sameLocation, 'same-location')
75+
)}
5276
{...props}
5377
/>
5478
)

src/public/components/link.less

Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 3 deletions
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

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
1-
import * as L from 'partial.lenses'
2-
import * as R from '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.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)} />
23+
return U.fromKefir(render(resolved))
2724
}
28-
return <NotFound />
29-
})
30-
31-
export const Router = U.withContext(({routes, NotFound}, {path}) =>
32-
U.fromKefir(router(prepareRoutes(routes), NotFound, path))
3325
)

0 commit comments

Comments
 (0)