})
// it('redirects with route record redirect')
+
+ describe('Dynamic Routing', () => {
+ it('resolves new added routes', async () => {
+ const { router } = await newRouter()
+ expect(router.resolve('/new-route')).toMatchObject({
+ name: undefined,
+ matched: [],
+ })
+ router.addRoute({
+ path: '/new-route',
+ component: components.Foo,
+ name: 'new route',
+ })
+ expect(router.resolve('/new-route')).toMatchObject({
+ name: 'new route',
+ })
+ })
+
+ it('can redirect to children in the middle of navigation', async () => {
+ const { router } = await newRouter()
+ expect(router.resolve('/new-route')).toMatchObject({
+ name: undefined,
+ matched: [],
+ })
+ let removeRoute: (() => void) | undefined
+ router.addRoute({
+ path: '/dynamic',
+ component: components.Nested,
+ name: 'dynamic parent',
+ options: { end: false, strict: true },
+ beforeEnter(to, from, next) {
+ if (!removeRoute) {
+ removeRoute = router.addRoute('dynamic parent', {
+ path: 'child',
+ name: 'dynamic child',
+ component: components.Foo,
+ })
+ next(to.fullPath)
+ } else next()
+ },
+ })
+
+ router.push('/dynamic/child').catch(() => {})
+ await tick()
+ expect(router.currentRoute.value).toMatchObject({
+ name: 'dynamic child',
+ })
+ })
+
+ it('can reroute when adding a new route', async () => {
+ const { router } = await newRouter()
+ await router.push('/p/p')
+ expect(router.currentRoute.value).toMatchObject({
+ name: 'Param',
+ })
+ router.addRoute({
+ path: '/p/p',
+ component: components.Foo,
+ name: 'pp',
+ })
+ await router.replace(router.currentRoute.value.fullPath)
+ expect(router.currentRoute.value).toMatchObject({
+ name: 'pp',
+ })
+ })
+
+ it('stops resolving removed routes', async () => {
+ const { router } = await newRouter()
+ // regular route
+ router.removeRoute('Foo')
+ expect(router.resolve('/foo')).toMatchObject({
+ name: undefined,
+ matched: [],
+ })
+ // dynamic route
+ const removeRoute = router.addRoute({
+ path: '/new-route',
+ component: components.Foo,
+ name: 'new route',
+ })
+ removeRoute()
+ expect(router.resolve('/new-route')).toMatchObject({
+ name: undefined,
+ matched: [],
+ })
+ })
+
+ it('can reroute when removing route', async () => {
+ const { router } = await newRouter()
+ router.addRoute({
+ path: '/p/p',
+ component: components.Foo,
+ name: 'pp',
+ })
+ await router.push('/p/p')
+ router.removeRoute('pp')
+ await router.replace(router.currentRoute.value.fullPath)
+ expect(router.currentRoute.value).toMatchObject({
+ name: 'Param',
+ })
+ })
+
+ it('can reroute when removing route through returned function', async () => {
+ const { router } = await newRouter()
+ const remove = router.addRoute({
+ path: '/p/p',
+ component: components.Foo,
+ name: 'pp',
+ })
+ await router.push('/p/p')
+ remove()
+ await router.push('/p/p')
+ expect(router.currentRoute.value).toMatchObject({
+ name: 'Param',
+ })
+ })
+ })
})
import { createRouter, createWebHistory } from '../src'
import Home from './views/Home.vue'
import Nested from './views/Nested.vue'
+import Dynamic from './views/Dynamic.vue'
import User from './views/User.vue'
import NotFound from './views/NotFound.vue'
import component from './views/Generic.vue'
import ComponentWithData from './views/ComponentWithData.vue'
import { globalState } from './store'
import { scrollWaiter } from './scrollWaiter'
+let removeRoute: (() => void) | undefined
// const hist = new HTML5History()
// const hist = new HashHistory()
},
],
},
+ {
+ path: '/dynamic',
+ name: 'dynamic',
+ component: Nested,
+ options: { end: false, strict: true },
+ beforeEnter(to, from, next) {
+ if (!removeRoute) {
+ removeRoute = router.addRoute('dynamic', {
+ path: 'child',
+ component: Dynamic,
+ })
+ next(to.fullPath)
+ } else next()
+ },
+ },
],
async scrollBehavior(to, from, savedPosition) {
await scrollWaiter.wait()
--- /dev/null
+<template>
+ <div>This was added dynamically</div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ name: 'Dynamic',
+})
+</script>
(matcher: RouteRecordMatcher): void
(name: Required<RouteRecord>['name']): void
}
- // TODO:
- // getRoutes: () => RouteRecordMatcher[]
+ getRoutes: () => RouteRecordMatcher[]
getRecordMatcher: (
name: Required<RouteRecord>['name']
) => RouteRecordMatcher | undefined
return matcherMap.get(name)
}
+ // TODO: add routes to children of parent
function addRoute(
record: Readonly<RouteRecord>,
parent?: RouteRecordMatcher
}
}
+ function getRoutes() {
+ return matchers
+ }
+
function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0
// console.log('i is', { i })
// add initial routes
routes.forEach(route => addRoute(route))
- return { addRoute, resolve, removeRoute, getRecordMatcher }
+ return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
/**
}
// only apply the strict bonus to the last score
- if (options.strict) {
+ if (options.strict && options.end) {
const i = score.length - 1
score[i][score[i].length - 1] += PathScore.BonusStrict
}
if (!options.strict) pattern += '/?'
if (options.end) pattern += '$'
+ // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_somethingelse
+ else if (options.strict) pattern += '(?:/|$)'
const re = new RegExp(pattern, options.sensitive ? '' : 'i')
} from './utils'
import { useCallbacks } from './utils/callbacks'
import { encodeParam, decode } from './utils/encoding'
-import { normalizeQuery, parseQuery, stringifyQuery } from './utils/query'
-import { ref, Ref, markNonReactive, nextTick, App } from 'vue'
+import {
+ normalizeQuery,
+ parseQuery,
+ stringifyQuery,
+ LocationQueryValue,
+} from './utils/query'
+import { ref, Ref, markNonReactive, nextTick, App, warn } from 'vue'
import { RouteRecordNormalized } from './matcher/types'
import { Link } from './components/Link'
import { View } from './components/View'
history: RouterHistory
currentRoute: Ref<Immutable<RouteLocationNormalized>>
+ addRoute(parentName: string, route: RouteRecord): () => void
+ addRoute(route: RouteRecord): () => void
+ removeRoute(name: string): void
+ getRoutes(): RouteRecordNormalized[]
+
resolve(to: RouteLocation): RouteLocationNormalized
createHref(to: RouteLocationNormalized): string
push(to: RouteLocation): Promise<RouteLocationNormalized>
const encodeParams = applyToParams.bind(null, encodeParam)
const decodeParams = applyToParams.bind(null, decode)
+ function addRoute(parentOrRoute: string | RouteRecord, route?: RouteRecord) {
+ let parent: Parameters<typeof matcher['addRoute']>[1] | undefined
+ let record: RouteRecord
+ if (typeof parentOrRoute === 'string') {
+ parent = matcher.getRecordMatcher(parentOrRoute)
+ record = route!
+ } else {
+ record = parentOrRoute
+ }
+
+ return matcher.addRoute(record, parent)
+ }
+
+ function removeRoute(name: string) {
+ let recordMatcher = matcher.getRecordMatcher(name)
+ if (recordMatcher) {
+ matcher.removeRoute(recordMatcher)
+ } else if (__DEV__) {
+ // TODO: adapt if we allow Symbol as a name
+ warn(`Cannot remove non-existant route "${name}"`)
+ }
+ }
+
+ function getRoutes(): RouteRecordNormalized[] {
+ return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
+ }
+
function resolve(
location: RouteLocation,
currentLocation?: RouteLocationNormalized
): Promise<RouteLocationNormalized> {
const toLocation: RouteLocationNormalized = (pendingLocation = resolve(to))
const from: RouteLocationNormalized = currentRoute.value
+ // @ts-ignore: no need to check the string as force do not exist on a string
+ const force: boolean | undefined = to.force
// TODO: should we throw an error as the navigation was aborted
// TODO: needs a proper check because order in query could be different
- if (
- from !== START_LOCATION_NORMALIZED &&
- from.fullPath === toLocation.fullPath
- )
- return from
+ if (!force && isSameLocation(from, toLocation)) return from
toLocation.redirectedFrom = redirectedFrom
const router: Router = {
currentRoute,
+
+ addRoute,
+ removeRoute,
+ getRoutes,
+
push,
replace,
resolve,
+
beforeEach: beforeGuards.add,
afterEach: afterGuards.add,
createHref,
+
onError: errorHandlers.add,
isReady,
return [leavingRecords, updatingRecords, enteringRecords]
}
+
+function isSameLocation(
+ a: RouteLocationNormalized,
+ b: RouteLocationNormalized
+): boolean {
+ return (
+ a.name === b.name &&
+ a.path === b.path &&
+ a.hash === b.hash &&
+ isSameLocationQuery(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 interface RouteLocationOptions {
+ /**
+ * Replace the entry in the history instead of pushing a new entry
+ */
replace?: boolean
+ /**
+ * Triggers the navigation even if the location is the same as the current one
+ */
+ force?: boolean
}
// User level location
import { decode, encodeQueryProperty } from '../utils/encoding'
-type LocationQueryValue = string | null
+export type LocationQueryValue = string | null
type LocationQueryValueRaw = LocationQueryValue | number | undefined
export type LocationQuery = Record<
string,