From: Eduardo San Martin Morote Date: Fri, 29 Mar 2019 18:27:46 +0000 (+0100) Subject: feat: add previous, next, current, replaced and navigation type X-Git-Tag: v4.0.0-alpha.0~473 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b01b33da32947544c7829b6862fc4519533e3a11;p=thirdparty%2Fvuejs%2Frouter.git feat: add previous, next, current, replaced and navigation type --- diff --git a/explorations/html5.ts b/explorations/html5.ts index 32db56ec..b0241d29 100644 --- a/explorations/html5.ts +++ b/explorations/html5.ts @@ -12,14 +12,16 @@ const r = new Router({ ], }) -const h = new HTML5History() +// const h = new HTML5History() +// @ts-ignore +const h = r.history // @ts-ignore window.h = h // @ts-ignore window.r = r -h.listen((to, from) => { - console.log('popstate', { to, from }) +h.listen((to, from, { type }) => { + console.log(`popstate(${type})`, { to, from }) }) // h.push('/hey') diff --git a/src/history/base.ts b/src/history/base.ts index 28b6d548..35833b0e 100644 --- a/src/history/base.ts +++ b/src/history/base.ts @@ -8,13 +8,14 @@ import { export default abstract class BaseHistory { // previousState: object location: HistoryLocation = START + base: string = '' /** * Sync source with a different location. * Adds an entry to the history * @param to URL to go to */ - abstract push(to: HistoryLocation): void + abstract push(to: HistoryLocation, data?: any): void /** * Syncs source with a different location @@ -36,6 +37,7 @@ export default abstract class BaseHistory { * ensure the current location matches the external source * For example, in HTML5 and hash history, that would be * location.pathname + * TODO: is this necessary? */ abstract ensureLocation(): void } diff --git a/src/history/html5.ts b/src/history/html5.ts index ca666827..2dc939e5 100644 --- a/src/history/html5.ts +++ b/src/history/html5.ts @@ -1,61 +1,130 @@ +import consola from 'consola' import BaseHistory from './base' -import { HistoryLocation, NavigationCallback } from '../types/index' +import { + HistoryLocation, + NavigationCallback, + HistoryState, + NavigationType, +} from '../types/index' + +const cs = consola.withTag('html5') + +type PopStateListener = (this: Window, ev: PopStateEvent) => any export default class HTML5History extends BaseHistory { private history = window.history - private _popStateListener: - | null - | ((this: Window, ev: PopStateEvent) => any) = null + private _popStateListeners: PopStateListener[] = [] + private _listeners: NavigationCallback[] = [] + private _teardowns: Array<() => void> = [] constructor() { super() } + // TODO: is this necessary ensureLocation() { - const to = window.location.pathname - this.replace(to) + const to = buildFullPath() + cs.log('ensureLocation', to) + this.history.replaceState( + { + _back: null, + _current: to, + _forward: null, + }, + '', + to + ) + this.location = to + cs.warn('changed location to', this.location) } replace(to: HistoryLocation) { if (to === this.location) return - console.log('replace', this.location, to) + cs.info('replace', this.location, to) this.history.replaceState( { - replacedState: this.history.state || {}, - from: this.location, - to, + // 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 + cs.warn('changed location to', this.location) } - push(to: HistoryLocation) { - // TODO: resolve url + 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 - if (to === this.location) return + // NEW NOTE: I think it shouldn't be history responsibility to check that + // if (to === this.location) return const state = { - from: this.location, - to, + _back: this.location, + _current: to, + _forward: null, + ...data, } - console.log('push', this.location, to) + 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) { - this._popStateListener = ({ state }) => { + // 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.to : window.location.pathname - callback(this.location, from) + this.location = state ? state._current : buildFullPath + cs.warn('changed location to', this.location) + callback(this.location, from, { + type: + from === state._forward + ? NavigationType.back + : NavigationType.forward, + }) } - window.addEventListener('popstate', this._popStateListener) - return () => { - window.removeEventListener('popstate', this._popStateListener!) + // 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 + } + + 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/index.ts b/src/index.ts index 988af460..c6899dd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,7 +49,14 @@ export class Router { // TODO: resolve URL const path = this.resolve(to) // TODO: call hooks, guards - this.history.push(path) + this.history.push( + path + + // TODO: test purposes only + '?value=' + + Math.round(Math.random() * 10) + + '#e' + + Math.round(Math.random() * 10) + ) } getRouteRecord(location: Location) {} diff --git a/src/types/index.ts b/src/types/index.ts index 9e1a4339..1f356bc4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -39,6 +39,21 @@ export type Location = export type HistoryLocation = string +// pushState clones the state passed and do not accept everything +// it doesn't accept symbols, nor functions. It also ignores Symbols as keys +type HistoryStateValue = + | string + | number + | boolean + | HistoryState + | HistoryStateArray +export interface HistoryState { + [x: number]: HistoryStateValue + [x: string]: HistoryStateValue +} +interface HistoryStateArray extends Array {} +// export type HistoryState = Record h() }, } +export enum NavigationType { + back, + forward, +} + export interface NavigationCallback { - (to: HistoryLocation, from: HistoryLocation): void + ( + to: HistoryLocation, + from: HistoryLocation, + info: { type: NavigationType } + ): void } export type RemoveListener = () => void