Skip to content

Commit

Permalink
Simplification of the library
Browse files Browse the repository at this point in the history
- Route map is now a default (optional) integration
- We explicitly require LocationProvider to prevent 'magic'
- No more implicit LocationProviders
  • Loading branch information
EECOLOR committed Jul 29, 2020
1 parent a5caa0c commit 4a073e6
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 226 deletions.
13 changes: 11 additions & 2 deletions example/src/App.js
@@ -1,7 +1,16 @@
import { useRouting, Link, interpolate } from '@kaliber/routing'
import { useRouting, Link, interpolate, LocationProvider } from '@kaliber/routing'

export default function App({ initialLocation }) {
const { routes } = useRouting({ initialLocation })
return (
<LocationProvider {...{ initialLocation }}>
<Page />
</LocationProvider>
)
}

function Page() {
const { routes } = useRouting()

return (
<>
<Navigation />
Expand Down
39 changes: 16 additions & 23 deletions example/src/advanced/App.js
@@ -1,28 +1,20 @@
import { useRouting, Link, useRouteContext, useRelativePick, useHistory } from '@kaliber/routing'
import { useRouteMap, pick } from '@kaliber/routing/routeMap'
import { useRouting, Link, useRouteContext, useRelativePick, useHistory, LocationProvider, pickFromRouteMap } from '@kaliber/routing'
import { routeMap } from './routeMap'

export default function App({ initialLocation, basePath }) {
// use the route map as an advanced extension to routing
const advanced = useRouteMap(routeMap)

/*
main routing entrypoint
Everything without the `route` or `routes` function has a context (LocationContext) which
provides `location` and `navigate` (history functionality). We might want to consider using
the <LocationProvider> to make it more obvious that from that point on a location is available.
*/
const { routes, context } = useRouting({ initialLocation, advanced, basePath })
return (
<LocationProvider {...{ initialLocation, basePath, routeMap }}>
<Page {...{ basePath }} />
</LocationProvider>
)
}

// The context (provided by `advanced` provides `path` and `root`)
// - path - relative paths
// - root - root based paths
const { path, root } = context
function Page({ basePath }) {
const { routes, path } = useRouting()

return (
<Language {...{ basePath }}>
<Navigation {...{ basePath, path }} />
<Navigation />
{routes(
[path.home, <Home />],
[path.articles, <Articles />],
Expand All @@ -44,7 +36,7 @@ function Language({ children, basePath }) {
React.useEffect(
() => {
// A trick to obtain the current route and the params
const { route, ...params } = pick(history.location.pathname.replace(basePath, ''), [routeMap, x => x])
const { route, ...params } = pickFromRouteMap(history.location.pathname.replace(basePath, ''), [routeMap, x => x])
// delete * from the params (we don't want to supply that dynamic bit)
delete params['*']
// construct the route
Expand Down Expand Up @@ -87,13 +79,14 @@ function useLanguage() {
return context
}

function Navigation({ basePath, path }) {
function Navigation() {
const { path } = useRouteContext()
const language = useLanguage()
return (
<div>
<Link {...{ basePath }} to={path.home()}>Home</Link>
<Link {...{ basePath }} to={path.articles()[language]}>{path.articles.meta.title[language]}</Link>
<Link {...{ basePath }} to={path.articles.article({ articleId: 'article1' })[language]}>Featured article</Link>
<Link to={path.home()}>Home</Link>
<Link to={path.articles()[language]}>{path.articles.meta.title[language]}</Link>
<Link to={path.articles.article({ articleId: 'article1' })[language]}>Featured article</Link>
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions example/src/advanced/index.html.js
Expand Up @@ -4,7 +4,7 @@ import stylesheet from '@kaliber/build/lib/stylesheet'
import javascript from '@kaliber/build/lib/javascript'
import polyfill from '@kaliber/build/lib/polyfill'
import App from './App?universal'
import { pick } from '@kaliber/routing/routeMap'
import { pickFromRouteMap } from '@kaliber/routing'
import { routeMap } from './routeMap'

// TODO, should we add another example directory for advanced and only show the base path concept here?
Expand All @@ -13,7 +13,7 @@ const basePath = '/advanced' // TODO: Get from file name, maybe also add a publi
Index.routes = {
match(location) {
const ok = { status: 200 }
const result = pick(location.pathname.replace(`${basePath}`, ''),
const result = pickFromRouteMap(location.pathname.replace(`${basePath}`, ''),
[routeMap, ok],
[routeMap.notFound, { status: 404 }],
[routeMap.articles.article.notFound, { status: 404 }]
Expand Down
2 changes: 1 addition & 1 deletion example/src/advanced/routeMap.js
@@ -1,4 +1,4 @@
import { asRouteMap } from '@kaliber/routing/routeMap'
import { asRouteMap } from '@kaliber/routing'

export const routeMap = asRouteMap({
home: { path: '', meta: { title: 'Home' } },
Expand Down
2 changes: 1 addition & 1 deletion index.js
@@ -1 +1 @@
export * from './src/index'
export * from './src/routing'
1 change: 0 additions & 1 deletion routeMap.js

This file was deleted.

169 changes: 76 additions & 93 deletions src/routeMap.js
Expand Up @@ -10,106 +10,97 @@
- Allow segmentation (sub-objects available to sub-components)
- Should be compatible with component thinking
*/
import { pick as originalPick } from './index'
import { interpolate, callOrReturn } from './matching'
import { pick, interpolate, callOrReturn } from './matching'

const _ = Symbol('route')
export const routeSymbol = Symbol('routeSymbol')

export function asRouteMap(map) {
return addParentPaths(normalize(map))
}
export function asRouteMap(map) { return addParentPaths(normalize(map)) }

export function useRouteMap(map) {
if (!map[_]) throw new Error(`Use asRouteMap`)
const context = React.useMemo(
() => {
return {
root: map,
path: map,
}
},
[map]
)
return {
context,
handlers: { extractPath, determineNestedContext }
}
}

export function pick(pathname, [routeMap, defaultHandler], ...overrides) {
export function pickFromRouteMap(pathname, [routeMap, defaultHandler], ...overrides) {
if (!routeMap[_]) throw new Error('Please wrap normalize your routeMap using the `asRouteMap` function')
const routes = routeToRoutes(routeMap, defaultHandler, overrides)
return originalPick(pathname, ...Object.entries(routes))
return pick(pathname, ...Object.entries(routes))

function routeToRoutes(route, defaultHandler, overrides, base = '') {
const { [_]: internal, path, meta, data, ...children } = route
const { children, route: { path } } = splitChildren(route)
if (path) {
const [override, handler] = overrides.find(([x]) => x === route) || []
const absolutePath = makePathAbsolute(path, base)
return {
[absolutePath]: params => callOrReturn(override ? handler : defaultHandler, { ...params, route }),
[absolutePath]: params => callOrReturn(
override ? handler : defaultHandler,
{ ...params, route }
),
...routeChildrenToRoutes(children, absolutePath),
}
} else return routeChildrenToRoutes(children, base)

function routeChildrenToRoutes(children, base) {
return Object.entries(children).reduce(
(result, [k, v]) => ({ ...result, ...routeToRoutes(v, defaultHandler, overrides, base) }),
return Object.values(children).reduce(
(result, child) => ({ ...result, ...routeToRoutes(child, defaultHandler, overrides, base) }),
{}
)
}
}
}

export function extractPathFromRoute(route) {
if (!route.hasOwnProperty(_)) throw new Error(`It seems the route '${JSON.stringify(route)}' is not from the route map`)
const { children, route: { [routeSymbol]: { parentPaths }, path } } = splitChildren(route)
const abs = [...parentPaths, path].reduce(
(base, path) => makePathAbsolute(path, base),
''
)
return `${abs}${Object.keys(children).length ? '/*' : ''}`

}

export function determineNestedContextForRoute(context, route) {
const { children } = splitChildren(route)
return {
root: context.root,
path: addParentPathsToChildren(children),
}
}

function normalize({ path, meta, data, ...children }) {
return route({ [_]: {}, path, meta, data, ...normalizeChildren(children) })
return withReverseRouting({ [routeSymbol]: {}, path, meta, data, ...normalizeChildren(children) })

function normalizeChildren(children) {
return Object.entries(children).reduce(
(result, [k, v]) => {
const route = normalize(typeof v === 'string' ? { path: v } : v)
return { ...result, [k]: route }
},
{}
return mapValues(children, child =>
normalize(typeof child === 'string' ? { path: child } : child)
)
}
}

// TODO: yeah: clean this up
function route(info) {
const { [_]: { parentPaths = [] }, path } = info
return Object.assign(route, info)
function withReverseRouting(route) {
const { [routeSymbol]: { parentPaths = [] }, path } = route
return Object.assign(reverseRoute, route)

function route(params) {
function reverseRoute(params) {
return [...parentPaths, path].reduce(
(base = '', path) => {
(base, path) => {
return (
base && typeof base === 'object'
? Object.entries(base).reduce(
(result, [k, base]) => {
const pathValue = interpolate(path[k] || path, params)
return {
...result,
[k]: base ? `${base}/${pathValue}` : pathValue
}
},
{}
)
: (
path && typeof path === 'object'
? Object.entries(path).reduce(
(result, [k, path]) => ({
...result,
[k]: base ? `${base}/${interpolate(path, params)}` : interpolate(path, params)
}),
{}
)
: base ? `${base}/${interpolate(path, params)}` : interpolate(path, params)
)
base && typeof base === 'object' ? resolveBaseObject(path, base, params) :
path && typeof path === 'object' ? resolvePathObject(path, base, params) :
resolve(path, base, params)
)
},
''
)

function resolveBaseObject(path, base, params) {
return mapValues(base, (base, k) => resolve(path[k] || path, base, params))
}

function resolvePathObject(path, base, params) {
return mapValues(path, path => resolve(path, base, params))
}

function resolve(path, base, params) {
const pathValue = interpolate(path, params)
return base ? `${base}/${pathValue}` : pathValue
}
}
}

Expand All @@ -120,45 +111,37 @@ function makePathAbsolute(path, base) {
path ? pathAsString(path) :
path
)
}

function pathAsString(path) {
return (
typeof path === 'string' ? path :
path ? `(?:${Object.values(path).join('|')})` :
path
)
function pathAsString(path) {
return (
typeof path === 'string' ? path :
path ? `(?:${Object.values(path).join('|')})` :
path
)
}
}

function addParentPaths({ [_]: internal, path, meta, data, ...children }, parentPaths = []) {
return route({
[_]: { parentPaths }, path, meta, data,

function addParentPaths(route, parentPaths = []) {
const { children, route: { path, meta, data } } = splitChildren(route)
return withReverseRouting({
[routeSymbol]: { parentPaths },
path, meta, data,
...addParentPathsToChildren(children, parentPaths.concat(path || []))
})
}

function addParentPathsToChildren(children, parentPaths = []) {
return Object.entries(children).reduce(
(result, [k, v]) => ({ ...result, [k]: addParentPaths(v, parentPaths)}),
{}
)

return mapValues(children, child => addParentPaths(child, parentPaths))
}

function extractPath(route) {
if (!route.hasOwnProperty(_)) throw new Error(`It seems the route '${JSON.stringify(route)}' is not from the route map`)
const { [_]: { parentPaths }, path, meta, data, ...children } = route
const abs = [...parentPaths, path].reduce(
(base, path) => makePathAbsolute(path, base),
''
)
return `${abs}${Object.keys(children).length ? '/*' : ''}`

function mapValues(x, f) {
return Object.entries(x)
.map(([k, v]) => [k, f(v, k, x)])
.reduce((result, [k, v]) => ({ ...result, [k]: v }), {})
}

function determineNestedContext(context, { [_]: internal, path, meta, data, ...children }) {
return {
root: context.root,
path: addParentPathsToChildren(children),
}
function splitChildren(route) {
const { [routeSymbol]: internal, path, meta, data, ...children } = route
return { route, children }
}

0 comments on commit 4a073e6

Please sign in to comment.