RawHistoryLocation,
} from '../../src/history/common'
-const loc: RawHistoryLocation = {
- path: '/foo',
-}
-const loc2: RawHistoryLocation = {
- path: '/bar',
-}
+const loc: RawHistoryLocation = '/foo'
+
+const loc2: RawHistoryLocation = '/bar'
const normaliezedLoc: HistoryLocationNormalized = {
- path: '/foo',
- query: {},
- hash: '',
fullPath: '/foo',
}
const normaliezedLoc2: HistoryLocationNormalized = {
- path: '/bar',
- query: {},
- hash: '',
fullPath: '/bar',
}
it('can push a location', () => {
const history = createMemoryHistory()
- // partial version
- history.push({ path: '/somewhere', hash: '#hey', query: { foo: 'foo' } })
+ history.push('/somewhere?foo=foo#hey')
expect(history.location).toEqual({
fullPath: '/somewhere?foo=foo#hey',
- path: '/somewhere',
- query: { foo: 'foo' },
- hash: '#hey',
- })
-
- // partial version
- history.push({ path: '/path', hash: '#ho' })
- expect(history.location).toEqual({
- fullPath: '/path#ho',
- path: '/path',
- query: {},
- hash: '#ho',
})
})
it('can replace a location', () => {
const history = createMemoryHistory()
// partial version
- history.replace({ path: '/somewhere', hash: '#hey', query: { foo: 'foo' } })
+ history.replace('/somewhere?foo=foo#hey')
expect(history.location).toEqual({
fullPath: '/somewhere?foo=foo#hey',
- path: '/somewhere',
- query: { foo: 'foo' },
- hash: '#hey',
})
})
const history = createMemoryHistory()
history.replace('/search?q=dog#footer')
const { router } = await newRouter({ history })
- await router.push(history.location)
+ await router.push(history.location.fullPath)
expect(router.currentRoute).toEqual({
fullPath: '/search?q=dog#footer',
hash: '#footer',
})
describe('matcher', () => {
- it('handles one redirect from route record', async () => {
+ // TODO: rewrite after redirect refactor
+ it.skip('handles one redirect from route record', async () => {
const history = createMemoryHistory()
const router = createRouter({ history, routes })
const loc = await router.push('/to-foo')
})
})
- it('drops query and params on redirect if not provided', async () => {
+ // TODO: rewrite after redirect refactor
+ it.skip('drops query and params on redirect if not provided', async () => {
const history = createMemoryHistory()
const router = createRouter({ history, routes })
const loc = await router.push('/to-foo?hey=foo#fa')
})
})
- it('allows object in redirect', async () => {
+ // TODO: rewrite after redirect refactor
+ it.skip('allows object in redirect', async () => {
const history = createMemoryHistory()
const router = createRouter({ history, routes })
const loc = await router.push('/to-foo-named')
})
})
- it('can pass on query and hash when redirecting', async () => {
+ // TODO: rewrite after redirect refactor
+ it.skip('can pass on query and hash when redirecting', async () => {
const history = createMemoryHistory()
const router = createRouter({ history, routes })
const loc = await router.push('/inc-query-hash?n=3#fa')
path: '/inc-query-hash',
})
})
-
- it('handles multiple redirect fields in route record', async () => {
- const history = createMemoryHistory()
- const router = createRouter({ history, routes })
- const loc = await router.push('/to-foo2')
- expect(loc.name).toBe('Foo')
- expect(loc.redirectedFrom).toMatchObject({
- path: '/to-foo',
- redirectedFrom: {
- path: '/to-foo2',
- },
- })
- })
})
it('allows base option in abstract history', async () => {
import {
- parseURL,
- stringifyURL,
- normalizeLocation,
+ parseURL as originalParseURL,
+ stringifyURL as originalStringifyURL,
+ parseQuery,
+ stringifyQuery,
+ normalizeHistoryLocation as normalizeLocation,
} from '../src/history/common'
describe('parseURL', () => {
+ let parseURL = originalParseURL.bind(null, parseQuery)
+
it('works with no query no hash', () => {
expect(parseURL('/foo')).toEqual({
fullPath: '/foo',
})
describe('stringifyURL', () => {
+ let stringifyURL = originalStringifyURL.bind(null, stringifyQuery)
+
it('stringifies a path', () => {
expect(
stringifyURL({
describe('normalizeLocation', () => {
it('works with string', () => {
- expect(normalizeLocation('/foo')).toEqual(parseURL('/foo'))
+ expect(normalizeLocation('/foo')).toEqual({ fullPath: '/foo' })
})
it('works with objects', () => {
expect(
normalizeLocation({
- path: '/foo',
- })
- ).toEqual({ path: '/foo', fullPath: '/foo', query: {}, hash: '' })
- })
-
- it('works with objects and keeps query and hash', () => {
- expect(
- normalizeLocation({
- path: '/foo',
- query: { foo: 'a' },
- hash: '#hey',
+ fullPath: '/foo',
})
- ).toEqual({
- path: '/foo',
- fullPath: '/foo?foo=a#hey',
- query: { foo: 'a' },
- hash: '#hey',
- })
+ ).toEqual({ fullPath: '/foo' })
})
})
import { ListenerRemover } from '../types'
// import { encodeQueryProperty, encodeHash } from '../utils/encoding'
-// TODO: allow numbers
export type HistoryQuery = Record<string, string | string[]>
interface HistoryLocation {
- // pathname section
+ fullPath: string
+ state?: HistoryState
+}
+
+export type RawHistoryLocation = HistoryLocation | string
+export type HistoryLocationNormalized = Pick<HistoryLocation, 'fullPath'>
+export interface LocationPartial {
path: string
- // search string parsed
query?: HistoryQuery
- // hash with the #
hash?: string
}
-
-export type RawHistoryLocation = HistoryLocation | string
-
-export interface HistoryLocationNormalized extends Required<HistoryLocation> {
- // full path (like href)
+export interface LocationNormalized {
+ path: string
fullPath: string
+ hash: string
query: HistoryQuery
}
const START_PATH = ''
export const START: HistoryLocationNormalized = {
fullPath: START_PATH,
- path: START_PATH,
- query: {},
- hash: '',
}
export type ValueContainer<T> = { value: T }
readonly location: HistoryLocationNormalized
// readonly location: ValueContainer<HistoryLocationNormalized>
- push(to: RawHistoryLocation, data?: any): void
+ push(to: RawHistoryLocation): void
replace(to: RawHistoryLocation): void
back(triggerListeners?: boolean): void
/**
* Transforms an URI into a normalized history location
+ * @param parseQuery
* @param location URI to normalize
* @returns a normalized history location
*/
-export function parseURL(location: string): HistoryLocationNormalized {
+export function parseURL(
+ parseQuery: (search: string) => HistoryQuery,
+ location: string
+): LocationNormalized {
let path = '',
query: HistoryQuery = {},
searchString = '',
hashPos > -1 ? hashPos : location.length
)
- // TODO: can we remove the normalize call?
query = parseQuery(searchString)
}
/**
* Stringify a URL object
+ * @param stringifyQuery
* @param location
*/
-export function stringifyURL(location: HistoryLocation): string {
- let url = location.path
- let query = location.query ? stringifyQuery(location.query) : ''
-
- return url + (query && '?' + query) + (location.hash || '')
+export function stringifyURL(
+ stringifyQuery: (query: HistoryQuery) => string,
+ location: LocationPartial
+): string {
+ let query: string = location.query ? stringifyQuery(location.query) : ''
+ return location.path + (query && '?') + query + (location.hash || '')
}
/**
* @returns a query object
*/
export function parseQuery(search: string): HistoryQuery {
- const hasLeadingIM = search[0] === '?'
const query: HistoryQuery = {}
// avoid creating an object with an empty key and empty value
// because of split('&')
if (search === '' || search === '?') return query
+ const hasLeadingIM = search[0] === '?'
const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&')
for (let i = 0; i < searchParams.length; ++i) {
let [key, value] = searchParams[i].split('=')
return search
}
-/**
- * Normalize a History location object or string into a HistoryLocationNoramlized
- * @param location
- */
-export function normalizeLocation(
- location: RawHistoryLocation
-): HistoryLocationNormalized {
- if (typeof location === 'string') return parseURL(location)
- else
- return {
- fullPath: stringifyURL(location),
- path: location.path,
- query: location.query || {},
- hash: location.hash || '',
- }
-}
-
/**
* Strips off the base from the beginning of a location.pathname
* @param pathname location.pathname
pathname
)
}
+
+export function normalizeHistoryLocation(
+ location: RawHistoryLocation
+): HistoryLocationNormalized {
+ return {
+ // to avoid doing a typeof or in that is quite long
+ fullPath: (location as HistoryLocation).fullPath || (location as string),
+ }
+}
import {
RouterHistory,
NavigationCallback,
- normalizeLocation,
stripBase,
NavigationType,
NavigationDirection,
HistoryLocationNormalized,
+ normalizeHistoryLocation,
HistoryState,
RawHistoryLocation,
ValueContainer,
type PopStateListener = (this: Window, ev: PopStateEvent) => any
-interface StateEntry {
+interface StateEntry extends HistoryState {
back: HistoryLocationNormalized | null
current: HistoryLocationNormalized
forward: HistoryLocationNormalized | null
// allows hash based url
if (base.indexOf('#') > -1) {
// prepend the starting slash to hash so the url starts with /#
- return normalizeLocation(stripBase('/' + hash, base))
+ return normalizeHistoryLocation(stripBase('/' + hash, base))
}
const path = stripBase(pathname, base)
- return normalizeLocation(path + search + hash)
+ return normalizeHistoryLocation(path + search + hash)
}
function useHistoryListeners(
// build current history entry as this is a fresh navigation
if (!historyState.value) {
changeLocation(
+ location.value,
{
back: null,
current: location.value,
replaced: true,
scroll: computeScrollPosition(),
},
- '',
- location.value.fullPath,
true
)
}
function changeLocation(
+ to: HistoryLocationNormalized,
state: StateEntry,
- title: string,
- fullPath: string,
replace: boolean
): void {
- const url = base + fullPath
+ const url = base + to.fullPath
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
const newState: StateEntry = replace
? { ...historyState.value, ...state }
: state
- history[replace ? 'replaceState' : 'pushState'](newState, title, url)
+ history[replace ? 'replaceState' : 'pushState'](newState, '', url)
historyState.value = state
} catch (err) {
cs.log('[vue-router]: Error with push/replace State', err)
}
}
+ // TODO: allow data as well
function replace(to: RawHistoryLocation) {
- const normalized = normalizeLocation(to)
-
+ const normalized = normalizeHistoryLocation(to)
// cs.info('replace', location, normalized)
const state: StateEntry = buildState(
true
)
if (historyState) state.position = historyState.value.position
- changeLocation(
- // TODO: refactor state building
- state,
- '',
- normalized.fullPath,
- true
- )
+
+ changeLocation(normalized, state, true)
location.value = normalized
}
function push(to: RawHistoryLocation, data?: HistoryState) {
- const normalized = normalizeLocation(to)
+ const normalized = normalizeHistoryLocation(to)
// Add to current entry the information of where we are going
// as well as saving the current position
forward: normalized,
scroll: computeScrollPosition(),
}
- changeLocation(currentState, '', currentState.current.fullPath, true)
+ changeLocation(normalized, currentState, true)
const state: StateEntry = {
...buildState(location.value, normalized, null),
...data,
}
- changeLocation(state, '', normalized.fullPath, false)
+ changeLocation(normalized, state, false)
location.value = normalized
}
RouterHistory,
NavigationCallback,
START,
- normalizeLocation,
+ normalizeHistoryLocation,
HistoryLocationNormalized,
HistoryState,
NavigationType,
base,
replace(to) {
- const toNormalized = normalizeLocation(to)
+ const toNormalized = normalizeHistoryLocation(to)
// remove current entry and decrement position
queue.splice(position--, 1)
setLocation(toNormalized)
},
push(to, data?: HistoryState) {
- setLocation(normalizeLocation(to))
+ setLocation(normalizeHistoryLocation(to))
},
listen(callback) {
ListenerRemover,
PostNavigationGuard,
START_LOCATION_NORMALIZED,
- MatcherLocation,
- RouteQueryAndHash,
Lazy,
TODO,
Immutable,
+ RouteParams,
+ MatcherLocationNormalized,
} from './types'
-import { RouterHistory, normalizeLocation } from './history/common'
+import {
+ RouterHistory,
+ parseQuery,
+ parseURL,
+ stringifyQuery,
+ stringifyURL,
+} from './history/common'
import {
ScrollToPosition,
ScrollPosition,
window.history.scrollRestoration = 'manual'
}
+ function createHref(to: RouteLocationNormalized): string {
+ return history.base + to.fullPath
+ }
+
+ function encodeParams(params: RouteParams): RouteParams {
+ // TODO:
+ return params
+ }
+
+ function decodeParams(params: RouteParams): RouteParams {
+ // TODO:
+ return params
+ }
+
function resolve(
to: RouteLocation,
- currentLocation?: RouteLocationNormalized /*, append?: boolean */
+ from?: RouteLocationNormalized
): RouteLocationNormalized {
- if (typeof to === 'string')
- return resolveLocation(
- // TODO: refactor and remove import
- normalizeLocation(to),
- currentLocation
- )
- return resolveLocation(
- {
- // TODO: refactor with url utils
- // TODO: query must be encoded by normalizeLocation
- // refactor the whole thing so it uses the same overridable functions
- // sent to history
- query: {},
- hash: '',
- ...to,
- },
- currentLocation
- )
- }
-
- function createHref(to: RouteLocationNormalized): string {
- return history.base + to.fullPath
+ return resolveLocation(to, from || currentRoute.value)
}
function resolveLocation(
- location: MatcherLocation & Required<RouteQueryAndHash>,
- currentLocation?: RouteLocationNormalized,
+ location: RouteLocation,
+ currentLocation: RouteLocationNormalized,
+ // TODO: we should benefit from this with navigation guards
+ // https://github.com/vuejs/vue-router/issues/1822
redirectedFrom?: RouteLocationNormalized
- // ensure when returning that the redirectedFrom is a normalized location
): RouteLocationNormalized {
- currentLocation = currentLocation || currentRoute.value
- // TODO: still return a normalized location with no matched records if no location is found
- const matchedRoute = matcher.resolve(location, currentLocation)
-
- if ('redirect' in matchedRoute) {
- const { redirect } = matchedRoute
- // target location normalized, used if we want to redirect again
- const normalizedLocation: RouteLocationNormalized = {
- ...matchedRoute.normalizedLocation,
- ...normalizeLocation({
- path: matchedRoute.normalizedLocation.path,
- query: location.query,
- hash: location.hash,
- }),
- redirectedFrom,
- meta: {},
- }
-
- if (typeof redirect === 'string') {
- // match the redirect instead
- return resolveLocation(
- normalizeLocation(redirect),
- currentLocation,
- normalizedLocation
- )
- } else if (typeof redirect === 'function') {
- const newLocation = redirect(normalizedLocation)
-
- if (typeof newLocation === 'string') {
- return resolveLocation(
- normalizeLocation(newLocation),
- currentLocation,
- normalizedLocation
- )
- }
+ // const objectLocation = routerLocationAsObject(location)
+ if (typeof location === 'string') {
+ // TODO: remove as cast when redirect is removed from matcher
+ // TODO: ensure parseURL encodes the query in fullPath but not in query object
+ let locationNormalized = parseURL(parseQuery, location)
+ let matchedRoute = matcher.resolve(
+ { path: locationNormalized.path },
+ currentLocation
+ ) as MatcherLocationNormalized
- // TODO: should we allow partial redirects? I think we should not because it's impredictable if
- // there was a redirect before
- // if (!('path' in newLocation) && !('name' in newLocation)) throw new Error('TODO: redirect canot be relative')
-
- return resolveLocation(
- {
- ...newLocation,
- query: newLocation.query || {},
- hash: newLocation.hash || '',
- },
- currentLocation,
- normalizedLocation
- )
- } else {
- return resolveLocation(
- {
- ...redirect,
- query: redirect.query || {},
- hash: redirect.hash || '',
- },
- currentLocation,
- normalizedLocation
- )
- }
- } else {
- // add the redirectedFrom field
- const url = normalizeLocation({
- path: matchedRoute.path,
- query: location.query,
- hash: location.hash,
- })
return {
+ ...locationNormalized,
...matchedRoute,
- ...url,
redirectedFrom,
}
}
+
+ const hasParams = 'params' in location
+
+ // relative or named location, path is ignored
+ // for same reason TS thinks location.params can be undefined
+ // TODO: remove cast like above
+ let matchedRoute = matcher.resolve(
+ hasParams
+ ? // we know we have the params attribute
+ { ...location, params: encodeParams((location as any).params) }
+ : location,
+ currentLocation
+ ) as MatcherLocationNormalized
+
+ // put back the unencoded params as given by the user (avoid the cost of decoding them)
+ matchedRoute.params = hasParams
+ ? // we know we have the params attribute
+ (location as any).params!
+ : decodeParams(matchedRoute.params)
+
+ return {
+ fullPath: stringifyURL(stringifyQuery, {
+ ...location,
+ path: matchedRoute.path,
+ }),
+ hash: location.hash || '',
+ query: location.query || {},
+ ...matchedRoute,
+ redirectedFrom,
+ }
}
+ // function oldresolveLocation(
+ // location: MatcherLocation & Required<RouteQueryAndHash>,
+ // currentLocation?: RouteLocationNormalized,
+ // redirectedFrom?: RouteLocationNormalized
+ // // ensure when returning that the redirectedFrom is a normalized location
+ // ): RouteLocationNormalized {
+ // currentLocation = currentLocation || currentRoute.value
+ // // TODO: still return a normalized location with no matched records if no location is found
+ // const matchedRoute = matcher.resolve(location, currentLocation)
+
+ // if ('redirect' in matchedRoute) {
+ // const { redirect } = matchedRoute
+ // // target location normalized, used if we want to redirect again
+ // const normalizedLocation: RouteLocationNormalized = {
+ // ...matchedRoute.normalizedLocation,
+ // ...normalizeLocation({
+ // path: matchedRoute.normalizedLocation.path,
+ // query: location.query,
+ // hash: location.hash,
+ // }),
+ // redirectedFrom,
+ // meta: {},
+ // }
+
+ // if (typeof redirect === 'string') {
+ // // match the redirect instead
+ // return resolveLocation(
+ // normalizeLocation(redirect),
+ // currentLocation,
+ // normalizedLocation
+ // )
+ // } else if (typeof redirect === 'function') {
+ // const newLocation = redirect(normalizedLocation)
+
+ // if (typeof newLocation === 'string') {
+ // return resolveLocation(
+ // normalizeLocation(newLocation),
+ // currentLocation,
+ // normalizedLocation
+ // )
+ // }
+
+ // // TODO: should we allow partial redirects? I think we should not because it's impredictable if
+ // // there was a redirect before
+ // // if (!('path' in newLocation) && !('name' in newLocation)) throw new Error('TODO: redirect canot be relative')
+
+ // return resolveLocation(
+ // {
+ // ...newLocation,
+ // query: newLocation.query || {},
+ // hash: newLocation.hash || '',
+ // },
+ // currentLocation,
+ // normalizedLocation
+ // )
+ // } else {
+ // return resolveLocation(
+ // {
+ // ...redirect,
+ // query: redirect.query || {},
+ // hash: redirect.hash || '',
+ // },
+ // currentLocation,
+ // normalizedLocation
+ // )
+ // }
+ // } else {
+ // // add the redirectedFrom field
+ // const url = normalizeLocation({
+ // path: matchedRoute.path,
+ // query: location.query,
+ // hash: location.hash,
+ // })
+ // return {
+ // ...matchedRoute,
+ // ...url,
+ // redirectedFrom,
+ // }
+ // }
+ // }
+
async function push(to: RouteLocation): Promise<RouteLocationNormalized> {
const toLocation: RouteLocationNormalized = (pendingLocation = resolve(to))
// attach listener to history to trigger navigations
history.listen(async (to, from, info) => {
- const matchedRoute = resolveLocation(to, currentRoute.value)
+ const toLocation = resolve(to.fullPath)
// console.log({ to, matchedRoute })
- const toLocation: RouteLocationNormalized = { ...to, ...matchedRoute }
pendingLocation = toLocation
try {
if (!started) {
// TODO: this initial navigation is only necessary on client, on server it doesn't make sense
// because it will create an extra unecessary navigation and could lead to problems
- router.push(router.history.location).catch(err => {
+ router.push(router.history.location.fullPath).catch(err => {
console.error('Unhandled error', err)
})
started = true
// record?
params: RouteLocationNormalized['params']
matched: RouteRecordMatched[]
+ // TODO: remove optional and allow null as value (monomorphic)
redirectedFrom?: MatcherLocationNormalized
meta: RouteLocationNormalized['meta']
}
// import { RouteLocationNormalized } from '../types'
-export interface ScrollToPosition {
+export type ScrollToPosition = {
x: number
y: number
}