@sanity/state-router
Advanced tools
Comparing version 0.99.4 to 0.99.6
@@ -34,2 +34,11 @@ 'use strict'; | ||
var _IntentLink = require('./components/IntentLink'); | ||
Object.defineProperty(exports, 'IntentLink', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_IntentLink).default; | ||
} | ||
}); | ||
var _RouteScope = require('./components/RouteScope'); | ||
@@ -44,2 +53,11 @@ | ||
var _withRouter = require('./components/withRouter'); | ||
Object.defineProperty(exports, 'withRouter', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_withRouter).default; | ||
} | ||
}); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } |
@@ -31,2 +31,8 @@ 'use strict'; | ||
/*:: import type {RouterProviderContext} from './types'*/ | ||
/*:: type Props = { | ||
intent: string, | ||
params: Object | ||
}*/ | ||
var IntentLink = function (_React$Component) { | ||
@@ -46,5 +52,15 @@ _inherits(IntentLink, _React$Component); | ||
intent = _props.intent, | ||
params = _props.params; | ||
params = _props.params, | ||
children = _props.children; | ||
// @todo Temporary hack | ||
if (intent === 'edit' && params.type) { | ||
return _react2.default.createElement( | ||
_Link2.default, | ||
{ href: '/desk/' + params.type + '/edit/' + params.id.replace(/\//g, '.') }, | ||
children | ||
); | ||
} | ||
var url = this.context.__internalRouter.resolveIntentLink(intent, params); | ||
@@ -59,6 +75,2 @@ var rest = (0, _omit2.default)(this.props, 'intent', 'params'); | ||
IntentLink.propTypes = { | ||
intent: _react.PropTypes.string.isRequired, | ||
params: _react.PropTypes.object | ||
}; | ||
IntentLink.contextTypes = { | ||
@@ -65,0 +77,0 @@ __internalRouter: _react.PropTypes.object |
@@ -27,10 +27,20 @@ 'use strict'; | ||
function isLeftClickEvent(event) { | ||
/*:: import type {RouterProviderContext} from './types'*/ | ||
function isLeftClickEvent(event /*: SyntheticMouseEvent*/) { | ||
return event.button === 0; | ||
} | ||
function isModifiedEvent(event) { | ||
function isModifiedEvent(event /*: SyntheticMouseEvent*/) { | ||
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); | ||
} | ||
/*:: type Props = { | ||
replace: boolean, | ||
onClick: (event : SyntheticMouseEvent) => void, | ||
href: string, | ||
target: string | ||
}*/ | ||
var Link = function (_React$Component) { | ||
@@ -40,25 +50,25 @@ _inherits(Link, _React$Component); | ||
function Link() { | ||
var _ref; | ||
var _temp, _this, _ret; | ||
_classCallCheck(this, Link); | ||
var _this = _possibleConstructorReturn(this, (Link.__proto__ || Object.getPrototypeOf(Link)).call(this)); | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
_this.handleClick = _this.handleClick.bind(_this); | ||
return _this; | ||
} | ||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Link.__proto__ || Object.getPrototypeOf(Link)).call.apply(_ref, [this].concat(args))), _this), _this.handleClick = function (event /*: SyntheticMouseEvent*/) /*: void*/ { | ||
var _this$props = _this.props, | ||
onClick = _this$props.onClick, | ||
href = _this$props.href, | ||
target = _this$props.target, | ||
replace = _this$props.replace; | ||
_createClass(Link, [{ | ||
key: 'handleClick', | ||
value: function handleClick(e) { | ||
var _props = this.props, | ||
onClick = _props.onClick, | ||
href = _props.href, | ||
target = _props.target, | ||
replace = _props.replace; | ||
if (onClick) { | ||
onClick(e); | ||
onClick(event); | ||
} | ||
if (isModifiedEvent(e) || !isLeftClickEvent(e)) { | ||
if (isModifiedEvent(event) || !isLeftClickEvent(event)) { | ||
return; | ||
@@ -72,6 +82,9 @@ } | ||
e.preventDefault(); | ||
this.context.__internalRouter.navigateUrl(href, { replace: replace }); | ||
} | ||
}, { | ||
event.preventDefault(); | ||
_this.context.__internalRouter.navigateUrl(href, { replace: replace }); | ||
}, _temp), _possibleConstructorReturn(_this, _ret); | ||
} | ||
_createClass(Link, [{ | ||
key: 'render', | ||
@@ -89,5 +102,2 @@ value: function render() { | ||
}; | ||
Link.propTypes = { | ||
replace: _react.PropTypes.bool | ||
}; | ||
Link.contextTypes = { | ||
@@ -94,0 +104,0 @@ __internalRouter: _react.PropTypes.object |
@@ -21,2 +21,11 @@ 'use strict'; | ||
/*:: import type {Router} from '../types'*/ | ||
/*:: import type {RouterProviderContext, NavigateOptions} from './types'*/ | ||
/*:: type Props = { | ||
onNavigate: () => void, | ||
router: Router, | ||
state: Object, | ||
children: Element<*> | ||
}*/ | ||
var RouterProvider = function (_React$Component) { | ||
@@ -36,17 +45,14 @@ _inherits(RouterProvider, _React$Component); | ||
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] : {}, | ||
replace = _ref2.replace; | ||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = RouterProvider.__proto__ || Object.getPrototypeOf(RouterProvider)).call.apply(_ref, [this].concat(args))), _this), _this.navigateUrl = function (url /*: string*/) /*: void*/ { | ||
var options /*: NavigateOptions*/ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var onNavigate = _this.props.onNavigate; | ||
onNavigate(url, { replace: replace }); | ||
}, _this.navigateState = function (nextState) { | ||
var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
replace = _ref3.replace; | ||
onNavigate(url, options); | ||
}, _this.navigateState = function (nextState /*: Object*/) /*: void*/ { | ||
var options /*: NavigateOptions*/ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
_this.navigateUrl(_this.resolvePathFromState(nextState), { replace: replace }); | ||
}, _this.resolvePathFromState = function (state) { | ||
_this.navigateUrl(_this.resolvePathFromState(nextState), options); | ||
}, _this.resolvePathFromState = function (state /*: Object*/) /*: string*/ { | ||
return _this.props.router.encode(state); | ||
}, _this.resolveIntentLink = function (intent, params) { | ||
}, _this.resolveIntentLink = function (intent /*: string*/, params /*: Object*/) /*: string*/ { | ||
return _this.props.router.encode({ intent: intent, params: params }); | ||
@@ -58,3 +64,3 @@ }, _temp), _possibleConstructorReturn(_this, _ret); | ||
key: 'getChildContext', | ||
value: function getChildContext() { | ||
value: function getChildContext() /*: RouterProviderContext*/ { | ||
var state = this.props.state; | ||
@@ -84,13 +90,6 @@ | ||
RouterProvider.propTypes = { | ||
onNavigate: _react.PropTypes.func, | ||
router: _react.PropTypes.object, | ||
state: _react.PropTypes.object, | ||
children: _react.PropTypes.node | ||
}; | ||
exports.default = RouterProvider; | ||
RouterProvider.childContextTypes = { | ||
__internalRouter: _react.PropTypes.object, | ||
router: _react.PropTypes.object | ||
}; | ||
}; | ||
exports.default = RouterProvider; |
@@ -9,2 +9,4 @@ 'use strict'; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
var _react = require('react'); | ||
@@ -14,6 +16,8 @@ | ||
var _isEmpty = require('../utils/isEmpty'); | ||
var _isEmpty2 = _interopRequireDefault(_isEmpty); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -25,2 +29,15 @@ | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
/*:: import type {RouterProviderContext, NavigateOptions, InternalRouter, ContextRouter} from './types'*/ | ||
/*:: type Props = { | ||
scope: string, | ||
children: Element<*> | ||
}*/ | ||
function addScope(routerState /*: Object*/, scope /*: string*/, scopedState /*: Object*/) { | ||
return scopedState && _extends({}, routerState, _defineProperty({}, scope, scopedState)); | ||
} | ||
var RouteScope = function (_React$Component) { | ||
@@ -30,5 +47,24 @@ _inherits(RouteScope, _React$Component); | ||
function RouteScope() { | ||
var _ref; | ||
var _temp, _this, _ret; | ||
_classCallCheck(this, RouteScope); | ||
return _possibleConstructorReturn(this, (RouteScope.__proto__ || Object.getPrototypeOf(RouteScope)).apply(this, arguments)); | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = RouteScope.__proto__ || Object.getPrototypeOf(RouteScope)).call.apply(_ref, [this].concat(args))), _this), _this.resolvePathFromState = function (nextState /*: Object*/) /*: string*/ { | ||
var context = _this.context; | ||
var scope = _this.props.scope; | ||
var nextStateScoped /*: Object*/ = (0, _isEmpty2.default)(nextState) ? {} : addScope(context.router.state, scope, nextState); | ||
return context.__internalRouter.resolvePathFromState(nextStateScoped); | ||
}, _this.navigate = function (nextState /*: Object*/, options /*: NavigateOptions*/) /*: void*/ { | ||
var scope = _this.props.scope; | ||
var router = _this.context.router; | ||
router.navigate(addScope(router.state, scope, nextState), options); | ||
}, _temp), _possibleConstructorReturn(_this, _ret); | ||
} | ||
@@ -38,28 +74,19 @@ | ||
key: 'getChildContext', | ||
value: function getChildContext() { | ||
value: function getChildContext() /*: RouterProviderContext*/ { | ||
var scope = this.props.scope; | ||
var _context = this.context, | ||
router = _context.router, | ||
__internalRouter = _context.__internalRouter; | ||
var internalRouter /*: InternalRouter*/ = this.context.__internalRouter; | ||
var router /*: ContextRouter*/ = this.context.router; | ||
return { | ||
__internalRouter: { | ||
resolvePathFromState: function resolvePathFromState(nextState) { | ||
var empty = Object.keys(nextState).length === 0; | ||
return __internalRouter.resolvePathFromState(empty ? {} : addScope(nextState)); | ||
}, | ||
resolveIntentLink: __internalRouter.resolveIntentLink, | ||
navigateUrl: __internalRouter.navigateUrl | ||
resolvePathFromState: this.resolvePathFromState, | ||
resolveIntentLink: internalRouter.resolveIntentLink, | ||
navigateUrl: internalRouter.navigateUrl | ||
}, | ||
router: { | ||
navigate: function navigate(nextState, options) { | ||
router.navigate(addScope(nextState), options); | ||
}, | ||
navigate: this.navigate, | ||
state: router.state[scope] | ||
} | ||
}; | ||
function addScope(nextState) { | ||
return nextState && Object.assign({}, router.state, _defineProperty({}, scope, nextState)); | ||
} | ||
} | ||
@@ -76,6 +103,2 @@ }, { | ||
RouteScope.propTypes = { | ||
scope: _react.PropTypes.string, | ||
children: _react.PropTypes.node | ||
}; | ||
RouteScope.childContextTypes = RouteScope.contextTypes = { | ||
@@ -82,0 +105,0 @@ __internalRouter: _react.PropTypes.object, |
@@ -31,4 +31,12 @@ 'use strict'; | ||
/*:: import type {RouterProviderContext} from './types'*/ | ||
var EMPTY_STATE = {}; | ||
/*:: type Props = { | ||
state: string, | ||
toIndex: boolean | ||
}*/ | ||
var StateLink = function (_React$Component) { | ||
@@ -45,3 +53,3 @@ _inherits(StateLink, _React$Component); | ||
key: 'resolveUrl', | ||
value: function resolveUrl() { | ||
value: function resolveUrl() /*: string*/ { | ||
var _props = this.props, | ||
@@ -57,5 +65,8 @@ toIndex = _props.toIndex, | ||
if (!state && !toIndex) { | ||
// eslint-disable-next-line no-console | ||
console.error(new Error('No state passed to StateLink. If you want to link to an empty state, its better to use the the `toIndex` property')); | ||
} | ||
var nextState = toIndex ? EMPTY_STATE : state || EMPTY_STATE; | ||
return this.context.__internalRouter.resolvePathFromState(nextState); | ||
@@ -66,3 +77,3 @@ } | ||
value: function render() { | ||
var rest = (0, _omit2.default)(this.props, 'state', 'toIndex'); | ||
var rest = (0, _omit2.default)(this.props, 'replace', 'state', 'toIndex'); | ||
return _react2.default.createElement(_Link2.default, _extends({ href: this.resolveUrl() }, rest)); | ||
@@ -75,7 +86,2 @@ } | ||
StateLink.propTypes = { | ||
state: _react.PropTypes.object, | ||
replace: _react.PropTypes.bool, | ||
toIndex: _react.PropTypes.bool | ||
}; | ||
StateLink.defaultProps = { | ||
@@ -82,0 +88,0 @@ replace: false, |
@@ -29,7 +29,10 @@ 'use strict'; | ||
function createMatchResult(nodes, missing, remaining) { | ||
/*:: import type {Node, MatchResult} from './types'*/ | ||
function createMatchResult(nodes /*: Node[]*/, missing /*: string[]*/, remaining /*: string[]*/) /*: MatchResult*/ { | ||
return { nodes: nodes, missing: missing, remaining: remaining }; | ||
} | ||
function findMatchingRoutes(node, _state) { | ||
function findMatchingRoutes(node /*: Node*/, _state /*: ?Object*/) /*: MatchResult*/ { | ||
@@ -50,23 +53,23 @@ if (_state === null || _state === undefined) { | ||
var consumedKeys = (0, _intersection3.default)(stateKeys, requiredParams); | ||
var missingKeys = (0, _difference3.default)(requiredParams, consumedKeys); | ||
var remainingKeys = (0, _difference3.default)(stateKeys, consumedKeys); | ||
var consumedParams = (0, _intersection3.default)(stateKeys, requiredParams); | ||
var missingParams = (0, _difference3.default)(requiredParams, consumedParams); | ||
var remainingParams = (0, _difference3.default)(stateKeys, consumedParams); | ||
if (missingKeys.length > 0) { | ||
return createMatchResult([], missingKeys, []); | ||
if (missingParams.length > 0) { | ||
return createMatchResult([], missingParams, []); | ||
} | ||
if (remainingKeys.length === 0) { | ||
if (remainingParams.length === 0) { | ||
return createMatchResult([node], [], []); | ||
} | ||
var children = typeof node.children === 'function' ? node.children(state) : node.children; | ||
var children = (typeof node.children === 'function' ? node.children(state) : node.children) || []; | ||
if (remainingKeys.length > 0 && children.length === 0) { | ||
return createMatchResult([], remainingKeys, []); | ||
if (remainingParams.length > 0 && children.length === 0) { | ||
return createMatchResult([], remainingParams, []); | ||
} | ||
var remainingState = (0, _pick3.default)(state, remainingKeys); | ||
var remainingState = (0, _pick3.default)(state, remainingParams); | ||
var matchingChild = { nodes: [], remaining: [], missing: [] }; | ||
var matchingChild /*: MatchResult*/ = { nodes: [], remaining: [], missing: [] }; | ||
@@ -79,3 +82,3 @@ (0, _arrayify2.default)(children).some(function (childNode) { | ||
if (matchingChild.nodes.length === 0) { | ||
return createMatchResult([], missingKeys, remainingKeys); | ||
return createMatchResult([], missingParams, remainingParams); | ||
} | ||
@@ -82,0 +85,0 @@ |
@@ -10,2 +10,3 @@ 'use strict'; | ||
exports.default = parseRoute; | ||
/*:: import type {Route, Segment} from './types'*/ | ||
@@ -15,4 +16,3 @@ | ||
function createSegment(segment) { | ||
function createSegment(segment /*: string*/) /*: ?Segment*/ { | ||
if (!segment) { | ||
@@ -33,3 +33,3 @@ return null; | ||
function parseRoute(route) { | ||
function parseRoute(route /*: string*/) /*: Route*/ { | ||
var _route$split = route.split('?'), | ||
@@ -39,3 +39,3 @@ _route$split2 = _slicedToArray(_route$split, 1), | ||
var segments = pathname.split('/').map(createSegment).filter(Boolean); | ||
var segments /*: Segment[]*/ = pathname.split('/').map(createSegment).filter(Boolean); | ||
@@ -42,0 +42,0 @@ return { |
@@ -21,11 +21,10 @@ 'use strict'; | ||
function resolvePathFromState(node, state) { | ||
/*:: import type {Node, MatchResult} from './types'*/ | ||
function resolvePathFromState(node /*: Node*/, state /*: Object*/) /*: string*/ { | ||
(0, _debug.debug)('Resolving path from state %o', state); | ||
var match = (0, _findMatchingNodes2.default)(node, state); | ||
var match /*: MatchResult*/ = (0, _findMatchingNodes2.default)(node, state); | ||
if (match.remaining.length > 0) { | ||
var formatted = match.remaining.map(function (key) { | ||
return key + ' (=' + JSON.stringify(state[key]) + ')'; | ||
}).join(', '); | ||
throw new Error('State key' + (match.remaining.length == 1 ? '' : 's') + ' not mapped to url params: ' + formatted); | ||
var remaining = match.remaining; | ||
throw new Error('Unable to find matching route for state. Could not map the following state key' + (remaining.length == 1 ? '' : 's') + ' to a valid url: ' + remaining.join(', ')); | ||
} | ||
@@ -32,0 +31,0 @@ |
@@ -21,3 +21,6 @@ 'use strict'; | ||
function matchPath(node, path) { | ||
/*:: import type {Node} from './types'*/ | ||
function matchPath(node /*: Node*/, path /*: string*/) /*: ?{[key: string]: string}*/ { | ||
var parts = path.split('/').filter(Boolean); | ||
@@ -62,3 +65,3 @@ var segmentsLength = node.route.segments.length; | ||
function resolveStateFromPath(node, path) { | ||
function resolveStateFromPath(node /*: Node*/, path /*: string*/) /*: ?Object*/ { | ||
(0, _debug.debug)('resolving state from path %s', path); | ||
@@ -65,0 +68,0 @@ |
@@ -31,3 +31,12 @@ 'use strict'; | ||
function normalizeChildren(children) { | ||
/*:: import type {Transform, Router, RouteChildren} from './types'*/ | ||
/*:: type NodeOptions = { | ||
path?: string, | ||
children?: RouteChildren, | ||
transform?: {[key: string] : Transform<*>}, | ||
scope?: string | ||
}*/ | ||
function normalizeChildren(children /*: any*/) /*: RouteChildren*/ { | ||
if (Array.isArray(children) || typeof children === 'function') { | ||
@@ -39,7 +48,7 @@ return children; | ||
function isRoute(val) { | ||
function isRoute(val /*: NodeOptions | Router | RouteChildren*/) { | ||
return val && '_isRoute' in val; | ||
} | ||
function normalizeArgs(path, childrenOrOpts, children) { | ||
function normalizeArgs(path /*: string | NodeOptions*/, childrenOrOpts /*: NodeOptions | Router | RouteChildren*/, children /*: Router | RouteChildren*/) /*: NodeOptions*/ { | ||
if ((typeof path === 'undefined' ? 'undefined' : _typeof(path)) === 'object') { | ||
@@ -57,7 +66,7 @@ return path; | ||
function route(routeOrOpts, childrenOrOpts, children) { | ||
function route(routeOrOpts /*: string | NodeOptions*/, childrenOrOpts /*: NodeOptions | RouteChildren*/, children /*: Router | RouteChildren*/) /*: Router*/ { | ||
return createNode(normalizeArgs(routeOrOpts, childrenOrOpts, children)); | ||
} | ||
route.scope = function scope(scopeName) { | ||
route.scope = function scope(scopeName /*: string*/) /*: Router*/ { | ||
for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
@@ -97,3 +106,3 @@ rest[_key - 1] = arguments[_key]; | ||
var EMPTY_STATE = {}; | ||
function isRoot(pathname) { | ||
function isRoot(pathname /*: string*/) /*: boolean*/ { | ||
var parts = pathname.split('/'); | ||
@@ -108,3 +117,3 @@ for (var i = 0; i < parts.length; i++) { | ||
function createNode(options) { | ||
function createNode(options /*: NodeOptions*/) /*: Router*/ { | ||
var path = options.path, | ||
@@ -134,9 +143,9 @@ scope = options.scope, | ||
isRoot: isRoot, | ||
getBasePath: function getBasePath() { | ||
getBasePath: function getBasePath() /*: boolean*/ { | ||
return this.encode(EMPTY_STATE); | ||
}, | ||
isNotFound: function isNotFound(pathname) { | ||
isNotFound: function isNotFound(pathname /*: string*/) /*: boolean*/ { | ||
return this.decode(pathname) === null; | ||
}, | ||
getRedirectBase: function getRedirectBase(pathname) { | ||
getRedirectBase: function getRedirectBase(pathname /*: string*/) /*: ?string*/ { | ||
if (isRoot(pathname)) { | ||
@@ -143,0 +152,0 @@ var basePath = this.getBasePath(); |
@@ -1,1 +0,33 @@ | ||
'use strict'; | ||
'use strict'; | ||
/*:: export type Segment = { | ||
name: string, | ||
type: 'dir' | 'param' | ||
}*/ | ||
/*:: export type Transform<T> = { | ||
toState: (value: string) => T, | ||
toPath: (value : T) => string | ||
}*/ | ||
// eslint-disable-next-line no-use-before-define | ||
/*:: export type Route = { | ||
raw: string, | ||
segments: Segment[] | ||
}*/ | ||
/*:: export type RouteChildren = Node[] | (state : Object) => Node[]*/ | ||
/*:: export type Node = { | ||
route: Route, | ||
scope?: string, | ||
transform?: {[key: string] : Transform<*>}, | ||
children: RouteChildren | ||
}*/ | ||
/*:: export type Router = Node & { | ||
encode: (state : Object) => string, | ||
decode: (path : string) => ?Object | ||
}*/ | ||
/*:: export type MatchResult = { | ||
nodes: Node[], | ||
missing: string[], | ||
remaining: string[] | ||
}*/ |
@@ -7,3 +7,3 @@ 'use strict'; | ||
exports.default = arrayify; | ||
function arrayify(val) { | ||
function arrayify /*:: <T>*/(val /*: Array<T> | T*/) /*: Array<T>*/ { | ||
if (Array.isArray(val)) { | ||
@@ -10,0 +10,0 @@ return val; |
{ | ||
"name": "@sanity/state-router", | ||
"version": "0.99.4", | ||
"version": "0.99.6", | ||
"description": "A path pattern => state object bidirectional mapper", | ||
@@ -23,21 +23,23 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"lodash": "^4.17.2" | ||
"lodash": "^4.17.4" | ||
}, | ||
"devDependencies": { | ||
"babel-plugin-syntax-flow": "^6.18.0", | ||
"babel-plugin-transform-flow-strip-types": "^6.18.0", | ||
"babel-plugin-transform-flow-comments": "^6.22.0", | ||
"babelify": "^7.3.0", | ||
"browserify": "^13.1.1", | ||
"browserify": "^13.3.0", | ||
"error-capture-middleware": "0.0.2", | ||
"eslint-config-bengler": "^2.0.0", | ||
"eslint": "^3.14.0", | ||
"eslint-config-bengler": "^3.0.1", | ||
"eslint-plugin-flowtype": "^2.30.0", | ||
"eslint-plugin-import": "^2.2.0", | ||
"eslint-plugin-react": "^6.7.1", | ||
"eslint-plugin-react": "^6.9.0", | ||
"express": "^4.14.0", | ||
"flow-bin": "^0.35.0", | ||
"history": "^4.4.0", | ||
"flow-bin": "^0.38.0", | ||
"history": "^4.5.1", | ||
"in-publish": "^2.0.0", | ||
"object-inspect": "^1.2.1", | ||
"quickreload": "^2.1.2", | ||
"react": "^15.4.0", | ||
"react-dom": "^15.4.0", | ||
"react": "^15.4.2", | ||
"react-dom": "^15.4.2", | ||
"rebundler": "^0.3.0", | ||
@@ -47,3 +49,3 @@ "remon": "^1.0.2", | ||
"staticr": "^4.0.2", | ||
"tap": "^8.0.0" | ||
"tap": "^9.0.3" | ||
}, | ||
@@ -50,0 +52,0 @@ "repository": { |
@@ -10,3 +10,3 @@ ## @sanity/state-router | ||
## Usage | ||
## API Usage | ||
@@ -48,2 +48,49 @@ Define the routes for your application and how they should map to application state | ||
## React usage | ||
### Setup routes and provider | ||
```jsx | ||
import {route} from '@sanity/state-router' | ||
import {RouterProvider, withRouter} from '@sanity/state-router/components' | ||
const router = route('/', [ | ||
route('/bikes/:bikeId') | ||
]) | ||
const history = createHistory() | ||
function handleNavigate(nextUrl, {replace} = {}) { | ||
if (replace) { | ||
history.replace(nextUrl) | ||
} else { | ||
history.push(nextUrl) | ||
} | ||
} | ||
const App = withRouter(function App({router}) { | ||
if (router.state.bikeId) { | ||
return <BikePage id={router.state.bikeId} /> | ||
} | ||
return ( | ||
<div> | ||
<h1>Welcome</h1> | ||
<StateLink state={{bikeId: 22}}>Go to bike 22</StateLink> | ||
</div> | ||
) | ||
}) | ||
function render(location) { | ||
ReactDOM.render(( | ||
<RouterProvider | ||
router={router} | ||
onNavigate={handleNavigate} | ||
state={router.decode(location.pathname)}> | ||
<App /> | ||
</RouterProvider> | ||
), document.getElementById('container')) | ||
} | ||
history.listen(() => render(document.location)) | ||
``` | ||
## API | ||
@@ -185,2 +232,38 @@ | ||
## Intents | ||
An _intent_ is a kind of global route that can be used for dispatching user actions. The intent route can be mounted with | ||
```js | ||
route.intents(<basePath>) | ||
``` | ||
Intent links bypasses scoping, and will always be mapped to the configured `basePath`. | ||
An intent consists of a name, e.g. `open` and a set of parameters, e.g. `{id: 'abc33'}` and the easiest way to make a link to an intent is using the `IntentLink` React component: | ||
```jsx | ||
<IntentLink intent="open" params={{id: abc33}}>Open document</IntentLink> | ||
``` | ||
This will generate an `<a` tag with a href like `/<base path>/open/id=abc33` depending on where the intent handler is mounted | ||
State router comes with a built in intent-route parser that decodes an intent route to route state. | ||
Full example: | ||
``` | ||
const router = route('/', [ | ||
route('/users/:username'), | ||
route.intents('/intents') // <-- sets up intent routes at the /intents base path | ||
]) | ||
``` | ||
Decoding the url `/intents/open/id=abc33` will produce the following state: | ||
```js | ||
{ | ||
intent: 'open', | ||
params: {id: 'abc33'} | ||
} | ||
``` | ||
It is now up to your application logic to translate this intent into an action, and redirect accordingly. | ||
## 404s | ||
@@ -187,0 +270,0 @@ |
export {default as RouterProvider} from './components/RouterProvider' | ||
export {default as Link} from './components/Link' | ||
export {default as StateLink} from './components/StateLink' | ||
export {default as IntentLink} from './components/IntentLink' | ||
export {default as RouteScope} from './components/RouteScope' | ||
export {default as withRouter} from './components/withRouter' |
@@ -0,10 +1,15 @@ | ||
// @flow | ||
import React, {PropTypes} from 'react' | ||
import omit from 'lodash/omit' | ||
import Link from './Link' | ||
import type {RouterProviderContext} from './types' | ||
type Props = { | ||
intent: string, | ||
params: Object | ||
} | ||
export default class IntentLink extends React.Component { | ||
static propTypes = { | ||
intent: PropTypes.string.isRequired, | ||
params: PropTypes.object | ||
} | ||
props: Props; | ||
context: RouterProviderContext | ||
@@ -16,4 +21,9 @@ static contextTypes = { | ||
render() { | ||
const {intent, params} = this.props | ||
const {intent, params, children} = this.props | ||
// @todo Temporary hack | ||
if (intent === 'edit' && params.type) { | ||
return <Link href={`/desk/${params.type}/edit/${params.id.replace(/\//g, '.')}`}>{children}</Link> | ||
} | ||
const url = this.context.__internalRouter.resolveIntentLink(intent, params) | ||
@@ -20,0 +30,0 @@ const rest = omit(this.props, 'intent', 'params') |
@@ -0,19 +1,29 @@ | ||
// @flow | ||
import React, {PropTypes} from 'react' | ||
import omit from 'lodash/omit' | ||
import type {RouterProviderContext} from './types' | ||
function isLeftClickEvent(event) { | ||
function isLeftClickEvent(event : SyntheticMouseEvent) { | ||
return event.button === 0 | ||
} | ||
function isModifiedEvent(event) { | ||
function isModifiedEvent(event : SyntheticMouseEvent) { | ||
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) | ||
} | ||
type Props = { | ||
replace: boolean, | ||
onClick: (event : SyntheticMouseEvent) => void, | ||
href: string, | ||
target: string | ||
} | ||
export default class Link extends React.Component { | ||
props: Props | ||
context: RouterProviderContext | ||
static defaultProps = { | ||
replace: false, | ||
} | ||
static propTypes = { | ||
replace: PropTypes.bool | ||
} | ||
static contextTypes = { | ||
@@ -23,16 +33,11 @@ __internalRouter: PropTypes.object | ||
constructor() { | ||
super() | ||
this.handleClick = this.handleClick.bind(this) | ||
} | ||
handleClick = (event : SyntheticMouseEvent) : void => { | ||
handleClick(e) { | ||
const {onClick, href, target, replace} = this.props | ||
if (onClick) { | ||
onClick(e) | ||
onClick(event) | ||
} | ||
if (isModifiedEvent(e) || !isLeftClickEvent(e)) { | ||
if (isModifiedEvent(event) || !isLeftClickEvent(event)) { | ||
return | ||
@@ -46,3 +51,4 @@ } | ||
e.preventDefault() | ||
event.preventDefault() | ||
this.context.__internalRouter.navigateUrl(href, {replace}) | ||
@@ -49,0 +55,0 @@ } |
@@ -1,29 +0,39 @@ | ||
import React, {PropTypes} from 'react' | ||
// @flow | ||
import React, {PropTypes, Element} from 'react' | ||
import type {Router} from '../types' | ||
import type {RouterProviderContext, NavigateOptions} from './types' | ||
type Props = { | ||
onNavigate: () => void, | ||
router: Router, | ||
state: Object, | ||
children: Element<*> | ||
} | ||
export default class RouterProvider extends React.Component { | ||
static propTypes = { | ||
onNavigate: PropTypes.func, | ||
router: PropTypes.object, | ||
state: PropTypes.object, | ||
children: PropTypes.node | ||
props: Props | ||
static childContextTypes = { | ||
__internalRouter: PropTypes.object, | ||
router: PropTypes.object | ||
} | ||
navigateUrl = (url, {replace} = {}) => { | ||
navigateUrl = (url : string, options : NavigateOptions = {}) : void => { | ||
const {onNavigate} = this.props | ||
onNavigate(url, {replace}) | ||
onNavigate(url, options) | ||
} | ||
navigateState = (nextState, {replace} = {}) => { | ||
this.navigateUrl(this.resolvePathFromState(nextState), {replace}) | ||
navigateState = (nextState : Object, options : NavigateOptions = {}) : void => { | ||
this.navigateUrl(this.resolvePathFromState(nextState), options) | ||
} | ||
resolvePathFromState = state => { | ||
resolvePathFromState = (state : Object) : string => { | ||
return this.props.router.encode(state) | ||
} | ||
resolveIntentLink = (intent, params) => { | ||
resolveIntentLink = (intent : string, params : Object) : string => { | ||
return this.props.router.encode({intent, params}) | ||
} | ||
getChildContext() { | ||
getChildContext() : RouterProviderContext { | ||
const {state} = this.props | ||
@@ -47,5 +57,1 @@ return { | ||
} | ||
RouterProvider.childContextTypes = { | ||
__internalRouter: PropTypes.object, | ||
router: PropTypes.object | ||
} |
@@ -1,8 +0,21 @@ | ||
import React, {PropTypes} from 'react' | ||
// @flow | ||
import React, {PropTypes, Element} from 'react' | ||
import isEmpty from '../utils/isEmpty' | ||
import type {RouterProviderContext, NavigateOptions, InternalRouter, ContextRouter} from './types' | ||
type Props = { | ||
scope: string, | ||
children: Element<*> | ||
} | ||
function addScope(routerState : Object, scope : string, scopedState : Object) { | ||
return scopedState && { | ||
...routerState, | ||
[scope]: scopedState | ||
} | ||
} | ||
export default class RouteScope extends React.Component { | ||
static propTypes = { | ||
scope: PropTypes.string, | ||
children: PropTypes.node | ||
} | ||
props: Props | ||
context: RouterProviderContext | ||
@@ -13,28 +26,37 @@ static childContextTypes = RouteScope.contextTypes = { | ||
} | ||
getChildContext() : RouterProviderContext { | ||
const {scope} = this.props | ||
const internalRouter: InternalRouter = this.context.__internalRouter | ||
const router: ContextRouter = this.context.router | ||
getChildContext() { | ||
const {scope} = this.props | ||
const {router, __internalRouter} = this.context | ||
return { | ||
__internalRouter: { | ||
resolvePathFromState: nextState => { | ||
const empty = Object.keys(nextState).length === 0 | ||
return __internalRouter.resolvePathFromState(empty ? {} : addScope(nextState)) | ||
}, | ||
resolveIntentLink: __internalRouter.resolveIntentLink, | ||
navigateUrl: __internalRouter.navigateUrl | ||
resolvePathFromState: this.resolvePathFromState, | ||
resolveIntentLink: internalRouter.resolveIntentLink, | ||
navigateUrl: internalRouter.navigateUrl | ||
}, | ||
router: { | ||
navigate: (nextState, options) => { | ||
router.navigate(addScope(nextState), options) | ||
}, | ||
navigate: this.navigate, | ||
state: router.state[scope] | ||
} | ||
} | ||
} | ||
function addScope(nextState) { | ||
return nextState && Object.assign({}, router.state, {[scope]: nextState}) | ||
} | ||
resolvePathFromState = (nextState: Object): string => { | ||
const context = this.context | ||
const scope = this.props.scope | ||
const nextStateScoped : Object = isEmpty(nextState) | ||
? {} | ||
: addScope(context.router.state, scope, nextState) | ||
return context.__internalRouter.resolvePathFromState(nextStateScoped) | ||
} | ||
navigate = (nextState: Object, options?: NavigateOptions) : void => { | ||
const scope = this.props.scope | ||
const router = this.context.router | ||
router.navigate(addScope(router.state, scope, nextState), options) | ||
} | ||
render() { | ||
@@ -41,0 +63,0 @@ return this.props.children |
@@ -0,13 +1,16 @@ | ||
// @flow | ||
import React, {PropTypes} from 'react' | ||
import omit from 'lodash/omit' | ||
import Link from './Link' | ||
import type {RouterProviderContext} from './types' | ||
const EMPTY_STATE = {} | ||
type Props = { | ||
state: string, | ||
toIndex: boolean | ||
} | ||
export default class StateLink extends React.Component { | ||
static propTypes = { | ||
state: PropTypes.object, | ||
replace: PropTypes.bool, | ||
toIndex: PropTypes.bool | ||
} | ||
props: Props | ||
context: RouterProviderContext | ||
@@ -23,3 +26,3 @@ static defaultProps = { | ||
resolveUrl() { | ||
resolveUrl() : string { | ||
const {toIndex, state} = this.props | ||
@@ -32,11 +35,14 @@ | ||
if (!state && !toIndex) { | ||
// eslint-disable-next-line no-console | ||
console.error(new Error('No state passed to StateLink. If you want to link to an empty state, its better to use the the `toIndex` property')) | ||
} | ||
const nextState = toIndex ? EMPTY_STATE : (state || EMPTY_STATE) | ||
return this.context.__internalRouter.resolvePathFromState(nextState) | ||
} | ||
render() { | ||
const rest = omit(this.props, 'state', 'toIndex') | ||
const rest = omit(this.props, 'replace', 'state', 'toIndex') | ||
return <Link href={this.resolveUrl()} {...rest} /> | ||
} | ||
} |
@@ -24,21 +24,21 @@ // @flow | ||
const consumedKeys = intersection(stateKeys, requiredParams) | ||
const missingKeys = difference(requiredParams, consumedKeys) | ||
const remainingKeys = difference(stateKeys, consumedKeys) | ||
const consumedParams = intersection(stateKeys, requiredParams) | ||
const missingParams = difference(requiredParams, consumedParams) | ||
const remainingParams = difference(stateKeys, consumedParams) | ||
if (missingKeys.length > 0) { | ||
return createMatchResult([], missingKeys, []) | ||
if (missingParams.length > 0) { | ||
return createMatchResult([], missingParams, []) | ||
} | ||
if (remainingKeys.length === 0) { | ||
if (remainingParams.length === 0) { | ||
return createMatchResult([node], [], []) | ||
} | ||
const children = (typeof node.children === 'function') ? node.children(state) : node.children | ||
const children = ((typeof node.children === 'function') ? node.children(state) : node.children) || [] | ||
if (remainingKeys.length > 0 && children.length === 0) { | ||
return createMatchResult([], remainingKeys, []) | ||
if (remainingParams.length > 0 && children.length === 0) { | ||
return createMatchResult([], remainingParams, []) | ||
} | ||
const remainingState = pick(state, remainingKeys) | ||
const remainingState = pick(state, remainingParams) | ||
@@ -53,3 +53,3 @@ let matchingChild : MatchResult = {nodes: [], remaining: [], missing: []} | ||
if (matchingChild.nodes.length === 0) { | ||
return createMatchResult([], missingKeys, remainingKeys) | ||
return createMatchResult([], missingParams, remainingParams) | ||
} | ||
@@ -56,0 +56,0 @@ |
@@ -27,3 +27,3 @@ // @flow | ||
const segments: [Segment] = pathname | ||
const segments: Segment[] = pathname | ||
.split('/') | ||
@@ -30,0 +30,0 @@ .map(createSegment) |
@@ -12,4 +12,8 @@ // @flow | ||
if (match.remaining.length > 0) { | ||
const formatted = match.remaining.map(key => `${key} (=${JSON.stringify(state[key])})`).join(', ') | ||
throw new Error(`State key${match.remaining.length == 1 ? '' : 's'} not mapped to url params: ${formatted}`) | ||
const remaining = match.remaining | ||
throw new Error( | ||
`Unable to find matching route for state. Could not map the following state key${ | ||
remaining.length == 1 ? '' : 's' | ||
} to a valid url: ${remaining.join(', ')}` | ||
) | ||
} | ||
@@ -16,0 +20,0 @@ |
@@ -58,3 +58,3 @@ // @flow | ||
const examples : [[Object, MatchResult]] = [ | ||
const examples : [Object, MatchResult][] = [ | ||
[{}, { | ||
@@ -61,0 +61,0 @@ nodes: [], missing: ['bar'], remaining: [] |
@@ -16,3 +16,3 @@ // @flow | ||
test('throws on unresolvable state', {todo: false}, t => { | ||
test('throws on unresolvable state', t => { | ||
const rootRoute = route('/root', [ | ||
@@ -23,5 +23,33 @@ route('/:page', [ | ||
]) | ||
t.throws(() => resolvePathFromState(rootRoute, {foo: 'bar'}), /.*not mapped .* params.*foo.*/) | ||
t.throws( | ||
() => resolvePathFromState(rootRoute, {foo: 'bar'}), | ||
new Error('Unable to find matching route for state. Could not map the following state key to a valid url: foo') | ||
) | ||
}) | ||
test('points to unmapped keys', t => { | ||
const routesDef = route('/:dataset', [ | ||
route('/settings/:setting'), | ||
route('/tools/:tool', params => { | ||
if (params.tool === 'desk') { | ||
return [route.scope('desk', '/collections/:collection')] | ||
} | ||
if (params.tool === 'another-tool') { | ||
return [route.scope('foo', '/omg/:nope')] | ||
} | ||
}) | ||
]) | ||
const state = { | ||
dataset: 'some-dataset', | ||
tool: 'another-tool', | ||
foo: { | ||
nop: 'bar' | ||
}, | ||
} | ||
t.throws( | ||
() => resolvePathFromState(routesDef, state), | ||
new Error('Unable to find matching route for state. Could not map the following state keys to a valid url: tool, foo') | ||
) | ||
}) | ||
test('Resolves this', t => { | ||
@@ -28,0 +56,0 @@ const routesDef = route('/:dataset', [ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
72
2263
310
224906
23
Updatedlodash@^4.17.4