import { createMemoryHistory } from '../src'
import { mount, tick } from './mount'
import { ref, markNonReactive } from 'vue'
+import { RouteRecordNormalized } from '../src/matcher/types'
+
+const records = {
+ home: {} as RouteRecordNormalized,
+ foo: {} as RouteRecordNormalized,
+}
const locations: Record<
string,
meta: {},
query: {},
hash: '',
- matched: [],
+ matched: [records.home],
redirectedFrom: undefined,
name: undefined,
},
meta: {},
query: {},
hash: '',
- matched: [],
+ matched: [records.foo],
redirectedFrom: undefined,
name: undefined,
},
meta: {},
query: { foo: 'a', bar: 'b' },
hash: '',
- matched: [],
+ matched: [records.home],
redirectedFrom: undefined,
name: undefined,
},
{ to: locations.basic.string },
locations.basic.normalized
)
- expect(el.innerHTML).toBe('<a class="" href="/home">a link</a>')
+ expect(el.querySelector('a')!.getAttribute('href')).toBe('/home')
})
// TODO: not sure why this breaks. We could take a look at @vue/test-runtime
{ to: { path: locations.basic.string } },
locations.basic.normalized
)
- expect(el.innerHTML).toBe('<a class="" href="/home">a link</a>')
+ expect(el.querySelector('a')!.getAttribute('href')).toBe('/home')
})
it('can be active', () => {
{ to: locations.basic.string },
locations.basic.normalized
)
- expect(el.innerHTML).toBe(
- '<a class="router-link-active" href="/home">a link</a>'
+ expect(el.querySelector('a')!.className).toContain('router-link-active')
+ })
+
+ it('can be exact-active', () => {
+ const { el } = factory(
+ locations.basic.normalized,
+ { to: locations.basic.string },
+ locations.basic.normalized
+ )
+ expect(el.querySelector('a')!.className).toContain(
+ 'router-link-exact-active'
)
})
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RouterLink v-slot provides information on v-slot 1`] = `"<a class=\\"router-link-active\\" href=\\"/home\\"> route: {\\"fullPath\\":\\"/home\\",\\"path\\":\\"/home\\",\\"params\\":{},\\"meta\\":{},\\"query\\":{},\\"hash\\":\\"\\",\\"matched\\":[]} href: \\"/home\\" isActive: \\"true\\" </a>"`;
+exports[`RouterLink v-slot provides information on v-slot 1`] = `"<a class=\\"router-link-active router-link-exact-active\\" href=\\"/home\\"> route: {\\"fullPath\\":\\"/home\\",\\"path\\":\\"/home\\",\\"params\\":{},\\"meta\\":{},\\"query\\":{},\\"hash\\":\\"\\",\\"matched\\":[{}]} href: \\"/home\\" isActive: \\"true\\" </a>"`;
<a href="/documents/€">/documents/€ (force reload): not valid tho</a>
</li>
<li>
- <router-link to="/">Home</router-link>
+ <router-link to="/">Home (redirects)</router-link>
+ </li>
+ <li>
+ <router-link to="/home">Home</router-link>
+ </li>
+ <li>
+ <router-link to="/nested">/nested</router-link>
+ </li>
+ <li>
+ <router-link to="/nested/nested">/nested/nested</router-link>
</li>
<li>
<router-link to="/nested/nested/nested"
<router-link
:to="{
name: 'user',
- params: { id: Number(currentLocation.params.id || 0) + 1 },
+ params: { id: '' + (Number(currentLocation.params.id || 0) + 1) },
}"
>/users/{{ Number(currentLocation.params.id || 0) + 1 }}</router-link
>
children: [
{
path: 'nested',
+ name: 'NestedNested',
component: Nested,
- children: [{ path: 'nested', component: Nested }],
+ children: [
+ {
+ name: 'NestedNestedNested',
+ path: 'nested',
+ component: Nested,
+ },
+ ],
},
],
},
inject,
computed,
reactive,
- isRef,
Ref,
+ unref,
} from 'vue'
-import { RouteLocation } from '../types'
+import { RouteLocation, RouteLocationNormalized, Immutable } from '../types'
+import { isSameLocationObject } from '../utils'
import { routerKey } from '../injectKeys'
+import { RouteRecordNormalized } from '../matcher/types'
-interface UseLinkProps {
- to: Ref<RouteLocation> | RouteLocation
- replace?: Ref<boolean> | boolean
+type VueUseOptions<T> = {
+ [k in keyof T]: Ref<T[k]> | T[k]
+}
+
+interface LinkProps {
+ to: RouteLocation
+ // TODO: refactor using extra options allowed in router.push
+ replace?: boolean
+}
+
+type UseLinkOptions = VueUseOptions<LinkProps>
+
+function isSameRouteRecord(
+ a: Immutable<RouteRecordNormalized>,
+ b: Immutable<RouteRecordNormalized>
+): boolean {
+ // TODO: handle aliases
+ return a === b
+}
+
+function includesParams(
+ outter: Immutable<RouteLocationNormalized['params']>,
+ inner: Immutable<RouteLocationNormalized['params']>
+): boolean {
+ for (let key in inner) {
+ let innerValue = inner[key]
+ let outterValue = outter[key]
+ if (typeof innerValue === 'string') {
+ if (innerValue !== outterValue) return false
+ } else {
+ if (
+ !Array.isArray(outterValue) ||
+ innerValue.some((value, i) => value !== outterValue[i])
+ )
+ return false
+ }
+ }
+
+ return true
}
// TODO: what should be accepted as arguments?
-export function useLink(props: UseLinkProps) {
+export function useLink(props: UseLinkOptions) {
const router = inject(routerKey)!
- const route = computed(() =>
- router.resolve(isRef(props.to) ? props.to.value : props.to)
- )
+ const route = computed(() => router.resolve(unref(props.to)))
const href = computed(() => router.createHref(route.value))
+
+ const activeRecordIndex = computed<number>(() => {
+ const currentMatched = route.value.matched[route.value.matched.length - 1]
+ return router.currentRoute.value.matched.findIndex(
+ isSameRouteRecord.bind(null, currentMatched)
+ )
+ })
+
const isActive = computed<boolean>(
- () => router.currentRoute.value.path.indexOf(route.value.path) === 0
+ () =>
+ activeRecordIndex.value > -1 &&
+ includesParams(router.currentRoute.value.params, route.value.params)
+ )
+ const isExactActive = computed<boolean>(
+ () =>
+ activeRecordIndex.value ===
+ router.currentRoute.value.matched.length - 1 &&
+ isSameLocationObject(router.currentRoute.value.params, route.value.params)
)
// TODO: handle replace prop
+ // const method = unref(rep)
function navigate(e: MouseEvent = {} as MouseEvent) {
// TODO: handle navigate with empty parameters for scoped slot and composition api
route,
href,
isActive,
+ isExactActive,
navigate,
}
}
},
setup(props, { slots, attrs }) {
- const { route, isActive, href, navigate } = useLink(props)
+ const { route, isActive, isExactActive, href, navigate } = useLink(props)
const elClass = computed(() => ({
'router-link-active': isActive.value,
+ 'router-link-exact-active': isExactActive.value,
}))
// TODO: exact active classes
Lazy,
TODO,
Immutable,
+ MatcherLocationNormalized,
} from './types'
import { RouterHistory, parseURL, stringifyURL } from './history/common'
import {
import {
extractComponentsGuards,
guardToPromiseFn,
+ isSameLocationObject,
applyToParams,
} from './utils'
import { useCallbacks } from './utils/callbacks'
import { encodeParam, decode } from './utils/encoding'
-import {
- normalizeQuery,
- parseQuery,
- stringifyQuery,
- LocationQueryValue,
-} from './utils/query'
+import { normalizeQuery, parseQuery, stringifyQuery } from './utils/query'
import { ref, Ref, markNonReactive, nextTick, App, warn } from 'vue'
import { RouteRecordNormalized } from './matcher/types'
import { Link } from './components/Link'
}
}
- const hasParams = 'params' in location
-
- // relative or named location, path is ignored
- // for same reason TS thinks location.params can be undefined
- let matchedRoute = matcher.resolve(
- hasParams
- ? // we know we have the params attribute
- { ...location, params: encodeParams((location as any).params) }
- : location,
- currentLocation
- )
+ let matchedRoute: MatcherLocationNormalized = // relative or named location, path is ignored
+ // for same reason TS thinks location.params can be undefined
+ matcher.resolve(
+ 'params' in location
+ ? { ...location, params: encodeParams(location.params) }
+ : location,
+ currentLocation
+ )
// 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)
+ // TODO: normalize params if we accept numbers as raw values
+ matchedRoute.params =
+ 'params' in location
+ ? location.params!
+ : decodeParams(matchedRoute.params)
return {
fullPath: stringifyURL(stringifyQuery, {
}
function isSameLocation(
- a: RouteLocationNormalized,
- b: RouteLocationNormalized
+ a: Immutable<RouteLocationNormalized>,
+ b: Immutable<RouteLocationNormalized>
): boolean {
return (
a.name === b.name &&
a.path === b.path &&
a.hash === b.hash &&
- isSameLocationQuery(a.query, b.query)
+ isSameLocationObject(a.query, b.query)
)
}
-
-function isSameLocationQuery(
- a: RouteLocationNormalized['query'],
- b: RouteLocationNormalized['query']
-): boolean {
- const aKeys = Object.keys(a)
- const bKeys = Object.keys(b)
- if (aKeys.length !== bKeys.length) return false
- let i = 0
- let key: string
- while (i < aKeys.length) {
- key = aKeys[i]
- if (key !== bKeys[i]) return false
- if (!isSameLocationQueryValue(a[key], b[key])) return false
- i++
- }
-
- return true
-}
-
-function isSameLocationQueryValue(
- a: LocationQueryValue | LocationQueryValue[],
- b: LocationQueryValue | LocationQueryValue[]
-): boolean {
- if (typeof a !== typeof b) return false
- if (Array.isArray(a))
- return a.every((value, i) => value === (b as LocationQueryValue[])[i])
- return a === b
-}
export type ListenerRemover = () => void
-type RouteParamValue = string
+export type RouteParamValue = string
// TODO: should we allow more values like numbers and normalize them to strings?
// type RouteParamValueRaw = RouteParamValue | number
export type RouteParams = Record<string, RouteParamValue | RouteParamValue[]>
-import { RouteLocationNormalized, RouteParams } from '../types'
+import { RouteLocationNormalized, RouteParams, Immutable } from '../types'
import { guardToPromiseFn } from './guardToPromiseFn'
import { RouteRecordNormalized } from '../matcher/types'
+import { LocationQueryValue } from './query'
export * from './guardToPromiseFn'
export function applyToParams(
fn: (v: string) => string,
- params: RouteParams
+ params: RouteParams | undefined
): RouteParams {
const newParams: RouteParams = {}
return newParams
}
+
+export function isSameLocationObject(
+ a: Immutable<RouteLocationNormalized['query']>,
+ b: Immutable<RouteLocationNormalized['query']>
+): boolean
+export function isSameLocationObject(
+ a: Immutable<RouteLocationNormalized['params']>,
+ b: Immutable<RouteLocationNormalized['params']>
+): boolean
+export function isSameLocationObject(
+ a: Immutable<RouteLocationNormalized['query' | 'params']>,
+ b: Immutable<RouteLocationNormalized['query' | 'params']>
+): boolean {
+ const aKeys = Object.keys(a)
+ const bKeys = Object.keys(b)
+ if (aKeys.length !== bKeys.length) return false
+ let i = 0
+ let key: string
+ while (i < aKeys.length) {
+ key = aKeys[i]
+ if (key !== bKeys[i]) return false
+ if (!isSameLocationObjectValue(a[key], b[key])) return false
+ i++
+ }
+
+ return true
+}
+
+function isSameLocationObjectValue(
+ a: Immutable<LocationQueryValue | LocationQueryValue[]>,
+ b: Immutable<LocationQueryValue | LocationQueryValue[]>
+): boolean
+function isSameLocationObjectValue(
+ a: Immutable<RouteParams | RouteParams[]>,
+ b: Immutable<RouteParams | RouteParams[]>
+): boolean
+function isSameLocationObjectValue(
+ a: Immutable<
+ LocationQueryValue | LocationQueryValue[] | RouteParams | RouteParams[]
+ >,
+ b: Immutable<
+ LocationQueryValue | LocationQueryValue[] | RouteParams | RouteParams[]
+ >
+): boolean {
+ if (typeof a !== typeof b) return false
+ // both a and b are arrays
+ if (Array.isArray(a))
+ return a.every((value, i) => value === (b as LocationQueryValue[])[i])
+ return a === b
+}