dush-router
Version:
A simple regex-based router for `dush`, `base`, `minibase` and anything based on them. Works on Browser and Node.js
295 lines (269 loc) • 8.75 kB
JavaScript
/*!
* dush-router <https://github.com/tunnckoCore/dush-router>
*
* Copyright (c) Charlike Mike Reagent <@tunnckoCore> (https://i.am.charlike.online)
* Released under the MIT license.
*/
'use strict'
/**
* A plugin that adds `.createRoute`, `.addRoute` and `.navigate`
* methods for any app based on [dush][], [base][] or [minibase][].
* Notice that this plugin emit events - `route` if match, and `notFound` if
* not route found on defined routes.
*
* **Example**
*
* ```js
* var dush = require('dush')
* var router = require('dush-router')
*
* var app = dush()
* app.use(router())
*
* console.log(app._routes) // => []
* console.log(app.createRoute) // => function
* console.log(app.addRoute) // => function
* console.log(app.navigate) // => function
* ```
*
* @name router()
* @param {Object} `opts` no options currently
* @return {Function} a plugin function which should be passed to `.use` method
* @api public
*/
module.exports = function dushRouter () {
return function dushRouter_ (app) {
var el = null
app._routes = []
app.on('route', function onRoute (view, context) {
return view(context)
})
/**
* > Add/register an actual `route` with `handler`
* to the `app._routes` array. It uses `.createRoute` method
* to create an "route" object that is then pushed to `app._routes`.
*
* _**Note:** If route handler returns something the `app.navigate` method
* will return that exact value on route match._
*
* **Example**
*
* ```js
* app.addRoute('/foobar', (context) => {
* console.log('state:', context.state) // => { hello: 'world' }
* console.log('params:', context.params) // => {}
* console.log('route:', context.route) // => '/foobar'
* console.log('pathname:', context.pathname) // => '/foobar'
* })
*
* app.navigate('/foobar', { hello: 'world' })
*
* // or with params
* app.addRoute('/user/:id', ({ state, params, route, pathname }) => {
* console.log('Hello ', state.username) // => 'Hello Charlike'
* console.log('Your ID is', params.id) // => 'Your ID is 123'
*
* console.log('route', route) // => '/user/:id'
* console.log('path', pathname) // => '/user/123'
* })
*
* app.navigate('/user/123', { username: 'Charlike' })
* ```
*
* @name .addRoute
* @param {String} `route` a simple route, express-like definition, e.g. `/user/:id`
* @param {Function} `handler` a function to be called when `route` match
* @return {Object} instance for chaining
* @api public
*/
app.addRoute = function addRoute (route, handler) {
app._routes.push(app.createRoute(route, handler))
return app
}
/**
* > Just create a `route` with `handler`, same as `.addRoute` method,
* but without adding it to `app._routes` array. This "route" object
* contains `.match`, `.regex`, `.route` and `.handler` properties.
* Where `.match` is a function that accepts single argument "pathname"
* to check against given `route`, `.handler` is the passed `handler`
* function, `.regex` is the generated regex for that `route` string and
* the `.route` is the given `route`. The `.match` function returns `null`
* if passed "pathname" string match to the given `route` but not params
* and `false` if passed "pathname" not match.
*
* _**Note:** This method does not call the given route handler._
*
* **Example**
*
* ```js
* const r = app.createRoute('/user/:id', function abc (params) {
* console.log('hi user with id:', params.id)
* })
*
* console.log(r.match) // => function
* console.log(r.handler) // => function
* console.log(r.handler.name) // => 'abc'
* console.log(r.route) // => '/user/:id'
* console.log(r.regex) // => /^\/user\/(\w+)$/i
*
* var params = r.match('/user/123')
* console.log(params) // => { id: 123 }
*
* // manually call the route handler
* if (params !== false) {
* r.handler(params || {})
* }
*
* // not match, so returns `false`
* params = r.match('/foobar')
* console.log(params) // => false
*
* var route = app.createRoute('/foobie', () => {})
*
* // match, but no params, so return `null`
* var res = route.match('/foobie')
* console.log(res) // => null
* ```
*
* @name .createRoute
* @param {String} `route` a simple route, express-like definition, e.g. `/user/:id`
* @param {Function} `handler` a function to be called when `route` match
* @return {Object} a "route" object with few properties
* @api public
*/
app.use(createRoutePlugin())
/**
* > Manually navigate to some route with url `pathname` and
* returns what the route handler returns. You can pass
* a custom `state` which will be passed to route handler's context
* as `context.state`. This method fires `notFound` event when
* not found match, and `route` when find a route.
*
* **Example**
*
* ```js
* app.on('notFound', (context) => {
* console.log(`sorry ${context.pathname} page not exist`)
* console.log('this is incoming state:', context.state)
* })
* app.navigate('/foo/bar/qux', { aa: 11 })
*
* app.addRoute('/hello/:place', (context) => {
* console.log('hi', context.params.place) // => 'hi world'
* })
* app.navigate('/hello/world')
*
* // remove default "on route" handler
* app.off('route')
*
* // and define your custom one,
* // to change route handler arguments
* app.on('route', (handler, context) => {
* return handler(context.state, context.params)
* })
*
* // notice the handler signature, it's different than
* // the default one seen in above `/hello/:place` route
* app.addRoute('/user/:name', (state, params) => {
* var name = state.username || params.name
*
* console.log('name:', name) // => 'name: john' or 'name: charlike'
*
* return name
* })
*
* // it returns what the route handler return
* var res = app.navigate('/user/john')
* console.log(res) // => 'john', because there's no passed state
*
* var ret = app.navigate('/user/hey', { username: 'charlike '})
* console.log(ret) // => 'charlike'
* ```
*
* @name .navigate
* @param {String} `pathname` a url to navigate to
* @param {any} `state` optionally pass a "state", passed to route's handler
* @return {any} basically returns what the route handler return
* @api public
*/
app.navigate = function navigate (pathname, state) {
state = isObject(state) ? state : {}
var found = false
var len = app._routes.length
var i = 0
while (i < len) {
var r = app._routes[i++]
var params = r.match(pathname)
// if `false` -> no match and no params
// if `null` -> no params
if (params === false) {
continue
}
found = true
var context = {
state: state,
route: r.route,
params: params || {},
pathname: pathname
}
app.emit('route', function viewFn () {
el = r.handler.apply(app, arguments)
return el
}, context, el)
}
if (!found) {
app.emit('notFound', { pathname: pathname, state: state })
}
return el
}
}
}
/**
* Utils
*/
function isObject (val) {
return val && typeof val === 'object' && !Array.isArray(val)
}
/**
* Internal plugin
*/
function createRoutePlugin () {
return function createRoutePlugin_ (app) {
app.createRoute = function createRoute (route, handler) {
var r = makeRoute(route)
r.route = route
r.handler = handler
r.match = function match (pathname) {
return r.regex.test(pathname) ? collectParams(r, pathname) : false
}
return r
}
}
}
function makeRoute (route) {
var keys = []
var regex = '^' + route
.replace(/\//g, '\\/')
.replace(/:(\w+)/g, function (_, name) {
keys.push(name)
return '(\\w+)'
}) + '$'
return {
regex: new RegExp(regex, 'i'),
keys: keys
}
}
function collectParams (r, pathname) {
var match = null
r.params = {}
pathname.replace(r.regex, function (args) {
args = arguments
for (var i = 1; i < args.length - 2; i++) {
r.keys.forEach(function (key) {
r.params[key] = args[i]
})
match = true
}
})
return match ? r.params : match
}