boring-router
Advanced tools
Comparing version 0.2.1 to 0.3.0-alpha.0
@@ -0,1 +1,2 @@ | ||
export declare function then(handler: () => void): void; | ||
export declare function isPathPrefix(path: string, prefix: string): boolean; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const FULFILLED_PROMISE = Promise.resolve(); | ||
function then(handler) { | ||
// tslint:disable-next-line:no-floating-promises | ||
FULFILLED_PROMISE.then(handler); | ||
} | ||
exports.then = then; | ||
function isPathPrefix(path, prefix) { | ||
@@ -4,0 +10,0 @@ return (path.startsWith(prefix) && |
@@ -6,2 +6,3 @@ "use strict"; | ||
const react_1 = tslib_1.__importStar(require("react")); | ||
const _utils_1 = require("./@utils"); | ||
let Redirect = class Redirect extends react_1.Component { | ||
@@ -19,3 +20,3 @@ render() { | ||
if (matched) { | ||
requestAnimationFrame(() => this.redirect()); | ||
_utils_1.then(() => this.redirect()); | ||
} | ||
@@ -22,0 +23,0 @@ return react_1.default.createElement(react_1.default.Fragment, null); |
import { History } from 'history'; | ||
import { Dict } from 'tslang'; | ||
export declare type RouteMatchAction = () => void; | ||
/** | ||
* Route match interception callback. | ||
* @return Return `true` or `undefined` to do nothing; return `false` to ignore | ||
* this match; return a full path to redirect. | ||
*/ | ||
export declare type RouteMatchInterception = () => string | boolean | void; | ||
export declare type RouteMatchReaction = () => void; | ||
export declare type GeneralFragmentDict = Dict<string | undefined>; | ||
@@ -10,2 +16,3 @@ export declare type GeneralQueryDict = Dict<string | undefined>; | ||
query: Dict<boolean> | undefined; | ||
exact: boolean; | ||
} | ||
@@ -18,3 +25,3 @@ export declare class RouteMatch<TParamDict extends GeneralParamDict = GeneralParamDict> { | ||
readonly $name: string; | ||
constructor(name: string, history: History, { match, query }: RouteMatchOptions); | ||
constructor(name: string, history: History, { match, query, exact }: RouteMatchOptions); | ||
/** | ||
@@ -48,9 +55,15 @@ * A reactive value indicates whether this route is matched. | ||
/** | ||
* Perform an action if this `RouteMatch` matches. | ||
* @param action A callback to perform this action. | ||
* @param exact Perform this action only if it's an exact match. | ||
* Intercept route matching if this `RouteMatch` matches. | ||
* @param interception The interception callback. | ||
* @param exact Intercept only if it's an exact match. | ||
*/ | ||
$action(action: RouteMatchAction, exact?: boolean): void; | ||
$intercept(interception: RouteMatchInterception, exact?: boolean): void; | ||
/** | ||
* Perform a reaction if this `RouteMatch` matches. | ||
* @param reaction A callback to perform this reaction. | ||
* @param exact Perform this reaction only if it's an exact match. | ||
*/ | ||
$react(reaction: RouteMatchReaction, exact?: boolean): void; | ||
static fragment: RegExp; | ||
static rest: RegExp; | ||
} |
@@ -7,4 +7,6 @@ "use strict"; | ||
class RouteMatch { | ||
constructor(name, history, { match, query }) { | ||
constructor(name, history, { match, query, exact }) { | ||
/** @internal */ | ||
this._interceptionEntries = []; | ||
/** @internal */ | ||
this._matched = false; | ||
@@ -22,2 +24,3 @@ /** @internal */ | ||
} | ||
this._allowExact = exact; | ||
} | ||
@@ -86,10 +89,21 @@ /** | ||
/** | ||
* Perform an action if this `RouteMatch` matches. | ||
* @param action A callback to perform this action. | ||
* @param exact Perform this action only if it's an exact match. | ||
* Intercept route matching if this `RouteMatch` matches. | ||
* @param interception The interception callback. | ||
* @param exact Intercept only if it's an exact match. | ||
*/ | ||
$action(action, exact = false) { | ||
$intercept(interception, exact = false) { | ||
this._interceptionEntries.push({ | ||
interception, | ||
exact, | ||
}); | ||
} | ||
/** | ||
* Perform a reaction if this `RouteMatch` matches. | ||
* @param reaction A callback to perform this reaction. | ||
* @param exact Perform this reaction only if it's an exact match. | ||
*/ | ||
$react(reaction, exact = false) { | ||
mobx_1.autorun(() => { | ||
if (exact ? this.$exact : this.$matched) { | ||
requestAnimationFrame(() => action()); | ||
_utils_1.then(() => reaction()); | ||
} | ||
@@ -99,39 +113,6 @@ }); | ||
/** @internal */ | ||
_update(skipped, upperRest, upperPathFragmentDict, upperParamFragmentDict, sourceQueryDict) { | ||
let { current, rest } = this._match(skipped, upperRest); | ||
let name = this.$name; | ||
let matched = current !== undefined; | ||
let exact = matched && rest === ''; | ||
let matchPattern = this._matchPattern; | ||
let pathFragmentDict = Object.assign({}, upperPathFragmentDict, { [name]: typeof matchPattern === 'string' ? matchPattern : current }); | ||
let paramFragmentDict = Object.assign({}, upperParamFragmentDict, (typeof matchPattern === 'string' ? undefined : { [name]: current })); | ||
this._pathFragments = pathFragmentDict; | ||
let queryKeys = this._queryKeys; | ||
let queryDict = queryKeys | ||
? matched | ||
? queryKeys.reduce((dict, key) => { | ||
let value = sourceQueryDict[key]; | ||
if (value !== undefined) { | ||
dict[key] = sourceQueryDict[key]; | ||
} | ||
return dict; | ||
}, {}) | ||
: {} | ||
: undefined; | ||
this._sourceQuery = sourceQueryDict; | ||
this._params = Object.assign({}, paramFragmentDict, queryDict); | ||
this._matched = matched; | ||
this._exact = exact; | ||
return { | ||
matched, | ||
rest, | ||
pathFragmentDict, | ||
paramFragmentDict, | ||
}; | ||
} | ||
/** @internal */ | ||
_match(skipped, rest) { | ||
if (skipped || !rest) { | ||
_match(rest) { | ||
if (!rest) { | ||
return { | ||
current: undefined, | ||
fragment: undefined, | ||
rest: '', | ||
@@ -148,3 +129,3 @@ }; | ||
return { | ||
current: pattern, | ||
fragment: pattern, | ||
rest: rest.slice(pattern.length), | ||
@@ -155,3 +136,3 @@ }; | ||
return { | ||
current: undefined, | ||
fragment: undefined, | ||
rest: '', | ||
@@ -169,3 +150,3 @@ }; | ||
return { | ||
current: matched, | ||
fragment: matched, | ||
rest: rest.slice(matched.length), | ||
@@ -176,3 +157,3 @@ }; | ||
return { | ||
current: undefined, | ||
fragment: undefined, | ||
rest: '', | ||
@@ -183,2 +164,51 @@ }; | ||
} | ||
/** @internal */ | ||
_intercept(exact) { | ||
let entries = this._interceptionEntries; | ||
if (!exact) { | ||
entries = entries.filter(entry => !entry.exact); | ||
} | ||
for (let { interception } of entries) { | ||
let result = interception(); | ||
if (result === true || result === undefined) { | ||
continue; | ||
} | ||
if (result === false) { | ||
return false; | ||
} | ||
if (typeof result === 'string') { | ||
return result; | ||
} | ||
throw new Error('Invalid interception result'); | ||
} | ||
return undefined; | ||
} | ||
/** @internal */ | ||
_update(matched, exact, fragment, upperPathFragmentDict, upperParamFragmentDict, sourceQueryDict) { | ||
let name = this.$name; | ||
let matchPattern = this._matchPattern; | ||
let pathFragmentDict = Object.assign({}, upperPathFragmentDict, { [name]: typeof matchPattern === 'string' ? matchPattern : fragment }); | ||
let paramFragmentDict = Object.assign({}, upperParamFragmentDict, (typeof matchPattern === 'string' ? undefined : { [name]: fragment })); | ||
this._pathFragments = pathFragmentDict; | ||
let queryKeys = this._queryKeys; | ||
let queryDict = queryKeys | ||
? matched | ||
? queryKeys.reduce((dict, key) => { | ||
let value = sourceQueryDict[key]; | ||
if (value !== undefined) { | ||
dict[key] = sourceQueryDict[key]; | ||
} | ||
return dict; | ||
}, {}) | ||
: {} | ||
: undefined; | ||
this._sourceQuery = sourceQueryDict; | ||
this._params = Object.assign({}, paramFragmentDict, queryDict); | ||
this._matched = matched; | ||
this._exact = exact; | ||
return { | ||
pathFragmentDict, | ||
paramFragmentDict, | ||
}; | ||
} | ||
} | ||
@@ -185,0 +215,0 @@ RouteMatch.fragment = /[^/]+/; |
@@ -21,2 +21,7 @@ import { History } from 'history'; | ||
export declare type RouterType<TRouteSchemaDict> = Router & RouteMatchFragmentType<TRouteSchemaDict, never>; | ||
export interface RouteMatchEntry { | ||
match: RouteMatch; | ||
exact: boolean; | ||
fragment: string; | ||
} | ||
export interface RouterOptions { | ||
@@ -23,0 +28,0 @@ /** |
@@ -5,2 +5,3 @@ "use strict"; | ||
const hyphenate_1 = tslib_1.__importDefault(require("hyphenate")); | ||
const _utils_1 = require("./@utils"); | ||
const route_match_1 = require("./route-match"); | ||
@@ -17,3 +18,11 @@ const DEFAULT_FRAGMENT_MATCHER_CALLBACK = key => hyphenate_1.default(key, { lowerCase: true }); | ||
}, {}); | ||
this._pushRouteChange(this, false, pathname, {}, {}, queryDict); | ||
let matchResult = this._match(this, pathname); | ||
if (typeof matchResult === 'string') { | ||
this._history.replace(matchResult); | ||
return; | ||
} | ||
let routeMatchEntryMap = new Map(matchResult | ||
? matchResult.map((entry) => [entry.match, entry]) | ||
: undefined); | ||
this._update(this, routeMatchEntryMap, {}, {}, queryDict); | ||
}; | ||
@@ -23,21 +32,81 @@ this._history = history; | ||
fragmentMatcher || DEFAULT_FRAGMENT_MATCHER_CALLBACK; | ||
this._children = this._buildRouteMatches(this, schema); | ||
history.listen(this._onLocationChange); | ||
this._onLocationChange(history.location); | ||
this._children = this._build(this, schema); | ||
this._update(this, new Map(), {}, {}, {}); | ||
_utils_1.then(() => { | ||
history.listen(this._onLocationChange); | ||
this._onLocationChange(history.location); | ||
}); | ||
} | ||
/** @internal */ | ||
_pushRouteChange(target, skipped, upperRest, upperPathFragmentDict, upperParamFragmentDict, sourceQueryDict) { | ||
if (!target._children) { | ||
return; | ||
} | ||
for (let routeMatch of target._children) { | ||
let { matched, rest, pathFragmentDict, paramFragmentDict, } = routeMatch._update(skipped, upperRest, upperPathFragmentDict, upperParamFragmentDict, sourceQueryDict); | ||
_match(target, upperRest) { | ||
for (let routeMatch of target._children || []) { | ||
let { fragment, rest } = routeMatch._match(upperRest); | ||
let matched = fragment !== undefined; | ||
let exact = matched && rest === ''; | ||
if (matched) { | ||
skipped = true; | ||
let interceptionResult = routeMatch._intercept(exact); | ||
if (typeof interceptionResult === 'string') { | ||
return interceptionResult; | ||
} | ||
else if (interceptionResult === false) { | ||
matched = false; | ||
exact = false; | ||
} | ||
} | ||
this._pushRouteChange(routeMatch, !matched, rest, pathFragmentDict, paramFragmentDict, sourceQueryDict); | ||
if (!matched) { | ||
continue; | ||
} | ||
if (exact) { | ||
if (!routeMatch._children || routeMatch._allowExact) { | ||
return [ | ||
{ | ||
match: routeMatch, | ||
fragment: fragment, | ||
exact: true, | ||
}, | ||
]; | ||
} | ||
else { | ||
continue; | ||
} | ||
} | ||
let result = this._match(routeMatch, rest); | ||
if (typeof result === 'string') { | ||
return result; | ||
} | ||
else if (result) { | ||
return [ | ||
{ | ||
match: routeMatch, | ||
fragment: fragment, | ||
exact: false, | ||
}, | ||
...result, | ||
]; | ||
} | ||
} | ||
return undefined; | ||
} | ||
/** @internal */ | ||
_buildRouteMatches(target, schemaDict) { | ||
_update(target, routeMatchEntryMap, upperPathFragmentDict, upperParamFragmentDict, sourceQueryDict) { | ||
for (let routeMatch of target._children || []) { | ||
let entry = routeMatchEntryMap.get(routeMatch); | ||
let matched; | ||
let exact; | ||
let fragment; | ||
if (entry) { | ||
matched = true; | ||
exact = entry.exact; | ||
fragment = entry.fragment; | ||
} | ||
else { | ||
matched = false; | ||
exact = false; | ||
} | ||
let { pathFragmentDict, paramFragmentDict } = routeMatch._update(matched, exact, fragment, upperPathFragmentDict, upperParamFragmentDict, sourceQueryDict); | ||
this._update(routeMatch, routeMatchEntryMap, pathFragmentDict, paramFragmentDict, sourceQueryDict); | ||
} | ||
} | ||
/** @internal */ | ||
_build(target, schemaDict) { | ||
let routeMatches = []; | ||
@@ -48,4 +117,8 @@ for (let [key, schema] of Object.entries(schemaDict)) { | ||
} | ||
let { $match: match = this._fragmentMatcher(key), $query: query, $children: children, } = schema; | ||
let routeMatch = new route_match_1.RouteMatch(key, this._history, { match, query }); | ||
let { $match: match = this._fragmentMatcher(key), $query: query, $exact: exact = false, $children: children, } = schema; | ||
let routeMatch = new route_match_1.RouteMatch(key, this._history, { | ||
match, | ||
query, | ||
exact, | ||
}); | ||
routeMatches.push(routeMatch); | ||
@@ -56,3 +129,3 @@ target[key] = routeMatch; | ||
} | ||
routeMatch._children = this._buildRouteMatches(routeMatch, children); | ||
routeMatch._children = this._build(routeMatch, children); | ||
} | ||
@@ -59,0 +132,0 @@ return routeMatches; |
@@ -5,2 +5,7 @@ import { Dict } from 'tslang'; | ||
$query?: Dict<boolean>; | ||
/** | ||
* Whether to allow exact match while if this route has children. Only | ||
* applies if this route has children. | ||
*/ | ||
$exact?: boolean; | ||
$children?: RouteSchemaDict; | ||
@@ -7,0 +12,0 @@ } |
# Changelog | ||
## 0.3.0 - unreleased | ||
### Breaking changes | ||
- The router will no longer match parent if it has no matching child. | ||
- By default, the router will no longer match a route if it has children but the path is ended at the route itself. For example, if the path is `/account`, and route `account` has `$children`, `account` will not be matched by default. An option `$exact` is added for this scenario, and applies only to route schema with `$children`. | ||
- `RouteMatch#$action` has been replaced with `RouteMatch#$react`. | ||
## [0.2.1] - 2018-9-5 | ||
@@ -4,0 +12,0 @@ |
{ | ||
"name": "boring-router", | ||
"version": "0.2.1", | ||
"version": "0.3.0-alpha.0", | ||
"description": "A light-weight, type-safe, yet reactive router service using MobX.", | ||
@@ -37,2 +37,3 @@ "repository": { | ||
"@types/history": "^4.7.0", | ||
"@types/jest": "^23.3.1", | ||
"@types/react": "^16.4.12", | ||
@@ -55,3 +56,2 @@ "@types/react-dom": "^16.0.7", | ||
"dependencies": { | ||
"@types/jest": "^23.3.1", | ||
"classnames": "^2.2.6", | ||
@@ -58,0 +58,0 @@ "hyphenate": "^0.2.4", |
@@ -101,3 +101,3 @@ [](https://www.npmjs.com/package/boring-router) | ||
$replace(params?: Partial<TParamDict>, preserveQuery?: boolean): void; | ||
$action(action: RouteMatchAction, exact?: boolean): void; | ||
$react(reaction: RouteMatchReaction, exact?: boolean): void; | ||
} | ||
@@ -213,9 +213,9 @@ ``` | ||
- [Action](examples/action/main.tsx) | ||
- [Reaction](examples/reaction/main.tsx) | ||
Take an action on route match. | ||
Take a reaction on route match. | ||
```tsx | ||
router.account.$action(() => { | ||
router.about.$replace({source: 'action'}); | ||
router.account.$react(() => { | ||
router.about.$replace({source: 'reaction'}); | ||
}); | ||
@@ -222,0 +222,0 @@ ``` |
35859
7
687
19
- Removed@types/jest@^23.3.1
- Removed@types/jest@23.3.14(transitive)