diff --git a/README.md b/README.md index bd69d62..c618d70 100644 --- a/README.md +++ b/README.md @@ -33,19 +33,20 @@ var router = new Router([ ]); // match route -var route = router.getRoute('/user/garfield'); +var route = router.getRoute('/user/garfield?foo=bar'); if (route) { // this will output: // - "view_user" for route.name // - "/user/garfield" for route.url // - {id: "garfield"} for route.params // - {path: "/user/:id", method: "get", foo: { bar: "baz"}} for route.config + // - { foo: 'bar' } for route.query console.log('[Route found]:', route); } // generate path name (does not include query string) from route -// "path" will be "/user/garfield/post/favoriteFood" -var path = router.makePath('view_user_post', {id: 'garfield', post: 'favoriteFood'}); +// "path" will be "/user/garfield/post/favoriteFood?meal=breakfast" +var path = router.makePath('view_user_post', {id: 'garfield', post: 'favoriteFood'}, { meal: 'breakfast' }); ``` diff --git a/docs/routr.md b/docs/routr.md index 80cebc3..687c09d 100644 --- a/docs/routr.md +++ b/docs/routr.md @@ -1,6 +1,6 @@ # Routr API -## Constructor(routes) +## Constructor(routes, options) Creates a new routr plugin instance with the following parameters: @@ -9,6 +9,8 @@ Creates a new routr plugin instance with the following parameters: ** `route.path`: The matching pattern of the route. Follows rules of [path-to-regexp](https://github .com/pillarjs/path-to-regexp) ** `route.method=GET`: The method that the path should match to. + * `options` (optional): Options for parsing and generating the urls + ** `options.queryLib=require('query-string')`: Library to use to `parse` and `stringify` query strings ## Instance Methods @@ -20,9 +22,10 @@ Returns the matched route info if path/method matches to a route; null otherwise * `options` options object * `options.method` (optional) The case-insensitive HTTP method string. DEFAULT: 'get' -### makePath(name, params) +### makePath(name, params, query) Generates a path string with the route with the given name, using the specified params. * `name` (required) The route name - * `options` (required) The route parameters to be used to create the path string + * `params` (required) The route parameters to be used to create the path string + * `query` (optional) The query parameters to be used to create the path string diff --git a/lib/router.js b/lib/router.js index 146e765..b66e111 100644 --- a/lib/router.js +++ b/lib/router.js @@ -7,6 +7,7 @@ var debug = require('debug')('Routr:router'); var pathToRegexp = require('path-to-regexp'); +var queryString = require('query-string'); var METHODS = { GET: 'get' }; @@ -17,13 +18,17 @@ var cachedCompilers = {}; * @param {String} name The name of the route * @param {Object} config The configuration for this route. * @param {String} config.path The path of the route. + * @param {Object} [options] Options for parsing and generating the urls + * @param {String} [options.queryLib] Library to use for `parse` and `stringify` methods * @constructor */ -function Route(name, config) { +function Route(name, config, options) { + options = options || {}; this.name = name; this.config = config || {}; this.keys = []; this.regexp = pathToRegexp(this.config.path, this.keys); + this._queryLib = options.queryLib || queryString; } /** @@ -101,20 +106,31 @@ Route.prototype.match = function (url, options) { } } - return routeParams; + // 4. query params + var queryParams = {}; + if (-1 !== pos) { + queryParams = self._queryLib.parse(url.substring(pos+1)); + } + + return { + route: routeParams, + query: queryParams + }; }; /** * Generates a path string with this route, using the specified params. * @method makePath * @param {Object} params The route parameters to be used to create the path string + * @param {Object} [query] The query parameters to be used to create the path string * @return {String} The generated path string. * @for Route */ -Route.prototype.makePath = function (params) { +Route.prototype.makePath = function (params, query) { var routePath = this.config.path; var compiler; var err; + var url; if (Array.isArray(routePath)) { routePath = routePath[0]; @@ -125,7 +141,11 @@ Route.prototype.makePath = function (params) { cachedCompilers[routePath] = compiler; try { - return compiler(params); + url = compiler(params); + if (query) { + url += '?' + this._queryLib.stringify(query); + } + return url; } catch (e) { err = e; } @@ -141,6 +161,8 @@ Route.prototype.makePath = function (params) { * A Router class that provides route matching and route generation functionalities. * @class Router * @param {Object} routes Route table, which is a name to router config map. + * @param {Object} [options] Options for parsing and generating the urls + * @param {String} [options.queryLib] Library to use for `parse` and `stringify` methods * @constructor * @example var Router = require('routr'), @@ -167,17 +189,18 @@ Route.prototype.makePath = function (params) { console.log('[Route found]: name=', route.name, 'url=', route.url, 'params=', route.params, 'config=', route.config); } */ -function Router(routes) { +function Router(routes, options) { var self = this; self._routes = {}; self._routeOrder = []; + self._options = options || {}; debug('new Router, routes = ', routes); if (!Array.isArray(routes)) { // Support handling route config object as an ordered map (legacy) self._routeOrder = Object.keys(routes); self._routeOrder.forEach(function createRoute(name) { - self._routes[name] = new Route(name, routes[name]); + self._routes[name] = new Route(name, routes[name], self._options); }); } else if (routes) { routes.forEach(function createRouteFromArrayValue(route) { @@ -189,7 +212,7 @@ function Router(routes) { throw new Error('Duplicate route with name ' + route.name); } self._routeOrder.push(route.name); - self._routes[route.name] = new Route(route.name, route); + self._routes[route.name] = new Route(route.name, route, self._options); }); } @@ -227,8 +250,9 @@ Router.prototype.getRoute = function (url, options) { return { name: keys[i], url: url, - params: match, - config: route.config + params: match.route, + config: route.config, + query: match.query }; } } @@ -240,10 +264,11 @@ Router.prototype.getRoute = function (url, options) { * @method makePath * @param {String} name The route name * @param {Object} params The route parameters to be used to create the path string + * @param {Object} [query] The query parameters to be used to create the path string * @return {String} The generated path string, null if there is no route with the given name. */ -Router.prototype.makePath = function (name, params) { - return (name && this._routes[name] && this._routes[name].makePath(params)) || null; +Router.prototype.makePath = function (name, params, query) { + return (name && this._routes[name] && this._routes[name].makePath(params, query)) || null; }; module.exports = Router; diff --git a/package.json b/package.json index 49578b7..6e9137d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ ], "dependencies": { "debug": "^2.0.0", - "path-to-regexp": "^1.1.1" + "path-to-regexp": "^1.1.1", + "query-string": "^3.0.1" }, "devDependencies": { "babel-polyfill": "^6.7.2", @@ -32,7 +33,8 @@ "istanbul": "^0.3.2", "jshint": "^2.5.1", "mocha": "^2.0.1", - "precommit-hook": "^2.0.1" + "precommit-hook": "^2.0.1", + "sinon": "^1.17.3" }, "jshintConfig": { "node": true diff --git a/tests/unit/lib/router.js b/tests/unit/lib/router.js index 0ff9668..2a1f887 100644 --- a/tests/unit/lib/router.js +++ b/tests/unit/lib/router.js @@ -7,6 +7,7 @@ var expect = require('chai').expect; var Router = require('../../../lib/router'); +var sinon = require('sinon'); var routesObject = { article: { path: '/:site/:category?/:subcategory?/:alias', @@ -228,6 +229,8 @@ describe('Router', function () { route = router.getRoute('/new_article?foo=bar', {method: 'post'}); expect(route.name).to.equal('new_article'); + expect(route.params).to.deep.equal({}); + expect(route.query).to.deep.equal({foo: 'bar'}); }); it('method should be case-insensitive and defaults to get', function () { @@ -302,6 +305,16 @@ describe('Router', function () { }); expect(path).to.equal('/foo/bar'); }); + it('handle query params', function () { + var path = router.makePath('unamed_params', { + foo: 'foo', + 0: 'bar' + }, { + foo: 'bar', + baz: 'foo' + }); + expect(path).to.equal('/foo/bar?baz=foo&foo=bar'); + }); it('non-existing route', function () { var path = router.makePath('article_does_not_exist', { site: 'SITE', @@ -347,6 +360,44 @@ describe('Router', function () { ]); }).to.throw(Error); }); + + it('should allow custom query string library', function () { + var queryLib = { + parse: sinon.spy(function (queryString) { + return queryString.split('&').reduce(function (a, v) { + var split = v.split('='); + a[split[0]] = split[1] || null; + return a; + }, {}); + }), + stringify: sinon.spy(function (queryObject) { + return Object.keys(queryObject).map(function (key) { + return key + '=' + queryObject[key]; + }).join('&'); + }) + }; + var router = new Router([ + { + name: 'home', + path: '/', + method: 'get' + } + ], { + queryLib: queryLib + }); + var matched = router.getRoute('/?foo=bar&bar=baz'); + expect(queryLib.parse.called).to.equal(true); + expect(matched.query).to.deep.equal({ + foo: 'bar', + bar: 'baz' + }); + var stringified = router.makePath('home', {}, { + foo: 'bar', + bar: 'baz' + }); + expect(queryLib.stringify.called).to.equal(true); + expect(stringified).to.equal('/?foo=bar&bar=baz'); + }); }); describe('Route', function () {