Skip to content

Instantly share code, notes, and snippets.

@dmitriid

dmitriid/Link.js Secret

Last active January 24, 2018 16:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmitriid/675ceff4bd07ec6cdf06a560d7262407 to your computer and use it in GitHub Desktop.
Save dmitriid/675ceff4bd07ec6cdf06a560d7262407 to your computer and use it in GitHub Desktop.

Using routes:

import UserDisplay
import UserListDisplay

const UserRoutes = () => {
  return <RouteHandler>
    <Route name='user'
           path='/user'>
      <Route name='id'
             path='/:id'
             handler={UserDisplay}>
        <Route name='list'
               path='/:name'
               handler={UserListDisplay}>
        </Route>
      </Route>
    </Route>
  </RouteHandler>
}

ReactDOM.render(<UserRoutes />, document.getElementById('app'))

Using RouteMatcher:

The above UserRoutes sets up nested routes. user.id maps to /user/:id etc. Anywhere in your components you can use:

const SomeComponent = () => {
  return <RouteMatcher>
      <Match name='user.id'>
        <UserIDView />
      </Match>
      <Match name='user.list'>
        <UserListView />
      </Match>
   </RouteMatcher>
}

The router exposes a number of hooks. For example, to do some additional stuff when router starts:

router.onStart((err, args) => {
   ... do your stuff ...
})

ReactDOM.render(<YourRoutes>, el);

Using Link:

<Link name='user.id' params={{id: 1}}>Some text or nested components</Link>

This will create an a tag with the proper link and event handlers:

  • onClick will set router paths correctly and re-render RouteHandler
  • right-click etc. are not hijacked
import Route from './Route';
import Link from './Link';
import router from './router';
export { Route, Link, router };
import * as React from 'react'
let Route = class Route extends React.Component {
constructor(props) {
super(props)
this.handlers = {}
}
render() {
return null
}
}
export default Route
import * as React from 'react'
import router from './router'
let RouteHandler = class RouteHandler extends React.Component {
constructor(props) {
super(props)
this.handlers = {}
const {routes, handlers} = this.getRoutes(this.props.children, '')
this.handlers = handlers
if(this.props.handler) {
routes.push({
name: '___root___',
path: '/'
})
//handlers['___root___'] = this.props.handler
}
router.add(routes)
this.state = {
currentState: null,
pastState: null,
}
}
componentDidMount() {
router.addListener((to, from) => {
// console.info('listen', to.name)
// router.setTransition(to, from)
this.setState({
currentState: to,
pastState: from
})
})
router.onStart((err, args) => {
console.log(err, args)
this.setState({currentState: router.getState()})
return Promise.resolve(null)
})
router.start()
}
/**
*
* Here's what we do:
* - traverse all children
* - build a routing table for router5
* - build a "cache" that maps router5 path segments to handlers
*
* Example:
* Given this route:
*
* <Route path="/">
* <Route path="/events" name="events">
* <Route path="/list" name="list" handler={EventsList} />
* </Route>
* <Route path="venues" name="venues" handler={Venues} />
* </Route>
*
* The code will return the following:
*
* routes: [
* {
* name: 'events',
* path: '/events',
* children: [
* {
* name: 'list',
* path: '/list'
* ]
* },
* {
* name: 'venues',
* path: '/venues',
* children: []
* }
* ]
*
* handlers: {
* 'events.list' : <EventsList>,
* 'venues' : <Venues>
* }
*
* @param route : React.Children
* @param parentName : string
* @returns {{handlers: {}, routes: Array}}
*/
getRoutes(route, parentName) {
let localHandlers = {}
let localRoutes = []
React.Children.forEach(route, (child) => {
const childPath = child.props.path
const childName = child.props.name
const childChildren = child.props.children
const childHandler = child.props.handler
const segmentName = parentName === '' ? childName : `${parentName}.${childName}`
const {handlers, routes} = this.getRoutes(childChildren, segmentName)
if(childHandler) {
localHandlers[segmentName] = childHandler
}
localHandlers = Object.assign({}, localHandlers, handlers)
localRoutes.push({
name: childName,
path: childPath,
children: routes
})
})
return {
handlers: localHandlers,
routes: localRoutes
}
}
/**
*
* Lookup a Handler associated with the current route and render it
*
* If router isn't started or if no Handler is found, return null
*
* @returns {ReactElement}
*/
render() {
if(!router.isStarted) {
return null
}
const Handler = this.handlers[router.currentSegment]
if(!Handler) {
console.error(`RouteHandler: could not find handler for route segment '${router.currentSegment}'`)
if(this.props.handler) {
return React.createElement(this.props.handler, null)
}
return null
}
if(this.props.handler) {
return React.createElement(this.props.handler, null, React.createElement(Handler, null))
}
return React.createElement(Handler, null)
}
}
export default RouteHandler
import * as React from 'react'
import router from './router'
/**
* RouteMatcher is used to render components based on current path
*
* <RouteMatcher>
* <Match name='profile.me'>
* <Profile />
* </Match>
* <Match name='profile.me.redeem'>
* <ProfileMeRedeem />
* </Match>
* </RouteMatcher>
*
*
* The above is the preferred way of matching. You may also match on path
* However, at this time RouteMatcher will convert path to name and will still
* match on name
*
* <RouteMatcher>
* <Match name='/profile/me'>
* <Profile />
* </Match>
* <Match name='profile.me.redeem'>
* <ProfileMeRedeem />
* </Match>
* </RouteMatcher>
*
*/
/**
* This is a dummy wrapper to be used in RouteMatcher
*
* @type {React.ClassicComponentClass<P>}
*/
const Match = (_) => {
return null
}
let RouteMatcher = class RouteMatcher extends React.Component {
constructor(props) {
super(props)
this.mapping = {}
this.mapping = this.getRoutes(this.props.children, '')
this.state = {
currentState: null
}
}
componentDidMount() {
router.addListener((to, _from) => this.setState({currentState: to}))
this.setState(router.currentSegment)
}
/**
*
* Here's what we do:
* - traverse all children
* - build a mapping { routeSegment => handler }
*
* Example:
* Given this Matching:
*
* <RouteMatcher>
* <Match path="/events">
* <EventsList />
* </Match>
* <Match name="events.misc">
* <SomeOtherHandler />
* </Match>
* </Route>
*
* The code will return the following:
*
* mapping: [
* {
* name: 'events',
* children: [
* <EventsList />
* ]
* },
* {
* name: 'events.misc',
* children: [ <SomeOtherHandler /> ]
* }
* ]
*
* @param route : React.Children
* @param parentName : string
* @returns {{handlers: {}, routes: Array}}
*/
getRoutes(route, parentName) {
let localMapping = {}
React.Children.forEach(route, (child) => {
let name = child.props.name
if(typeof child.props.name === 'undefined') {
const match = router.matchUrl(child.props.path)
if(match === null) {
return
}
name = match.name
}
localMapping[name] = child.props.children
})
return localMapping
}
/**
*
* Lookup a Handler associated with the current route and render it
*
* If router isn't started or if no Handler is found, return null
*
* @returns {ReactElement}
*/
render() {
if(!router.isStarted) {
return null
}
const Children = this.mapping[router.currentSegment] || this.mapping['*']
if(!Children) {
console.error(`RouteMatcher: could not find handler for route segment '${router.currentSegment}'`)
return null
}
return Children
}
}
RouteMatcher = __decorate([
observer
], RouteMatcher)
export {RouteMatcher, Match}
import Router5 from 'router5'
import browserPluginFactory from 'router5/plugins/browser'
import listenersPlugin from 'router5/plugins/listeners'
import {seq} from '../promises'
const router5 = new Router5([], {
strictQueryParams: true,
autoCleanUp: true
})
const broswerPlugin = browserPluginFactory({
useHash: true
})
router5.usePlugin(broswerPlugin)
.usePlugin(listenersPlugin())
class RouterSingleton {
constructor() {
this._onStart = []
}
/*setTransition(to, from) {
extendObservable(this._previousState, from)
extendObservable(this._currentState, to)
}*/
get currentSegment() {
return router5.getState().name
}
segmentMatches(segment) {
if(typeof segment === 'string') {
return new RegExp(`^${segment.replace('.', '\\.')}$`).test(this.currentSegment)
}
else {
return segment.test(this.currentSegment)
}
}
/**
*
* Match a route, and navigate to it. Do not reload browser page
*
* router.navigate('profile.redeem-code', {code: 'XXX'})
*
* ^ this will change browser location to '/redeem-code/XXX'
* but *will not* reload the page
*
* @param args : {route: string, params: object}
* @returns {any}
*/
navigate(...args) {
return router5.navigate.apply(router5, args)
}
transitionTo(...args) {
return router5.navigate.apply(router5, args)
}
/**
*
* If path matches an existing route, navigate to it without reloading
* the page
*
* If path doesn't match, make browser go to this url
*
* E.g.
*
* router.navigateToPath('/redeem-code/XXX')
*
* ^ this will change path to '/redeem-code/XXX' and *will not*
* reload the page because it matches
* <Route name='redeem-code' path='/redeem-code/:code'>
*
* router.navigateToPath('/browse-book')
*
* ^ this will force the browser to go to '/browse-book' because
* no such path has been configured
*
* @param path : string
*/
navigateToPath(path) {
const match = router5.matchPath(path)
if(match === null) {
window.location.href = path
return
}
this.navigate(match.name, match.params)
}
/**
*
* Match a route and navigate to it, but force browser reload
*
* router.navigate('profile.redeem-code', {code: 'XXX'})
*
* ^ this will make browser load location '/redeem-code/XXX'
*
* @param args : {route: string, params: object}
*/
forceNavigate(...args) {
const route = router5.buildPath.apply(router5, args)
window.location.href = route
}
addListener(...args) {
return router5.addListener.apply(router5, args)
}
buildPath(...args) {
return router5.buildPath.apply(router5, args)
}
matchPath(...args) {
return router5.matchPath.apply(router5, args)
}
matchUrl(...args) {
return router5.matchUrl.apply(router5, args)
}
_setRouterIsStarted() {
this._isStarted = true
}
start() {
return router5.start.apply(router5, [(err, args) => {
seq(this._onStart
.map((callback) => () => callback(err, args)))
.then(() => this._setRouterIsStarted())
.catch(() => this._setRouterIsStarted())
}])
}
onStart(callback) {
this._onStart.push(callback)
}
get isStarted() {
return router5.isStarted()
}
add(...args) {
return router5.add.apply(router5, args)
}
getParams() {
const state = router5.getState()
return state.params
}
getState() {
return router5.getState()
}
getPath() {
return router5.getState().path
}
getActiveRoute() {
return this.currentSegment
}
}
const router = new RouterSingleton()
export default router
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment