]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: add previous, next, current, replaced and navigation type
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 29 Mar 2019 18:27:46 +0000 (19:27 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 29 Mar 2019 18:27:46 +0000 (19:27 +0100)
explorations/html5.ts
src/history/base.ts
src/history/html5.ts
src/index.ts
src/types/index.ts

index 32db56ec3d3abd81fbab56c0a12dbca9bb338a31..b0241d2952d74e64c1cea116ef4e7ecbaf31c50a 100644 (file)
@@ -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')
index 28b6d548a27cc82a9e0978e71f2c840f528c242e..35833b0e115fc0bff3d5248560660bc59f1739ad 100644 (file)
@@ -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
 }
index ca666827a1ea76341fadcb8e8deebe6aaeeaa793..2dc939e5c5b13ecb1eee4dc5d5350d79bf054711 100644 (file)
+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
index 988af460a2c344a372cfd27c66c3d13ad956a1b4..c6899dd1faba9083f441b2e98c265f695df71871 100644 (file)
@@ -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) {}
index 9e1a4339648b41bf1801111e28a9e48154af14fe..1f356bc4987152ccdbf30016dcd7d382d8013b4e 100644 (file)
@@ -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<HistoryStateValue> {}
+// export type HistoryState = Record<string | number, string | number | boolean | undefined | null |
+
 export const START: HistoryLocation = '/'
 export const START_RECORD: RouteRecord = {
   path: '/',
@@ -46,8 +61,17 @@ export const START_RECORD: RouteRecord = {
   component: { render: h => 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