id: 'no-name',
},
})
+
+r.push({
+ hash: '#hey',
+})
to
)
this.location = to
- cs.warn('changed location to', this.location)
}
replace(to: HistoryLocation) {
to
)
this.location = to
- cs.warn('changed location to', this.location)
}
push(to: HistoryLocation, data?: HistoryState) {
cs.info('push', this.location, '->', to, 'with state', state)
this.history.pushState(state, '', to)
this.location = to
- cs.warn('changed location to', this.location)
}
listen(callback: NavigationCallback) {
// we have the state from the old entry, not the current one being removed
// TODO: correctly parse pathname
this.location = state ? state._current : buildFullPath
- cs.warn('changed location to', this.location)
callback(this.location, from, {
type:
from === state._forward
import BaseHistory from './history/base'
-import pathToRegexp from 'path-to-regexp'
+import { RouterMatcher } from './matcher'
import {
RouterLocation,
RouteRecord,
- ParamsType,
- START_RECORD,
+ START_LOCATION_NORMALIZED,
+ RouterLocationNormalized,
} from './types/index'
interface RouterOptions {
routes: RouteRecord[]
}
-interface RouteMatcher {
- re: RegExp
- resolve: (params: ParamsType) => string
- record: RouteRecord
- keys: string[]
-}
-
-function generateMatcher(record: RouteRecord) {
- const keys: pathToRegexp.Key[] = []
- // TODO: if children use option end: false ?
- const re = pathToRegexp(record.path, keys)
- return {
- re,
- resolve: pathToRegexp.compile(record.path),
- keys: keys.map(k => '' + k.name),
- record,
- }
-}
-
-const START_MATCHER = generateMatcher(START_RECORD)
-
export class Router {
protected history: BaseHistory
- private routes: RouteMatcher[]
- currentRoute: RouteRecord = START_RECORD
- currentMatcher: RouteMatcher = START_MATCHER
+ private matcher: RouterMatcher
+ currentRoute: RouterLocationNormalized = START_LOCATION_NORMALIZED
constructor(options: RouterOptions) {
this.history = options.history
this.history.ensureLocation()
- this.routes = options.routes.map(generateMatcher)
+ this.matcher = new RouterMatcher(options.routes)
}
/**
*/
push(to: RouterLocation) {
// TODO: resolve URL
- const path = this.resolve(to)
+ const path = this.matcher.resolve(to, this.currentRoute)
// TODO: call hooks, guards
- this.history.push(
- path +
- // TODO: test purposes only
- '?value=' +
- Math.round(Math.random() * 10) +
- '#e' +
- Math.round(Math.random() * 10)
- )
+ this.history.push(path)
}
getRouteRecord(location: RouterLocation) {}
-
- /**
- * Transforms a RouterLocation object into a URL string. If a string is
- * passed, it returns the string itself
- * @param location RouterLocation to resolve to a url
- */
- resolve(location: Readonly<RouterLocation>): string {
- if (typeof location === 'string') return location
- if ('path' in location) {
- // TODO: convert query, hash, warn params
- return location.path
- }
-
- let matcher: RouteMatcher | void
- if (!('name' in location)) {
- // TODO: use current location
- // location = {...location, name: this.}
- matcher = this.routes.find(r => r.record.name === this.currentRoute.name)
- // return '/using current location'
- } else {
- matcher = this.routes.find(r => r.record.name === location.name)
- }
-
- if (!matcher) {
- // TODO: error
- throw new Error('No match for' + location)
- }
-
- return matcher.resolve(location.params || {})
- }
}
import pathToRegexp from 'path-to-regexp'
import {
RouteRecord,
- ParamsType,
- START_RECORD,
+ RouteParams,
RouterLocation,
RouterLocationNormalized,
} from './types/index'
+import { stringifyQuery } from './uitls'
// TODO: rename
interface RouteMatcher {
re: RegExp
- resolve: (params: ParamsType) => string
+ resolve: (params: RouteParams) => string
record: RouteRecord
keys: string[]
}
}
}
-const START_MATCHER = generateMatcher(START_RECORD)
-
export class RouterMatcher {
private matchers: RouteMatcher[] = []
* passed, it returns the string itself
* @param location RouterLocation to resolve to a url
*/
- resolve(location: Readonly<RouterLocation>): string {
+ resolve(
+ location: Readonly<RouterLocation>,
+ currentLocation: RouterLocationNormalized
+ ): string {
if (typeof location === 'string') return location
+
if ('path' in location) {
- // TODO: convert query, hash, warn params
- return location.path
+ // TODO: warn missing params
+ return (
+ location.path + stringifyQuery(location.query) + (location.hash || '')
+ )
}
let matcher: RouteMatcher | void
if (!('name' in location)) {
// TODO: use current location
// location = {...location, name: this.}
- matcher = this.routes.find(r => r.record.name === this.currentRoute.name)
+ if (currentLocation.name) {
+ // we don't want to match an undefined name
+ matcher = this.matchers.find(
+ m => m.record.name === currentLocation.name
+ )
+ } else {
+ matcher = this.matchers.find(
+ m => m.record.path === currentLocation.path
+ )
+ }
// return '/using current location'
} else {
- matcher = this.routes.find(r => r.record.name === location.name)
+ matcher = this.matchers.find(m => m.record.name === location.name)
}
if (!matcher) {
type TODO = any
-export type ParamsType = Record<string, string | string[]>
+export type RouteParams = Record<string, string | string[]>
+export type RouteQuery = Record<string, string | null>
// interface PropsTransformer {
-// (params: ParamsType): any
+// (params: RouteParams): any
// }
// export interface RouterLocation<PT extends PropsTransformer> {
| string
| {
path: string
+ query?: RouteQuery
+ hash?: string
}
| {
name: string
- params?: ParamsType
+ params?: RouteParams
+ query?: RouteQuery
+ hash?: string
}
| {
- params: ParamsType
+ params?: RouteParams
+ query?: RouteQuery
+ hash?: string
}
export interface RouterLocationNormalized {
path: string
name?: string
- params: ParamsType
+ params: RouteParams
query: TODO
hash: TODO
}
component: { render: h => h() },
}
+export const START_LOCATION_NORMALIZED: RouterLocationNormalized = {
+ path: '/',
+ params: {},
+ query: {},
+ hash: '',
+}
+
export enum NavigationType {
back,
forward,
--- /dev/null
+import { RouteQuery } from '../types'
+
+export function stringifyQuery(query: RouteQuery | void): string {
+ if (!query) return ''
+
+ let search = '?'
+ for (const key in query) {
+ search += `${key}=${query[key]}`
+ }
+
+ // no query means empty string
+ return search === '?' ? '' : ''
+}