boring-router
Advanced tools
Comparing version 0.3.0-alpha.4 to 0.3.0-alpha.5
import { History } from 'history'; | ||
import { Dict } from 'tslang'; | ||
import { Dict, EmptyObjectPatch, OmitValueOfKey } from 'tslang'; | ||
import { RouteSource } from './router'; | ||
/** | ||
* Route match interception callback. | ||
* @return Return `true` or `undefined` to do nothing; return `false` to ignore | ||
* this match; return a full path to redirect. | ||
* Route match before enter callback. | ||
* @return Return `true` or `undefined` to do nothing; return `false` to revert | ||
* this history change; return full path to redirect. | ||
*/ | ||
export declare type RouteMatchInterception = () => string | boolean | void; | ||
export declare type RouteMatchReaction = () => void; | ||
export declare type RouteMatchBeforeEnter<TRouteMatch extends RouteMatch = RouteMatch> = (next: OmitValueOfKey<TRouteMatch, Exclude<keyof RouteMatch, keyof MatchingRouteMatch>>) => string | boolean | void; | ||
/** | ||
* Route match before leave callback. | ||
* @return Return `true` or `undefined` to do nothing; return `false` to revert | ||
* this history change. | ||
*/ | ||
export declare type RouteMatchBeforeLeave = () => boolean | void; | ||
export declare type RouteMatchAfterEnter = () => void; | ||
export declare type RouteMatchAfterLeave = () => void; | ||
export declare type RouteMatchServiceFactory<TRouteMatch extends RouteMatch> = (match: TRouteMatch) => IRouteService<TRouteMatch>; | ||
export interface IRouteService<TRouteMatch extends RouteMatch = RouteMatch> { | ||
beforeEnter?: RouteMatchBeforeEnter<TRouteMatch>; | ||
afterEnter?: RouteMatchAfterEnter; | ||
beforeLeave?: RouteMatchBeforeLeave; | ||
afterLeave?: RouteMatchAfterLeave; | ||
} | ||
export declare type GeneralFragmentDict = Dict<string | undefined>; | ||
export declare type GeneralQueryDict = Dict<string | undefined>; | ||
export declare type GeneralParamDict = Dict<string | undefined>; | ||
export interface RouteMatchOptions { | ||
export interface RouteMatchSharedOptions { | ||
match: string | RegExp; | ||
query: Dict<boolean> | undefined; | ||
} | ||
export interface RouteMatchOptions extends RouteMatchSharedOptions { | ||
exact: boolean; | ||
} | ||
export declare class RouteMatch<TParamDict extends GeneralParamDict = GeneralParamDict> { | ||
declare abstract class RouteMatchShared<TParamDict extends GeneralParamDict = GeneralParamDict> { | ||
/** | ||
@@ -24,12 +41,4 @@ * Name of this `RouteMatch`, correspondent to the field name of route | ||
readonly $name: string; | ||
constructor(name: string, history: History, { match, query, exact }: RouteMatchOptions); | ||
constructor(name: string, source: RouteSource, parent: RouteMatchShared | undefined, history: History, { match, query }: RouteMatchSharedOptions); | ||
/** | ||
* A reactive value indicates whether this route is matched. | ||
*/ | ||
readonly $matched: boolean; | ||
/** | ||
* A reactive value indicates whether this route is exactly matched. | ||
*/ | ||
readonly $exact: boolean; | ||
/** | ||
* A dictionary of the combination of query string and fragments. | ||
@@ -44,25 +53,37 @@ */ | ||
*/ | ||
$ref(params?: Partial<TParamDict>, preserveQuery?: boolean): string; | ||
$ref(params?: Partial<TParamDict> & EmptyObjectPatch, preserveQuery?: boolean): string; | ||
/** | ||
* Perform a `history.push()` with `this.$ref(params, preserveQuery)`. | ||
*/ | ||
$push(params?: Partial<TParamDict>, preserveQuery?: boolean): void; | ||
$push(params?: Partial<TParamDict> & EmptyObjectPatch, preserveQuery?: boolean): void; | ||
/** | ||
* Perform a `history.replace()` with `this.$ref(params, preserveQuery)`. | ||
*/ | ||
$replace(params?: Partial<TParamDict>, preserveQuery?: boolean): void; | ||
$replace(params?: Partial<TParamDict> & EmptyObjectPatch, preserveQuery?: boolean): void; | ||
} | ||
export declare class MatchingRouteMatch<TParamDict extends GeneralParamDict = GeneralParamDict> extends RouteMatchShared<TParamDict> { | ||
constructor(name: string, source: RouteSource, parent: RouteMatchShared<TParamDict> | undefined, origin: RouteMatch<TParamDict>, history: History, options: RouteMatchSharedOptions); | ||
/** | ||
* Intercept route matching if this `RouteMatch` matches. | ||
* @param interception The interception callback. | ||
* @param exact Intercept only if it's an exact match. | ||
* A reactive value indicates whether this route is exactly matched. | ||
*/ | ||
$intercept(interception: RouteMatchInterception, exact?: boolean): void; | ||
readonly $exact: boolean; | ||
} | ||
export declare class RouteMatch<TParamDict extends GeneralParamDict = GeneralParamDict> extends RouteMatchShared<TParamDict> { | ||
constructor(name: string, source: RouteSource, parent: RouteMatch | undefined, history: History, { exact, ...sharedOptions }: RouteMatchOptions); | ||
/** | ||
* 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. | ||
* A reactive value indicates whether this route is matched. | ||
*/ | ||
$react(reaction: RouteMatchReaction, exact?: boolean): void; | ||
readonly $matched: boolean; | ||
/** | ||
* A reactive value indicates whether this route is exactly matched. | ||
*/ | ||
readonly $exact: boolean; | ||
$beforeEnter(callback: RouteMatchBeforeEnter<this>): this; | ||
$beforeLeave(callback: RouteMatchBeforeLeave): this; | ||
$afterEnter(callback: RouteMatchAfterEnter): this; | ||
$afterLeave(callback: RouteMatchAfterLeave): this; | ||
$service(factory: RouteMatchServiceFactory<this>): this; | ||
static fragment: RegExp; | ||
static rest: RegExp; | ||
} | ||
export {}; |
@@ -6,11 +6,7 @@ "use strict"; | ||
const _utils_1 = require("./@utils"); | ||
class RouteMatch { | ||
constructor(name, history, { match, query, exact }) { | ||
/** @internal */ | ||
this._interceptionEntries = []; | ||
/** @internal */ | ||
this._matched = false; | ||
/** @internal */ | ||
this._exact = false; | ||
class RouteMatchShared { | ||
constructor(name, source, parent, history, { match, query }) { | ||
this.$name = name; | ||
this._source = source; | ||
this._parent = parent; | ||
this._history = history; | ||
@@ -24,22 +20,48 @@ if (match instanceof RegExp && match.global) { | ||
} | ||
this._allowExact = exact; | ||
} | ||
/** | ||
* A reactive value indicates whether this route is matched. | ||
*/ | ||
get $matched() { | ||
return this._matched; | ||
} | ||
/** | ||
* A reactive value indicates whether this route is exactly matched. | ||
*/ | ||
get $exact() { | ||
return this._exact; | ||
} | ||
/** | ||
* A dictionary of the combination of query string and fragments. | ||
*/ | ||
get $params() { | ||
return this._params; | ||
return Object.assign({}, this._paramFragments, this._query); | ||
} | ||
/** @internal */ | ||
get _fragment() { | ||
let entry = this._getMatchEntry(this._source); | ||
return entry && entry.fragment; | ||
} | ||
/** @internal */ | ||
get _paramFragments() { | ||
let parent = this._parent; | ||
let upperFragmentDict = parent && parent._paramFragments; | ||
let matchPattern = this._matchPattern; | ||
let fragment = this._fragment; | ||
return Object.assign({}, upperFragmentDict, (typeof matchPattern === 'string' | ||
? undefined | ||
: { [this.$name]: fragment })); | ||
} | ||
/** @internal */ | ||
get _pathFragments() { | ||
let parent = this._parent; | ||
let upperFragmentDict = parent && parent._pathFragments; | ||
let matchPattern = this._matchPattern; | ||
let fragment = this._fragment; | ||
return Object.assign({}, upperFragmentDict, { | ||
[this.$name]: typeof matchPattern === 'string' ? matchPattern : fragment, | ||
}); | ||
} | ||
/** @internal */ | ||
get _query() { | ||
let queryKeys = this._queryKeys; | ||
let sourceQueryDict = this._source.queryDict; | ||
return queryKeys | ||
? queryKeys.reduce((dict, key) => { | ||
let value = sourceQueryDict[key]; | ||
if (value !== undefined) { | ||
dict[key] = sourceQueryDict[key]; | ||
} | ||
return dict; | ||
}, {}) | ||
: undefined; | ||
} | ||
/** | ||
@@ -53,2 +75,3 @@ * Generates a string reference that can be used for history navigation. | ||
let fragmentDict = this._pathFragments; | ||
let sourceQueryDict = this._source.queryDict; | ||
let paramKeySet = new Set(Object.keys(params)); | ||
@@ -66,3 +89,2 @@ let path = Object.keys(fragmentDict) | ||
.join(''); | ||
let sourceQueryDict = this._sourceQuery; | ||
let query = new URLSearchParams([ | ||
@@ -74,3 +96,3 @@ ...(preserveQuery | ||
]).toString(); | ||
return path + (query ? `?${query}` : ''); | ||
return `${path}${query ? `?${query}` : ''}`; | ||
} | ||
@@ -91,121 +113,200 @@ /** | ||
} | ||
} | ||
tslib_1.__decorate([ | ||
mobx_1.computed | ||
], RouteMatchShared.prototype, "$params", null); | ||
tslib_1.__decorate([ | ||
mobx_1.computed | ||
], RouteMatchShared.prototype, "_fragment", null); | ||
tslib_1.__decorate([ | ||
mobx_1.computed | ||
], RouteMatchShared.prototype, "_paramFragments", null); | ||
tslib_1.__decorate([ | ||
mobx_1.computed | ||
], RouteMatchShared.prototype, "_pathFragments", null); | ||
tslib_1.__decorate([ | ||
mobx_1.computed | ||
], RouteMatchShared.prototype, "_query", null); | ||
class MatchingRouteMatch extends RouteMatchShared { | ||
constructor(name, source, parent, origin, history, options) { | ||
super(name, source, parent, history, options); | ||
this._origin = origin; | ||
} | ||
/** | ||
* Intercept route matching if this `RouteMatch` matches. | ||
* @param interception The interception callback. | ||
* @param exact Intercept only if it's an exact match. | ||
* A reactive value indicates whether this route is exactly matched. | ||
*/ | ||
$intercept(interception, exact = false) { | ||
this._interceptionEntries.push({ | ||
interception, | ||
exact, | ||
}); | ||
get $exact() { | ||
let entry = this._getMatchEntry(); | ||
return !!entry && entry.exact; | ||
} | ||
/** @internal */ | ||
_getMatchEntry() { | ||
return this._origin._getMatchEntry(this._source); | ||
} | ||
} | ||
exports.MatchingRouteMatch = MatchingRouteMatch; | ||
class RouteMatch extends RouteMatchShared { | ||
constructor(name, source, parent, history, _a) { | ||
var { exact } = _a, sharedOptions = tslib_1.__rest(_a, ["exact"]); | ||
super(name, source, parent, history, sharedOptions); | ||
/** @internal */ | ||
this._beforeEnterCallbacks = []; | ||
/** @internal */ | ||
this._beforeLeaveCallbacks = []; | ||
/** @internal */ | ||
this._afterEnterCallbacks = []; | ||
/** @internal */ | ||
this._afterLeaveCallbacks = []; | ||
/** @internal */ | ||
this._matched = false; | ||
/** @internal */ | ||
this._exactlyMatched = false; | ||
this._allowExact = 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. | ||
* A reactive value indicates whether this route is matched. | ||
*/ | ||
$react(reaction, exact = false) { | ||
mobx_1.autorun(() => { | ||
if (exact ? this.$exact : this.$matched) { | ||
_utils_1.then(() => reaction()); | ||
} | ||
}); | ||
get $matched() { | ||
return this._matched; | ||
} | ||
/** | ||
* A reactive value indicates whether this route is exactly matched. | ||
*/ | ||
get $exact() { | ||
return this._exactlyMatched; | ||
} | ||
$beforeEnter(callback) { | ||
this._beforeEnterCallbacks.push(callback); | ||
return this; | ||
} | ||
$beforeLeave(callback) { | ||
this._beforeLeaveCallbacks.push(callback); | ||
return this; | ||
} | ||
$afterEnter(callback) { | ||
this._afterEnterCallbacks.push(callback); | ||
return this; | ||
} | ||
$afterLeave(callback) { | ||
this._afterLeaveCallbacks.push(callback); | ||
return this; | ||
} | ||
$service(factory) { | ||
if (this._service) { | ||
throw new Error(`Service has already been defined for "${this.$name}"`); | ||
} | ||
this._service = factory(this); | ||
return this; | ||
} | ||
/** @internal */ | ||
_match(rest) { | ||
if (!rest) { | ||
return { | ||
fragment: undefined, | ||
rest: '', | ||
}; | ||
} | ||
if (!rest.startsWith('/')) { | ||
throw new Error(`Expecting rest of path to be started with "/", but got ${JSON.stringify(rest)} instead`); | ||
} | ||
rest = rest.slice(1); | ||
let pattern = this._matchPattern; | ||
if (typeof pattern === 'string') { | ||
if (_utils_1.isPathPrefix(rest, pattern)) { | ||
return { | ||
fragment: pattern, | ||
rest: rest.slice(pattern.length), | ||
}; | ||
_match(upperRest) { | ||
let fragment; | ||
let rest; | ||
if (upperRest) { | ||
if (!upperRest.startsWith('/')) { | ||
throw new Error(`Expecting rest of path to be started with "/", but got ${JSON.stringify(upperRest)} instead`); | ||
} | ||
else { | ||
return { | ||
fragment: undefined, | ||
rest: '', | ||
}; | ||
} | ||
} | ||
else { | ||
let groups = pattern.exec(rest); | ||
if (groups) { | ||
let matched = groups[0]; | ||
if (!_utils_1.isPathPrefix(rest, matched)) { | ||
throw new Error(`Invalid regular expression pattern, expecting rest of path to be started with "/" after match (matched ${JSON.stringify(matched)} out of ${JSON.stringify(rest)})`); | ||
upperRest = upperRest.slice(1); | ||
let pattern = this._matchPattern; | ||
if (typeof pattern === 'string') { | ||
if (_utils_1.isPathPrefix(upperRest, pattern)) { | ||
fragment = pattern; | ||
rest = upperRest.slice(pattern.length); | ||
} | ||
return { | ||
fragment: matched, | ||
rest: rest.slice(matched.length), | ||
}; | ||
else { | ||
fragment = undefined; | ||
rest = ''; | ||
} | ||
} | ||
else { | ||
return { | ||
fragment: undefined, | ||
rest: '', | ||
}; | ||
let groups = pattern.exec(upperRest); | ||
if (groups) { | ||
let matched = groups[0]; | ||
if (!_utils_1.isPathPrefix(upperRest, matched)) { | ||
throw new Error(`Invalid regular expression pattern, expecting rest of path to be started with "/" after match (matched ${JSON.stringify(matched)} out of ${JSON.stringify(upperRest)})`); | ||
} | ||
fragment = matched; | ||
rest = upperRest.slice(matched.length); | ||
} | ||
else { | ||
fragment = undefined; | ||
rest = ''; | ||
} | ||
} | ||
} | ||
else { | ||
fragment = undefined; | ||
rest = ''; | ||
} | ||
let matched = fragment !== undefined; | ||
let exactlyMatched = matched && rest === ''; | ||
if (exactlyMatched && (this._children && !this._allowExact)) { | ||
matched = false; | ||
exactlyMatched = false; | ||
} | ||
return { | ||
matched, | ||
exactlyMatched, | ||
fragment, | ||
rest, | ||
}; | ||
} | ||
/** @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; | ||
} | ||
_beforeLeave() { | ||
for (let callback of this._beforeLeaveCallbacks) { | ||
let result = callback(); | ||
if (result === false) { | ||
return false; | ||
} | ||
if (typeof result === 'string') { | ||
} | ||
let service = this._service; | ||
if (service && service.beforeLeave) { | ||
service.beforeLeave(); | ||
} | ||
return true; | ||
} | ||
/** @internal */ | ||
_beforeEnter() { | ||
let next = this._matching; | ||
for (let callback of this._beforeEnterCallbacks) { | ||
let result = callback(next); | ||
if (typeof result === 'string' || result === false) { | ||
return result; | ||
} | ||
throw new Error('Invalid interception result'); | ||
} | ||
return undefined; | ||
let service = this._service; | ||
if (service && service.beforeEnter) { | ||
service.beforeEnter(next); | ||
} | ||
return true; | ||
} | ||
/** @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); | ||
_afterLeave() { | ||
for (let callback of this._afterLeaveCallbacks) { | ||
callback(); | ||
} | ||
let service = this._service; | ||
if (service && service.afterLeave) { | ||
service.afterLeave(); | ||
} | ||
} | ||
/** @internal */ | ||
_afterEnter() { | ||
for (let callback of this._afterEnterCallbacks) { | ||
callback(); | ||
} | ||
let service = this._service; | ||
if (service && service.afterEnter) { | ||
service.afterEnter(); | ||
} | ||
} | ||
/** @internal */ | ||
_update(matched, exactlyMatched) { | ||
this._matched = matched; | ||
this._exact = exact; | ||
return { | ||
pathFragmentDict, | ||
paramFragmentDict, | ||
}; | ||
this._exactlyMatched = exactlyMatched; | ||
} | ||
/** @internal */ | ||
_getMatchEntry(source) { | ||
return source.matchToMatchEntryMap.get(this); | ||
} | ||
} | ||
@@ -219,7 +320,4 @@ RouteMatch.fragment = /[^/]+/; | ||
mobx_1.observable | ||
], RouteMatch.prototype, "_exact", void 0); | ||
tslib_1.__decorate([ | ||
mobx_1.observable | ||
], RouteMatch.prototype, "_params", void 0); | ||
], RouteMatch.prototype, "_exactlyMatched", void 0); | ||
exports.RouteMatch = RouteMatch; | ||
//# sourceMappingURL=route-match.js.map |
import { History } from 'history'; | ||
import { RouteMatch } from './route-match'; | ||
import { GeneralQueryDict, RouteMatch } from './route-match'; | ||
import { RouteSchemaDict } from './schema'; | ||
@@ -11,5 +11,10 @@ export declare type FragmentMatcherCallback = (key: string) => string; | ||
} ? TMatch extends string ? never : T : never; | ||
interface RouteSchemaChildrenPartial<TRouteSchemaDict> { | ||
interface RouteSchemaChildrenSection<TRouteSchemaDict> { | ||
$children: TRouteSchemaDict; | ||
} | ||
export declare type NestedRouteSchemaDictType<TRouteSchema> = TRouteSchema extends RouteSchemaChildrenSection<infer TNestedRouteSchemaDict> ? TNestedRouteSchemaDict : {}; | ||
interface RouteSchemaExtensionSection<TRouteMatchExtension> { | ||
$extension: TRouteMatchExtension; | ||
} | ||
export declare type RouteMatchExtensionType<TRouteSchema> = TRouteSchema extends RouteSchemaExtensionSection<infer TRouteMatchExtension> ? TRouteMatchExtension : {}; | ||
export declare type RouteMatchFragmentType<TRouteSchemaDict, TFragmentKey extends string> = { | ||
@@ -20,3 +25,3 @@ [K in Extract<keyof TRouteSchemaDict, string>]: RouteMatchType<TRouteSchemaDict[K], TFragmentKey | FilterRouteMatchNonStringFragment<TRouteSchemaDict[K], K>>; | ||
[K in TFragmentKey]: string; | ||
}> & (TRouteSchema extends RouteSchemaChildrenPartial<infer TNestedRouteSchemaDict> ? RouteMatchFragmentType<TNestedRouteSchemaDict, TFragmentKey> : {}); | ||
}> & RouteMatchFragmentType<NestedRouteSchemaDictType<TRouteSchema>, TFragmentKey> & RouteMatchExtensionType<TRouteSchema>; | ||
export declare type RouterType<TRouteSchemaDict> = Router & RouteMatchFragmentType<TRouteSchemaDict, never>; | ||
@@ -28,2 +33,6 @@ export interface RouteMatchEntry { | ||
} | ||
export interface RouteSource { | ||
matchToMatchEntryMap: Map<RouteMatch, RouteMatchEntry>; | ||
queryDict: GeneralQueryDict; | ||
} | ||
export interface RouterOptions { | ||
@@ -35,7 +44,10 @@ /** | ||
fragmentMatcher?: FragmentMatcherCallback; | ||
/** Default path on error. */ | ||
default?: string; | ||
} | ||
export declare class Router { | ||
private constructor(); | ||
private _revert; | ||
static create<TSchema extends RouteSchemaDict>(schema: TSchema, history: History, options?: RouterOptions): RouterType<TSchema>; | ||
} | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const history_1 = require("history"); | ||
const hyphenate_1 = tslib_1.__importDefault(require("hyphenate")); | ||
const mobx_1 = require("mobx"); | ||
const _utils_1 = require("./@utils"); | ||
@@ -9,5 +11,20 @@ const route_match_1 = require("./route-match"); | ||
class Router { | ||
constructor(schema, history, { fragmentMatcher }) { | ||
constructor(schema, history, { fragmentMatcher, default: defaultPath = '/' }) { | ||
/** @internal */ | ||
this._onLocationChange = ({ pathname, search }) => { | ||
this._source = mobx_1.observable({ | ||
matchToMatchEntryMap: new Map(), | ||
queryDict: {}, | ||
}); | ||
/** @internal */ | ||
this._matchingSource = mobx_1.observable({ | ||
matchToMatchEntryMap: new Map(), | ||
queryDict: {}, | ||
}); | ||
/** @internal */ | ||
this._onLocationChange = ({ pathname, search }, action) => { | ||
let history = this._history; | ||
let location = history.location; | ||
if (history_1.locationsAreEqual(this._location, location)) { | ||
return; | ||
} | ||
let searchParams = new URLSearchParams(search); | ||
@@ -18,60 +35,87 @@ let queryDict = Array.from(searchParams).reduce((dict, [key, value]) => { | ||
}, {}); | ||
let matchResult = this._match(this, pathname); | ||
if (typeof matchResult === 'string') { | ||
this._history.replace(matchResult); | ||
return; | ||
let routeMatchEntries = this._match(this, pathname) || []; | ||
let matchToMatchEntryMap = new Map(routeMatchEntries.map((entry) => [entry.match, entry])); | ||
Object.assign(this._matchingSource, { | ||
matchToMatchEntryMap, | ||
queryDict, | ||
}); | ||
// Prepare previous/next match set | ||
let previousMatchSet = new Set(this._source.matchToMatchEntryMap.keys()); | ||
let nextMatchSet = new Set(matchToMatchEntryMap.keys()); | ||
let leavingMatchSet = new Set(previousMatchSet); | ||
for (let match of nextMatchSet) { | ||
leavingMatchSet.delete(match); | ||
} | ||
let routeMatchEntryMap = new Map(matchResult | ||
? matchResult.map((entry) => [entry.match, entry]) | ||
: undefined); | ||
this._update(this, routeMatchEntryMap, {}, {}, queryDict); | ||
let enteringMatchSet = new Set(nextMatchSet); | ||
for (let match of previousMatchSet) { | ||
enteringMatchSet.delete(match); | ||
} | ||
// Process before hooks | ||
for (let match of Array.from(leavingMatchSet).reverse()) { | ||
let result = match._beforeLeave(); | ||
if (!result) { | ||
this._revert(action); | ||
return; | ||
} | ||
} | ||
for (let match of enteringMatchSet) { | ||
let result = match._beforeEnter(); | ||
if (typeof result === 'string') { | ||
history.replace(result); | ||
return; | ||
} | ||
if (!result) { | ||
this._revert(action); | ||
return; | ||
} | ||
} | ||
this._location = location; | ||
Object.assign(this._source, this._matchingSource); | ||
// Update | ||
for (let match of leavingMatchSet) { | ||
match._update(false, false); | ||
} | ||
for (let match of nextMatchSet) { | ||
let { exact } = matchToMatchEntryMap.get(match); | ||
match._update(true, exact); | ||
} | ||
// Process after hooks | ||
for (let match of leavingMatchSet) { | ||
match._afterLeave(); | ||
} | ||
for (let match of enteringMatchSet) { | ||
match._afterEnter(); | ||
} | ||
}; | ||
this._history = history; | ||
this._location = history_1.parsePath(defaultPath); | ||
this._fragmentMatcher = | ||
fragmentMatcher || DEFAULT_FRAGMENT_MATCHER_CALLBACK; | ||
this._children = this._build(this, schema); | ||
this._update(this, new Map(), {}, {}, {}); | ||
this._children = this._build(schema, this); | ||
_utils_1.then(() => { | ||
history.listen(this._onLocationChange); | ||
this._onLocationChange(history.location); | ||
this._onLocationChange(history.location, 'POP'); | ||
}); | ||
} | ||
_revert(action) { | ||
let history = this._history; | ||
let location = this._location; | ||
switch (action) { | ||
case 'PUSH': | ||
history.goBack(); | ||
break; | ||
case 'POP': | ||
case 'REPLACE': | ||
history.replace(location); | ||
break; | ||
} | ||
} | ||
/** @internal */ | ||
_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) { | ||
let interceptionResult = routeMatch._intercept(exact); | ||
if (typeof interceptionResult === 'string') { | ||
return interceptionResult; | ||
} | ||
else if (interceptionResult === false) { | ||
matched = false; | ||
exact = false; | ||
} | ||
} | ||
let { matched, exactlyMatched, fragment, rest } = routeMatch._match(upperRest); | ||
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) { | ||
if (exactlyMatched) { | ||
return [ | ||
@@ -81,7 +125,18 @@ { | ||
fragment: fragment, | ||
exact: false, | ||
exact: true, | ||
}, | ||
...result, | ||
]; | ||
} | ||
let result = this._match(routeMatch, rest); | ||
if (!result) { | ||
continue; | ||
} | ||
return [ | ||
{ | ||
match: routeMatch, | ||
fragment: fragment, | ||
exact: false, | ||
}, | ||
...result, | ||
]; | ||
} | ||
@@ -91,24 +146,7 @@ return undefined; | ||
/** @internal */ | ||
_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) { | ||
_build(schemaDict, parent, matchingParent) { | ||
let routeMatches = []; | ||
let source = this._source; | ||
let matchingSource = this._matchingSource; | ||
let history = this._history; | ||
for (let [key, schema] of Object.entries(schemaDict)) { | ||
@@ -118,14 +156,31 @@ if (typeof schema === 'boolean') { | ||
} | ||
let { $match: match = this._fragmentMatcher(key), $query: query, $exact: exact = false, $children: children, } = schema; | ||
let routeMatch = new route_match_1.RouteMatch(key, this._history, { | ||
let { $match: match = this._fragmentMatcher(key), $query: query, $exact: exact = false, $children: children, $extension: extension = {}, } = schema; | ||
let options = { | ||
match, | ||
query, | ||
exact, | ||
}); | ||
}; | ||
let routeMatch = new route_match_1.RouteMatch(key, source, parent instanceof route_match_1.RouteMatch ? parent : undefined, history, options); | ||
mobx_1.extendObservable(routeMatch, extension); | ||
let matchingRouteMatch = new route_match_1.MatchingRouteMatch(key, matchingSource, matchingParent, routeMatch, history, options); | ||
for (let key of Object.keys(extension)) { | ||
Object.defineProperty(matchingRouteMatch, key, { | ||
get() { | ||
return routeMatch[key]; | ||
}, | ||
set(value) { | ||
routeMatch[key] = value; | ||
}, | ||
}); | ||
} | ||
routeMatch._matching = matchingRouteMatch; | ||
routeMatches.push(routeMatch); | ||
target[key] = routeMatch; | ||
parent[key] = routeMatch; | ||
if (matchingParent) { | ||
matchingParent[key] = matchingRouteMatch; | ||
} | ||
if (!children) { | ||
continue; | ||
} | ||
routeMatch._children = this._build(routeMatch, children); | ||
routeMatch._children = this._build(children, routeMatch, matchingRouteMatch); | ||
} | ||
@@ -138,3 +193,6 @@ return routeMatches; | ||
} | ||
tslib_1.__decorate([ | ||
mobx_1.observable | ||
], Router.prototype, "_matchingSource", void 0); | ||
exports.Router = Router; | ||
//# sourceMappingURL=router.js.map |
@@ -11,4 +11,5 @@ import { Dict } from 'tslang'; | ||
$children?: RouteSchemaDict; | ||
$extension?: object; | ||
} | ||
export declare type RouteSchemaDict = Dict<RouteSchema | boolean>; | ||
export declare function schema<T extends RouteSchemaDict>(schema: T): T; |
@@ -5,2 +5,7 @@ # Changelog | ||
### Changes | ||
- Added hooks `$beforeEnter`, `$afterEnter`, `$beforeLeave` and `$afterLeave`. | ||
- Added service `$service`. | ||
### Breaking changes | ||
@@ -10,3 +15,3 @@ | ||
- 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`. | ||
- `RouteMatch#$action` has been removed, see hooks for alternatives. | ||
@@ -13,0 +18,0 @@ ## [0.2.1] - 2018-9-5 |
{ | ||
"name": "boring-router", | ||
"version": "0.3.0-alpha.4", | ||
"version": "0.3.0-alpha.5", | ||
"description": "A light-weight, type-safe, yet reactive router service using MobX.", | ||
@@ -42,3 +42,3 @@ "repository": { | ||
"jest": "^23.5.0", | ||
"mobx": "^5.1.0", | ||
"mobx": "^5.5.0", | ||
"mobx-react": "^5.2.5", | ||
@@ -58,4 +58,4 @@ "prettier": "^1.13.7", | ||
"hyphenate": "^0.2.4", | ||
"tslang": "^0.1.7" | ||
"tslang": "^0.1.9" | ||
} | ||
} |
@@ -212,12 +212,29 @@ [](https://www.npmjs.com/package/boring-router) | ||
- [Reaction](examples/reaction/main.tsx) | ||
- [Hooks](examples/hooks/main.tsx) | ||
Take a reaction on route match. | ||
Add hooks to route match. | ||
```tsx | ||
router.account.$react(() => { | ||
router.about.$replace({source: 'reaction'}); | ||
router.account.$beforeEnter(() => { | ||
return router.about.$ref(); | ||
}); | ||
``` | ||
- [Service](examples/service/main.tsx) | ||
Add service to route match. | ||
```tsx | ||
router.account.$service(match => { | ||
return { | ||
beforeEnter() { | ||
match.account = new Account(); | ||
}, | ||
afterLeave() { | ||
match.account = undefined; | ||
}, | ||
}; | ||
}); | ||
``` | ||
### Run an example | ||
@@ -224,0 +241,0 @@ |
43797
884
253
Updatedtslang@^0.1.9