-import { START, Location } from '../types/index'
+import { START, HistoryLocation } from '../types/index'
export interface NavigationCallback {
- (location: string): void
+ (to: HistoryLocation, from: HistoryLocation): void
}
export default abstract class BaseHistory {
// previousState: object
- location: Location
- abstract push(to: Location): void
+ location: HistoryLocation
+ abstract push(to: HistoryLocation): void
+ abstract replace(to: HistoryLocation): void
abstract listen(callback: NavigationCallback): Function
+ /**
+ * ensure the current location using the external source
+ * for example, in HTML5 and hash history, that would be
+ * location.pathname
+ */
+ abstract ensureLocation(): void
constructor() {
this.location = START
import BaseHistory, { NavigationCallback } from './base'
-import { Location } from '../types/index'
+import { HistoryLocation } from '../types/index'
export default class HTML5History extends BaseHistory {
- history: typeof window.history
+ private history = window.history
constructor() {
super()
- this.history = window.history
}
- push(to: Location): void {
- // TODO resolve url
- this.history.pushState(
+ ensureLocation() {
+ const to = window.location.pathname
+ this.replace(to)
+ }
+
+ replace(to: HistoryLocation) {
+ if (to === this.location) return
+ this.history.replaceState(
{
- from: this.location,
+ ...(this.history.state || {}),
to,
},
'',
to
)
+ this.location = to
+ }
+
+ push(to: HistoryLocation) {
+ // TODO resolve url
+ // TODO compare current location to prevent navigation
+ if (to === this.location) return
+ const state = {
+ from: this.location,
+ to,
+ }
+ console.log('push', state)
+ this.history.pushState(state, '', to)
+ this.location = to
}
listen(callback: NavigationCallback): Function {
+ window.addEventListener('popstate', ({ state }) => {
+ const from = this.location
+ // we have the state from the old entry, not the current one being removed
+ // TODO correctly parse pathname
+ this.location = state ? state.to : window.location.pathname
+ callback(this.location, from)
+ })
+
return () => {}
}
}
-class Router {}
+import BaseHistory from './history/base'
+import pathToRegexp from 'path-to-regexp'
+import { Location, RouteRecord } from './types/index'
+
+interface RouterOptions {
+ history: BaseHistory
+ routes: RouteRecord[]
+}
+
+interface RouteMatcher {
+ re: RegExp
+ record: RouteRecord
+ keys: string[]
+}
+
+export class Router {
+ protected history: BaseHistory
+ private routes: RouteMatcher[]
+
+ constructor(options: RouterOptions) {
+ this.history = options.history
+ this.history.ensureLocation()
+
+ this.routes = options.routes.map(record => {
+ const keys: pathToRegexp.Key[] = []
+ // TODO: if children use option end: false ?
+ const re = pathToRegexp(record.path, keys)
+ return { re, keys: keys.map(k => '' + k.name), record }
+ })
+ }
+
+ /**
+ * Trigger a navigation, should resolve all guards first
+ * @param to Where to go
+ */
+ push(to: Location) {
+ // TODO: resolve URL
+ const path = this.resolve(to)
+ // TODO: call hooks, guards
+ this.history.push(path)
+ }
+
+ getRouteRecord(location: Location) {}
+
+ /**
+ * Transforms a Location object into a URL string. If a string is
+ * passed, it returns the string itself
+ * @param location Location to resolve to a url
+ */
+ resolve(location: Location): string {
+ if (typeof location === 'string') return location
+ if ('path' in location) {
+ // TODO: convert query, hash, warn params
+ return location.path
+ }
+
+ if (!('name' in location)) {
+ // TODO: use current location
+ // location = {...location, name: this.}
+ return '/heeey'
+ }
+ const matcher = this.routes.find(r => r.record.name === location.name)
+ if (!matcher) {
+ // TODO: error
+ throw new Error('No match for' + location)
+ }
+ matcher.re
+ return '/TODO'
+ // this.matcher.match(location)
+ }
+}
type TODO = any
-type ParamsType = Record<string, string | string[]>
+export type ParamsType = Record<string, string | string[]>
-interface PropsTransformer {
- (params: ParamsType): any
-}
+// interface PropsTransformer {
+// (params: ParamsType): any
+// }
// export interface Location<PT extends PropsTransformer> {
// record: RouteRecord<PT>
// since in callbacks we don't know where we are coming from
// and I don't thin it's possible to filter out the route
// by any means
-export interface RouteRecord<PT extends PropsTransformer> {
+export interface RouteRecord {
path: string | RegExp
component: TODO
- name: string
- props: PT
+ name?: string
+ // props: PT
}
// TODO location should be an object
-export type Location = string
+export type Location =
+ | string
+ | {
+ path: string
+ }
+ | {
+ name: string
+ params?: Record<string, string>
+ }
+export type HistoryLocation = string
-export const START: Location = ''
+export const START: HistoryLocation = '/'
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
- // "sourceMap": true, /* Generates corresponding '.map' file. */
+ "sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */