Skip to content

Add access to history state #2397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions flow/declarations.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* @flow */
declare var document: Document;

declare class RouteRegExp extends RegExp {
Expand Down Expand Up @@ -83,6 +84,7 @@ declare type Location = {
path?: string;
hash?: string;
query?: Dictionary<string>;
state?: mixed;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
Expand All @@ -95,6 +97,7 @@ declare type Route = {
name: ?string;
hash: string;
query: Dictionary<string>;
state: mixed;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>;
Expand Down
16,847 changes: 16,847 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node examples/server.js",
"dev:dist": "rollup -wm -c build/rollup.dev.config.js",
"build": "node build/build.js",
"lint": "eslint src examples",
"lint": "eslint src examples test flow build",
"test": "npm run lint && npm run flow && npm run test:unit && npm run test:e2e && npm run test:types",
"flow": "flow check",
"test:unit": "jasmine JASMINE_CONFIG_PATH=test/unit/jasmine.json",
Expand Down
9 changes: 8 additions & 1 deletion src/history/abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ export class AbstractHistory extends History {

getCurrentLocation () {
const current = this.stack[this.stack.length - 1]
return current ? current.fullPath : '/'
return current
? {
path: current.path,
query: current.query,
hash: current.hash,
state: current.state
}
: { path: '/' }
}

ensureURL () {
Expand Down
2 changes: 1 addition & 1 deletion src/history/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class History {
+push: (loc: RawLocation) => void;
+replace: (loc: RawLocation) => void;
+ensureURL: (push?: boolean) => void;
+getCurrentLocation: () => string;
+getCurrentLocation: () => Location;

constructor (router: Router, base: ?string) {
this.router = router
Expand Down
15 changes: 10 additions & 5 deletions src/history/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { cleanPath, parsePath } from '../util/path'
import { resolveQuery, stringifyQuery } from '../util/query'
import { getLocation } from './html5'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'
Expand Down Expand Up @@ -74,12 +75,16 @@ export class HashHistory extends History {
}

getCurrentLocation () {
return getHash()
const { path, query, hash } = parsePath(getHash())
return { path, query: resolveQuery(query), hash }
}
}

function checkFallback (base) {
const location = getLocation(base)
const rawLocation = getLocation(base)
const location = typeof rawLocation === 'string'
? rawLocation
: `${rawLocation.path || ''}${stringifyQuery(rawLocation.query || {})}${rawLocation.hash || ''}`
if (!/^\/#/.test(location)) {
window.location.replace(
cleanPath(base + '/#' + location)
Expand Down Expand Up @@ -114,15 +119,15 @@ function getUrl (path) {

function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
pushState(undefined, getUrl(path))
} else {
window.location.hash = path
}
}

function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
replaceState(undefined, getUrl(path))
} else {
window.location.replace(getUrl(path))
}
Expand Down
24 changes: 18 additions & 6 deletions src/history/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { resolveQuery } from '../util/query'
import { START } from '../util/route'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'
Expand Down Expand Up @@ -44,7 +45,8 @@ export class HTML5History extends History {
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
const state = typeof location === 'string' ? undefined : location.state
pushState(state, cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
Expand All @@ -53,7 +55,11 @@ export class HTML5History extends History {
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
const state = typeof location === 'string' ? undefined : location.state
replaceState(
state,
cleanPath(this.base + route.fullPath),
)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
Expand All @@ -62,19 +68,25 @@ export class HTML5History extends History {
ensureURL (push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
if (push) pushState(this.current.state, current)
else replaceState(this.current.state, current)
}
}

getCurrentLocation (): string {
getCurrentLocation (): Location {
return getLocation(this.base)
}
}

export function getLocation (base: string): string {
export function getLocation (base: string): Location {
let path = decodeURI(window.location.pathname)
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
return {
path: (path || '/'),
query: resolveQuery((window.location.search: string)),
hash: (window.location.hash: string),
state: window.history.state ? window.history.state.state : undefined
}
}
59 changes: 59 additions & 0 deletions src/util/deepEqual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const hasOwn = Object.prototype.hasOwnProperty

function is (x, y) {
if (x === y) {
// is(-0, +0) === false
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
/* eslint-disable no-self-compare */
return x !== x && y !== y // is(NaN, NaN) === true
/* eslint-enable no-self-compare */
}
}

// deep equal, with guard against circular references
const guardedDeepEqual = (objA, objB, bufferA, bufferB) => {
if (is(objA, objB)) return true

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const seenA = bufferA.indexOf(objA) !== -1
const seenB = bufferB.indexOf(objB) !== -1

if (seenA || seenB) return seenA && seenB

bufferA.push(objA)
bufferB.push(objB)

const keysA = Object.keys(objA)
const keysB = Object.keys(objB)

if (keysA.length !== keysB.length) return false

for (let i = 0; i < keysA.length; i += 1) {
if (!hasOwn.call(objB, keysA[i])) return false
const valA = objA[keysA[i]]
const valB = objB[keysA[i]]
if (
typeof valA === 'object' &&
valA &&
typeof valB === 'object' &&
valB &&
!guardedDeepEqual(valA, valB, bufferA, bufferB)
) {
return false
} else if (!is(valA, valB)) {
return false
}
}

return true
}

export default (objA, objB) => guardedDeepEqual(objA, objB, [], [])
3 changes: 2 additions & 1 deletion src/util/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function normalizeLocation (
_normalized: true,
path,
query,
hash
hash,
state: next.state
}
}
10 changes: 5 additions & 5 deletions src/util/push-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,23 @@ export function setStateKey (key: string) {
_key = key
}

export function pushState (url?: string, replace?: boolean) {
export function pushState (state: mixed, url: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
history.replaceState({ key: _key, state }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
history.pushState({ key: _key, state }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}

export function replaceState (url?: string) {
pushState(url, true)
export function replaceState (state: mixed, url: string) {
pushState(state, url, true)
}
8 changes: 6 additions & 2 deletions src/util/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type VueRouter from '../index'
import { stringifyQuery } from './query'
import deepEqual from './deepEqual'

const trailingSlashRE = /\/?$/

Expand All @@ -23,6 +24,7 @@ export function createRoute (
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
state: location.state,
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
Expand Down Expand Up @@ -79,14 +81,16 @@ export function isSameRoute (a: Route, b: ?Route): boolean {
return (
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query)
isObjectEqual(a.query, b.query) &&
deepEqual(a.state, b.state)
)
} else if (a.name && b.name) {
return (
a.name === b.name &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query) &&
isObjectEqual(a.params, b.params)
isObjectEqual(a.params, b.params) &&
deepEqual(a.state, b.state)
)
} else {
return false
Expand Down
1 change: 0 additions & 1 deletion test/e2e/runner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
var path = require('path')
var spawn = require('cross-spawn')
var args = process.argv.slice(2)

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/specs/active-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
module.exports = {
'active links': function (browser) {
browser
.url('http://localhost:8080/active-links/')
.url('http://localhost:8080/active-links/')
.waitForElementVisible('#app', 1000)
.assert.count('li a', 11)
// assert correct href with base
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/specs/auth-flow.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
'auth flow': function (browser) {
browser
.url('http://localhost:8080/auth-flow/')
.url('http://localhost:8080/auth-flow/')
.waitForElementVisible('#app', 1000)
.assert.containsText('#app p', 'You are logged out')

Expand All @@ -21,7 +21,7 @@ module.exports = {
.assert.containsText('#app p', 'Yay you made it!')

// reload
.url('http://localhost:8080/auth-flow/')
.url('http://localhost:8080/auth-flow/')
.waitForElementVisible('#app', 1000)
.assert.containsText('#app p', 'You are logged in')

Expand All @@ -32,7 +32,7 @@ module.exports = {
.assert.containsText('#app p', 'Yay you made it!')

// directly visit dashboard when logged in
.url('http://localhost:8080/auth-flow/dashboard')
.url('http://localhost:8080/auth-flow/dashboard')
.waitForElementVisible('#app', 1000)
.assert.urlEquals('http://localhost:8080/auth-flow/dashboard')
.assert.containsText('#app h2', 'Dashboard')
Expand All @@ -44,7 +44,7 @@ module.exports = {
.assert.containsText('#app p', 'You are logged out')

// directly visit dashboard when logged out
.url('http://localhost:8080/auth-flow/dashboard')
.url('http://localhost:8080/auth-flow/dashboard')
.waitForElementVisible('#app', 1000)
.assert.urlEquals('http://localhost:8080/auth-flow/login?redirect=%2Fdashboard')
.assert.containsText('#app h2', 'Login')
Expand Down
6 changes: 3 additions & 3 deletions test/e2e/specs/basic.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
basic: function (browser) {
browser
.url('http://localhost:8080/basic/')
.url('http://localhost:8080/basic/')
.waitForElementVisible('#app', 1000)
.assert.count('li', 5)
.assert.count('li a', 5)
Expand Down Expand Up @@ -34,10 +34,10 @@ module.exports = {
.assert.containsText('.view', 'unicode')

// check initial visit
.url('http://localhost:8080/basic/foo')
.url('http://localhost:8080/basic/foo')
.waitForElementVisible('#app', 1000)
.assert.containsText('.view', 'foo')
.url('http://localhost:8080/basic/%C3%A9')
.url('http://localhost:8080/basic/%C3%A9')
.waitForElementVisible('#app', 1000)
.assert.containsText('.view', 'unicode')
.end()
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/specs/data-fetching.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
'data fetching': function (browser) {
browser
.url('http://localhost:8080/data-fetching/')
.url('http://localhost:8080/data-fetching/')
.waitForElementVisible('#app', 1000)
.assert.count('li a', 4)
.assert.containsText('.view', 'home')
Expand Down
6 changes: 3 additions & 3 deletions test/e2e/specs/hash-mode.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
'Hash mode': function (browser) {
browser
.url('http://localhost:8080/hash-mode/')
.url('http://localhost:8080/hash-mode/')
.waitForElementVisible('#app', 1000)
.assert.count('li', 5)
.assert.count('li a', 4)
Expand All @@ -28,10 +28,10 @@ module.exports = {
.assert.containsText('.view', 'bar')

// check initial visit
.url('http://localhost:8080/hash-mode/#/foo')
.url('http://localhost:8080/hash-mode/#/foo')
.waitForElementVisible('#app', 1000)
.assert.containsText('.view', 'foo')
.url('http://localhost:8080/hash-mode/#/%C3%A9')
.url('http://localhost:8080/hash-mode/#/%C3%A9')
.waitForElementVisible('#app', 1000)
.assert.containsText('.view', 'unicode')
.end()
Expand Down
Loading