From: Eduardo San Martin Morote Date: Thu, 11 Apr 2019 13:09:23 +0000 (+0200) Subject: refactor: use named exports X-Git-Tag: v4.0.0-alpha.0~464 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=446aa9088587310c3e7044198a3a183ae65ef7c4;p=thirdparty%2Fvuejs%2Frouter.git refactor: use named exports --- diff --git a/__tests__/html5.spec.ts b/__tests__/html5.spec.ts index bd05fa08..65c4ecb6 100644 --- a/__tests__/html5.spec.ts +++ b/__tests__/html5.spec.ts @@ -1,4 +1,4 @@ -import History from '../src/history/html5' +import { HTML5History } from '../src/history/html5' import { JSDOM } from 'jsdom' describe('History HTMl5', () => { @@ -18,7 +18,7 @@ describe('History HTMl5', () => { }) it('can be instantiated', () => { - const history = new History() + const history = new HTML5History() expect(history.location).toBe('/') }) }) diff --git a/__tests__/router.spec.ts b/__tests__/router.spec.ts new file mode 100644 index 00000000..b27e1ffd --- /dev/null +++ b/__tests__/router.spec.ts @@ -0,0 +1,32 @@ +import { HTML5History } from '../src/history/html5' +import { Router } from '../src/router' +import { JSDOM } from 'jsdom' + +describe('Router', () => { + beforeAll(() => { + // TODO: move to utils for tests that need DOM + const dom = new JSDOM( + ``, + { + url: 'https://example.org/', + referrer: 'https://example.com/', + contentType: 'text/html', + } + ) + + // @ts-ignore + global.window = dom.window + }) + + it('can be instantiated', () => { + const history = new HTML5History() + const router = new Router({ history, routes: [] }) + expect(router.currentRoute).toEqual({ + fullPath: '/', + hash: '', + params: {}, + path: '/', + query: {}, + }) + }) +}) diff --git a/explorations/html5.ts b/explorations/html5.ts index fa922ca6..9af1d6f5 100644 --- a/explorations/html5.ts +++ b/explorations/html5.ts @@ -1,5 +1,4 @@ -import HTML5History from '../src/history/html5' -import { Router } from '../src' +import { Router, HTML5History } from '../src' const component = null diff --git a/package.json b/package.json index 2de6cf19..8062749b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "license": "MIT", "scripts": { "test:unit": "jest --coverage", + "test:unit:watch": "yarn run test:unit --watchAll", "dev": "webpack-dev-server --mode=development" }, "devDependencies": { diff --git a/src/history/abstract.ts b/src/history/abstract.ts new file mode 100644 index 00000000..d21ee4c9 --- /dev/null +++ b/src/history/abstract.ts @@ -0,0 +1,166 @@ +import consola from 'consola' +import { BaseHistory } from './base' +import { + HistoryLocation, + NavigationCallback, + HistoryState, + NavigationType, + HistoryURL, +} from './base' + +const cs = consola.withTag('html5') + +type PopStateListener = (this: Window, ev: PopStateEvent) => any + +export class AbstractHistory extends BaseHistory { + private history = window.history + private _popStateListeners: PopStateListener[] = [] + private _listeners: NavigationCallback[] = [] + private _teardowns: Array<() => void> = [] + + constructor() { + super() + } + + // TODO: is this necessary + ensureLocation() { + const to = buildFullPath() + cs.log('ensureLocation', to) + this.history.replaceState( + { + _back: null, + _current: to, + _forward: null, + }, + '', + to + ) + this.location = to + } + + replace(to: HistoryLocation) { + if (to === this.location) return + cs.info('replace', this.location, to) + this.history.replaceState( + { + // TODO: this should be user's responsibility + // _replacedState: this.history.state || null, + _back: this.location, + _current: to, + _forward: null, + _replaced: true, + }, + '', + to + ) + this.location = to + } + + push(to: HistoryLocation, data?: HistoryState) { + // replace current entry state to add the forward value + this.history.replaceState( + { + ...this.history.state, + _forward: to, + }, + '' + ) + // TODO: compare current location to prevent navigation + // NEW NOTE: I think it shouldn't be history responsibility to check that + // if (to === this.location) return + const state = { + _back: this.location, + _current: to, + _forward: null, + ...data, + } + cs.info('push', this.location, '->', to, 'with state', state) + this.history.pushState(state, '', to) + this.location = to + } + + listen(callback: NavigationCallback) { + // state is the same as history.state + const handler: PopStateListener = ({ state }) => { + cs.log(this) + cs.info('popstate fired', { + state, + location: this.location, + }) + 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._current : buildFullPath + callback(this.location, from, { + type: + from === state._forward + ? NavigationType.back + : NavigationType.forward, + }) + } + + // settup the listener and prepare teardown callbacks + this._popStateListeners.push(handler) + this._listeners.push(callback) + window.addEventListener('popstate', handler) + + const teardown = () => { + this._popStateListeners.splice( + this._popStateListeners.indexOf(handler), + 1 + ) + this._listeners.splice(this._listeners.indexOf(callback), 1) + window.removeEventListener('popstate', handler) + } + + this._teardowns.push(teardown) + return teardown + } + + parseURL(location: string): HistoryURL { + let path = '', + search: HistoryURL['search'] = {}, + searchString = '', + hash = '' + + // Could use URL and URLSearchParams but IE 11 doesn't support it + const searchPos = location.indexOf('?') + const hashPos = location.indexOf(location, searchPos > -1 ? searchPos : 0) + if (searchPos > -1) { + path = location.slice(0, searchPos) + searchString = location.slice( + searchPos + 1, + hashPos > -1 ? hashPos : location.length - 1 + ) + + // TODO: properly do this in a util function + search = searchString.split('&').reduce((search, entry) => { + const [key, value] = entry.split('=') + search[key] = value + return search + }, search) + } + + if (hashPos > -1) { + path = path || location.slice(0, hashPos) + hash = location.slice(hashPos, location.length - 1) + } + + path = path || location + + return { + path, + // TODO: transform searchString + search, + hash, + } + } + + destroy() { + for (const teardown of this._teardowns) teardown() + this._teardowns = [] + } +} + +const buildFullPath = () => + window.location.pathname + window.location.search + window.location.hash diff --git a/src/history/base.ts b/src/history/base.ts index 007749e3..8c396336 100644 --- a/src/history/base.ts +++ b/src/history/base.ts @@ -37,7 +37,7 @@ export interface NavigationCallback { export type RemoveListener = () => void -export default abstract class BaseHistory { +export abstract class BaseHistory { // previousState: object location: HistoryLocation = START base: string = '' diff --git a/src/history/html5.ts b/src/history/html5.ts index 8214e01a..90d69a92 100644 --- a/src/history/html5.ts +++ b/src/history/html5.ts @@ -1,5 +1,5 @@ import consola from 'consola' -import BaseHistory from './base' +import { BaseHistory } from './base' import { HistoryLocation, NavigationCallback, @@ -12,7 +12,7 @@ const cs = consola.withTag('html5') type PopStateListener = (this: Window, ev: PopStateEvent) => any -export default class HTML5History extends BaseHistory { +export class HTML5History extends BaseHistory { private history = window.history private _popStateListeners: PopStateListener[] = [] private _listeners: NavigationCallback[] = [] diff --git a/src/index.ts b/src/index.ts index 61adf906..61e7511b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,40 +1,4 @@ -import BaseHistory from './history/base' -import { RouterMatcher } from './matcher' -import { - RouterLocation, - RouteRecord, - START_LOCATION_NORMALIZED, - RouterLocationNormalized, -} from './types/index' +import { Router } from './router' +import { HTML5History } from './history/html5' -interface RouterOptions { - history: BaseHistory - routes: RouteRecord[] -} - -export class Router { - protected history: BaseHistory - private matcher: RouterMatcher - currentRoute: RouterLocationNormalized = START_LOCATION_NORMALIZED - - constructor(options: RouterOptions) { - this.history = options.history - this.history.ensureLocation() - - this.matcher = new RouterMatcher(options.routes) - } - - /** - * Trigger a navigation, should resolve all guards first - * @param to Where to go - */ - push(to: RouterLocation) { - // TODO: resolve URL - const location = this.matcher.resolve(to, this.currentRoute) - // TODO: call hooks, guards - this.history.push(location.fullPath) - this.currentRoute = location - } - - getRouteRecord(location: RouterLocation) {} -} +export { Router, HTML5History } diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 00000000..52d985fa --- /dev/null +++ b/src/router.ts @@ -0,0 +1,40 @@ +import { BaseHistory } from './history/base' +import { RouterMatcher } from './matcher' +import { + RouterLocation, + RouteRecord, + START_LOCATION_NORMALIZED, + RouterLocationNormalized, +} from './types/index' + +interface RouterOptions { + history: BaseHistory + routes: RouteRecord[] +} + +export class Router { + protected history: BaseHistory + private matcher: RouterMatcher + currentRoute: RouterLocationNormalized = START_LOCATION_NORMALIZED + + constructor(options: RouterOptions) { + this.history = options.history + this.history.ensureLocation() + + this.matcher = new RouterMatcher(options.routes) + } + + /** + * Trigger a navigation, should resolve all guards first + * @param to Where to go + */ + push(to: RouterLocation) { + // TODO: resolve URL + const location = this.matcher.resolve(to, this.currentRoute) + // TODO: call hooks, guards + this.history.push(location.fullPath) + this.currentRoute = location + } + + getRouteRecord(location: RouterLocation) {} +}