@sanity/state-router
Advanced tools
Comparing version 0.1.1 to 0.2.0-beta.0
@@ -50,7 +50,7 @@ 'use strict'; | ||
value: function handleClick(e) { | ||
var _props = this.props; | ||
var onClick = _props.onClick; | ||
var href = _props.href; | ||
var target = _props.target; | ||
var replace = _props.replace; | ||
var _props = this.props, | ||
onClick = _props.onClick, | ||
href = _props.href, | ||
target = _props.target, | ||
replace = _props.replace; | ||
@@ -57,0 +57,0 @@ |
@@ -36,5 +36,5 @@ 'use strict'; | ||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = RouterProvider.__proto__ || Object.getPrototypeOf(RouterProvider)).call.apply(_ref, [this].concat(args))), _this), _this.navigateUrl = function (url) { | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
replace = _ref2.replace; | ||
var replace = _ref2.replace; | ||
var onNavigate = _this.props.onNavigate; | ||
@@ -44,6 +44,5 @@ | ||
}, _this.navigateState = function (nextState) { | ||
var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
replace = _ref3.replace; | ||
var replace = _ref3.replace; | ||
_this.navigateUrl(_this.resolvePathFromState(nextState), { replace: replace }); | ||
@@ -58,6 +57,3 @@ }, _this.resolvePathFromState = function (state) { | ||
value: function getChildContext() { | ||
var _props = this.props; | ||
var router = _props.router; | ||
var location = _props.location; | ||
var state = _props.state; | ||
var state = this.props.state; | ||
@@ -71,3 +67,3 @@ return { | ||
navigate: this.navigateState, | ||
state: state || router.decode(location.pathname) | ||
state: state | ||
} | ||
@@ -89,5 +85,2 @@ }; | ||
router: _react.PropTypes.object, | ||
location: _react.PropTypes.shape({ | ||
pathname: _react.PropTypes.string | ||
}), | ||
state: _react.PropTypes.object, | ||
@@ -94,0 +87,0 @@ children: _react.PropTypes.node |
@@ -36,5 +36,5 @@ 'use strict'; | ||
var scope = this.props.scope; | ||
var _context = this.context; | ||
var router = _context.router; | ||
var __internalRouter = _context.__internalRouter; | ||
var _context = this.context, | ||
router = _context.router, | ||
__internalRouter = _context.__internalRouter; | ||
@@ -41,0 +41,0 @@ return { |
@@ -45,5 +45,5 @@ 'use strict'; | ||
value: function resolveUrl() { | ||
var _props = this.props; | ||
var toIndex = _props.toIndex; | ||
var state = _props.state; | ||
var _props = this.props, | ||
toIndex = _props.toIndex, | ||
state = _props.state; | ||
@@ -50,0 +50,0 @@ |
@@ -32,9 +32,6 @@ 'use strict'; | ||
function parseRoute(route) { | ||
var _route$split = route.split('?'); | ||
var _route$split = route.split('?'), | ||
_route$split2 = _slicedToArray(_route$split, 1), | ||
pathname = _route$split2[0]; | ||
var _route$split2 = _slicedToArray(_route$split, 1); | ||
var pathname = _route$split2[0]; | ||
var segments = pathname.split('/').map(createSegment).filter(Boolean); | ||
@@ -41,0 +38,0 @@ |
@@ -15,2 +15,9 @@ 'use strict'; | ||
function arrayify(val) { | ||
if (Array.isArray(val)) { | ||
return val; | ||
} | ||
return typeof val === 'undefined' ? [] : [val]; | ||
} | ||
function matchPath(node, path) { | ||
@@ -39,9 +46,10 @@ var parts = path.split('/').filter(Boolean); | ||
var childState = null; | ||
var children = typeof node.children === 'function' ? node.children(state) : node.children; | ||
if (children) { | ||
children.some(function (childNode) { | ||
childState = matchPath(childNode, rest.join('/')); | ||
return childState; | ||
}); | ||
} | ||
var children = typeof node.children === 'function' ? arrayify(node.children(state)) : node.children; | ||
children.some(function (childNode) { | ||
// console.log('----childNode') | ||
// console.log(childNode) | ||
// console.log('----childNode') | ||
childState = matchPath(childNode, rest.join('/')); | ||
return childState; | ||
}); | ||
@@ -48,0 +56,0 @@ if (rest.length > 0 && !childState) { |
@@ -29,2 +29,13 @@ 'use strict'; | ||
function normalizeChildren(children) { | ||
if (Array.isArray(children) || typeof children === 'function') { | ||
return children; | ||
} | ||
return children ? [children] : []; | ||
} | ||
function isRoute(val) { | ||
return val && '_isRoute' in val; | ||
} | ||
function normalizeArgs(path, childrenOrOpts, children) { | ||
@@ -34,7 +45,7 @@ if ((typeof path === 'undefined' ? 'undefined' : _typeof(path)) === 'object') { | ||
} | ||
if (Array.isArray(childrenOrOpts) || typeof childrenOrOpts === 'function') { | ||
return { path: path, children: childrenOrOpts }; | ||
if (Array.isArray(childrenOrOpts) || typeof childrenOrOpts === 'function' || isRoute(childrenOrOpts)) { | ||
return { path: path, children: normalizeChildren(childrenOrOpts) }; | ||
} | ||
if (children) { | ||
return _extends({ path: path }, childrenOrOpts, { children: children }); | ||
return _extends({ path: path }, childrenOrOpts, { children: normalizeChildren(children) }); | ||
} | ||
@@ -60,7 +71,18 @@ return _extends({ path: path }, childrenOrOpts); | ||
var EMPTY_STATE = {}; | ||
function isRoot(pathname) { | ||
var parts = pathname.split('/'); | ||
for (var i = 0; i < parts.length; i++) { | ||
if (parts[i]) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
function createNode(options) { | ||
var path = options.path; | ||
var scope = options.scope; | ||
var transform = options.transform; | ||
var children = options.children; | ||
var path = options.path, | ||
scope = options.scope, | ||
transform = options.transform, | ||
children = options.children; | ||
@@ -71,3 +93,5 @@ if (!path) { | ||
var parsedRoute = (0, _parseRoute2.default)(path); | ||
return { | ||
_isRoute: true, // todo: make a Router class instead | ||
scope: scope, | ||
@@ -82,4 +106,22 @@ route: parsedRoute, | ||
return (0, _resolveStateFromPath2.default)(this, _path); | ||
}, | ||
isRoot: isRoot, | ||
getBasePath: function getBasePath() { | ||
return this.encode(EMPTY_STATE); | ||
}, | ||
isNotFound: function isNotFound(pathname) { | ||
return this.decode(pathname) === null; | ||
}, | ||
getRedirectBase: function getRedirectBase(pathname) { | ||
if (isRoot(pathname)) { | ||
var basePath = this.getBasePath(); | ||
// Check if basepath is something different than given | ||
if (pathname !== basePath) { | ||
return basePath; | ||
} | ||
} | ||
return null; | ||
} | ||
}; | ||
} |
{ | ||
"name": "@sanity/state-router", | ||
"version": "0.1.1", | ||
"version": "0.2.0-beta.0", | ||
"description": "A path pattern => state object bidirectional mapper", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
147
README.md
@@ -12,31 +12,31 @@ ## @sanity/state-router | ||
```js | ||
import {createRoute, resolvePathFromState, resolveStateFromPath} from 'xroute' | ||
import {route} from 'xroute' | ||
const route = createRoute('/*', [ | ||
createRoute('/products/:productId'), | ||
createRoute('/users/:userId'), | ||
createRoute('/:page'), | ||
const router = route('/', [ | ||
route('/products/:productId'), | ||
route('/users/:userId'), | ||
route('/:page'), | ||
]) | ||
resolvePathFromState(route, {}) | ||
router.encode(route, {}) | ||
// => '/' | ||
resolveStateFromPath(route, '/') | ||
router.decode(route, '/') | ||
// => {} | ||
resolvePathFromState(route, {productId: 54}) | ||
router.encode(route, {productId: 54}) | ||
// => '/products/54' | ||
resolveStateFromPath(route, '/products/54') | ||
router.decode(route, '/products/54') | ||
// => {productId: 54} | ||
resolvePathFromState(route, {userId: 22}) | ||
router.encode(route, {userId: 22}) | ||
// => '/users/22' | ||
resolveStateFromPath(route, '/users/54') | ||
router.decode(route, '/users/54') | ||
// => {userId: 54} | ||
resolvePathFromState(route, {page: 'about'}) | ||
router.encode(route, {page: 'about'}) | ||
// => '/about' | ||
resolveStateFromPath(route, '/about') | ||
router.decode(route, '/about') | ||
// => {page: about} | ||
@@ -46,3 +46,31 @@ | ||
## Restrictions | ||
## API | ||
- `route(path : string, ?options : Options, ?children : ) : Router` | ||
- `route.scope(name : string, path : string, ?options : Options, ?children : ) : Router` | ||
- `Router`: | ||
- `encode(state : object) : string` | ||
- `decode(path : string) : object` | ||
- `isRoot(path : string) : boolean` | ||
- `getBasePath() : string`, | ||
- `isNotFound(pathname: string): boolean` | ||
- `getRedirectBase(pathname : string) : ?string` | ||
- `RouteChildren`: | ||
``` | ||
Router | [Router] | ((state) => Router | [Router]) | ||
``` | ||
- `Options`: | ||
``` | ||
{ | ||
path?: string, | ||
children?: RouteChildren, | ||
transform?: {[key: string] : Transform<*>}, | ||
scope?: string | ||
} | ||
``` | ||
- `children` can be either another router returned from another `route()-call`, an array of routers or a function that gets passed the matched parameters, and conditionally returns child routes | ||
## Limitations | ||
- Parameterized paths *only*. Each route must have at least one unique parameter. If not, there's no way of unambiguously resolve a path from an empty state. | ||
@@ -52,13 +80,92 @@ | ||
```js | ||
const rootRoute = createRoute('/', [ | ||
createRoute('/about'), | ||
createRoute('/contact') | ||
const router = route('/', [ | ||
route('/about'), | ||
route('/contact') | ||
]) | ||
``` | ||
What route should be resolved from an empty state? Since both `/about` and `/contact` above resolves to an empty state object, there's no way to resolve an empty state object back to either of them. The solution to this would be to introduce the page name as a parameter instead: | ||
What route should be resolved from an empty state? Since both `/about` and `/contact` above resolves to an empty state object, there's no way to encode an empty state unambiguously back to either of them. The solution to this would be to introduce the page name as a parameter instead: | ||
```js | ||
const rootRoute = createRoute('/:page') | ||
const router = route('/', route('/:page')) | ||
``` | ||
Now, `/about` would resolve to the state `{page: 'about'}` which unambiguously can map back to `/page`, and an empty state can map to `/`. To figure out if you are on the index page, you can check for `state.page == null`, (and set the state.page to null to navigate back to the index) | ||
### No query params | ||
Query parameters doesn't work too well with router scopes as they operate in a global namespace. A possible workaround is to "fake" query params in a path segment using transforms: | ||
Now, `/about` would resolve to the state `{page: 'about'}` which unambiguously can map back to `/page`, and an empty state can map to `/`. To figure out if you are on the index page, you can check for `state.page == null`, (and set the state.page to null to navigate back to the index) | ||
## Scopes | ||
A scope is a separate router state space, allowing different parts of an application to be completely agnostic about the overall routing schema is like. Let's illustrate: | ||
```js | ||
import {route} from './src' | ||
function findAppByName(name) { | ||
return name === 'pokemon' && { | ||
name: 'pokemon', | ||
router: route('/:section', route('/:pokemonName')) | ||
} | ||
} | ||
const router = route('/', [ | ||
route('/users/:username'), | ||
route('/apps/:appName', params => { | ||
const app = findAppByName(params.appName) | ||
return app && route.scope(app.name, '/', app.router) | ||
}) | ||
]) | ||
``` | ||
Decoding the following path... | ||
```js | ||
router.decode('/apps/pokemon/stats/bulbasaur') | ||
``` | ||
...will give us the state: | ||
```js | ||
{ | ||
appName: 'pokemon', | ||
pokemon: { | ||
section: 'stats', | ||
pokemonName: 'bulbasaur' | ||
} | ||
} | ||
``` | ||
## 404s | ||
To check whether a path name matches, you can use the isNotFound method on the returned router instance: | ||
```js | ||
const router = route('/pages/:page') | ||
router.isNotFound('/some/invalid/path') | ||
// => true | ||
``` | ||
## Base paths | ||
Using a base path is as simple as adding a toplevel route with no params: | ||
```js | ||
const router = route('/some/basepath', [ | ||
route('/:foo'), | ||
route('/:bar') | ||
]) | ||
``` | ||
Any empty router state will resolve to `/some/basepath`. To check if you should redirect to the base path on app init, you can use the `router.isRoot(path)` and `router.getBasePath()` method: | ||
```js | ||
if (router.isRoot(location.pathname)) { | ||
const basePath = router.getBasePath() | ||
if (basePath !== location.pathname) { | ||
history.replaceState(null, null, basePath) | ||
} | ||
} | ||
``` | ||
For convenience, this check is combined in the method `router.getRedirectBase()`, that if a redirect is needed, will return the base path, otherwise `null` | ||
``` | ||
const redirectTo = router.getRedirectBase(location.pathname) | ||
if (redirectTo) { | ||
history.replaceState(null, null, redirectTo) | ||
} | ||
``` |
@@ -7,5 +7,2 @@ import React, {PropTypes} from 'react' | ||
router: PropTypes.object, | ||
location: PropTypes.shape({ | ||
pathname: PropTypes.string | ||
}), | ||
state: PropTypes.object, | ||
@@ -29,3 +26,3 @@ children: PropTypes.node | ||
getChildContext() { | ||
const {router, location, state} = this.props | ||
const {state} = this.props | ||
return { | ||
@@ -38,3 +35,3 @@ __internalRouter: { | ||
navigate: this.navigateState, | ||
state: state || router.decode(location.pathname) | ||
state: state | ||
} | ||
@@ -41,0 +38,0 @@ } |
@@ -5,2 +5,9 @@ // @flow | ||
function arrayify<T>(val : Array<T> | T) : Array<T> { | ||
if (Array.isArray(val)) { | ||
return val | ||
} | ||
return typeof val === 'undefined' ? [] : [val] | ||
} | ||
function matchPath(node : Node, path : string) : ?{[key: string]: string} { | ||
@@ -29,9 +36,10 @@ const parts = path.split('/').filter(Boolean) | ||
let childState = null | ||
const children = typeof node.children === 'function' ? node.children(state) : node.children | ||
if (children) { | ||
children.some(childNode => { | ||
childState = matchPath(childNode, rest.join('/')) | ||
return childState | ||
}) | ||
} | ||
const children = typeof node.children === 'function' ? arrayify(node.children(state)) : node.children | ||
children.some(childNode => { | ||
// console.log('----childNode') | ||
// console.log(childNode) | ||
// console.log('----childNode') | ||
childState = matchPath(childNode, rest.join('/')) | ||
return childState | ||
}) | ||
@@ -38,0 +46,0 @@ if (rest.length > 0 && !childState) { |
@@ -15,11 +15,26 @@ // @flow | ||
function normalizeArgs(path, childrenOrOpts, children) : NodeOptions { | ||
function normalizeChildren(children : any) : RouteChildren { | ||
if (Array.isArray(children) || typeof children === 'function') { | ||
return children | ||
} | ||
return children ? [children] : [] | ||
} | ||
function isRoute(val? : NodeOptions | Router | RouteChildren) { | ||
return val && '_isRoute' in val | ||
} | ||
function normalizeArgs( | ||
path : string | NodeOptions, | ||
childrenOrOpts? : NodeOptions | Router | RouteChildren, | ||
children? : Router | RouteChildren | ||
) : NodeOptions { | ||
if (typeof path === 'object') { | ||
return path | ||
} | ||
if (Array.isArray(childrenOrOpts) || typeof childrenOrOpts === 'function') { | ||
return {path, children: childrenOrOpts} | ||
if (Array.isArray(childrenOrOpts) || typeof childrenOrOpts === 'function' || isRoute(childrenOrOpts)) { | ||
return {path, children: normalizeChildren(childrenOrOpts)} | ||
} | ||
if (children) { | ||
return {path, ...childrenOrOpts, children} | ||
return {path, ...childrenOrOpts, children: normalizeChildren(children)} | ||
} | ||
@@ -29,3 +44,7 @@ return {path, ...childrenOrOpts} | ||
export default function route(routeOrOpts : string | NodeOptions, childrenOrOpts? : NodeOptions | RouteChildren, children? : RouteChildren) : Router { | ||
export default function route( | ||
routeOrOpts : string | NodeOptions, | ||
childrenOrOpts? : NodeOptions | RouteChildren, | ||
children? : Router | RouteChildren | ||
) : Router { | ||
return createNode(normalizeArgs(routeOrOpts, childrenOrOpts, children)) | ||
@@ -44,2 +63,13 @@ } | ||
const EMPTY_STATE = {} | ||
function isRoot(pathname: string): boolean { | ||
const parts = pathname.split('/') | ||
for (let i = 0; i < parts.length; i++) { | ||
if (parts[i]) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
function createNode(options : NodeOptions) : Router { | ||
@@ -51,3 +81,5 @@ const {path, scope, transform, children} = options | ||
const parsedRoute = parseRoute(path) | ||
return { | ||
_isRoute: true, // todo: make a Router class instead | ||
scope, | ||
@@ -62,4 +94,21 @@ route: parsedRoute, | ||
return resolveStateFromPath(this, _path) | ||
}, | ||
isRoot: isRoot, | ||
getBasePath(): boolean { | ||
return this.encode(EMPTY_STATE) | ||
}, | ||
isNotFound(pathname: string): boolean { | ||
return this.decode(pathname) === null | ||
}, | ||
getRedirectBase(pathname : string) : ?string { | ||
if (isRoot(pathname)) { | ||
const basePath = this.getBasePath() | ||
// Check if basepath is something different than given | ||
if (pathname !== basePath) { | ||
return basePath | ||
} | ||
} | ||
return null | ||
} | ||
} | ||
} |
@@ -7,4 +7,6 @@ // @flow | ||
const original = obj[method] | ||
// $FlowIgnore | ||
obj[method] = mockFn | ||
return function restore() { | ||
// $FlowIgnore | ||
obj[method] = original | ||
@@ -11,0 +13,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
225688
57
1779
168