},
],
},
+
+ passthrough: {
+ fullPath: '/foo',
+ name: undefined,
+ path: '/foo',
+ query: {},
+ params: {},
+ hash: '',
+ meta: {},
+ matched: [
+ {
+ // @ts-ignore: FIXME:
+ components: null,
+ instances: {},
+ enterCallbacks: {},
+ path: '/',
+ props,
+ },
+ {
+ components: { default: components.Foo },
+ instances: {},
+ enterCallbacks: {},
+ path: 'foo',
+ props,
+ },
+ ],
+ },
})
describe('RouterView', () => {
expect(wrapper.html()).toBe(`<div>id:2;other:page</div>`)
})
+ it('pass through with empty children', async () => {
+ const { wrapper } = await factory(routes.passthrough)
+ expect(wrapper.html()).toBe(`<div>Foo</div>`)
+ })
+
describe('warnings', () => {
it('does not warn RouterView is wrapped', () => {
const route = createMockedRoute(routes.root)
const from = START_LOCATION_NORMALIZED
const NoGuard: RouteRecordRaw = { path: '/', component: components.Home }
+// @ts-expect-error
const InvalidRoute: RouteRecordRaw = {
path: '/',
- // @ts-expect-error
component: null,
}
const WrongLazyRoute: RouteRecordRaw = {
it('throws if component is null', async () => {
// @ts-expect-error
- await expect(checkGuards([InvalidRoute], 2)).rejects.toHaveProperty(
- 'message',
- expect.stringMatching('Invalid route component')
- )
- expect('is not a valid component').toHaveBeenWarned()
+ await expect(checkGuards([InvalidRoute], 0))
+ expect('either missing a "component(s)" or "children"').toHaveBeenWarned()
})
it('warns wrong lazy component', async () => {
MatcherLocationRaw,
MatcherLocation,
} from '../../src/types'
-import { MatcherLocationNormalizedLoose } from '../utils'
+import { MatcherLocationNormalizedLoose, RouteRecordViewLoose } from '../utils'
import { mockWarn } from 'jest-mock-warn'
+import { defineComponent } from '@vue/runtime-core'
-// @ts-expect-error
-const component: RouteComponent = null
+const component: RouteComponent = defineComponent({})
+
+const baseRouteRecordNormalized: RouteRecordViewLoose = {
+ instances: {},
+ enterCallbacks: {},
+ aliasOf: undefined,
+ components: null,
+ path: '',
+ props: {},
+}
// for normalized records
const components = { default: component }
matched: [
{
path: '/p',
+ // @ts-expect-error: doesn't matter
children,
components,
aliasOf: expect.objectContaining({ path: '/parent' }),
matched: [
{
path: '/parent',
+ // @ts-expect-error
children,
components,
aliasOf: undefined,
name: 'child-b',
path: '/foo/b',
params: {},
- matched: [Foo, { ...ChildB, path: `${Foo.path}/${ChildB.path}` }],
+ matched: [
+ Foo as any,
+ { ...ChildB, path: `${Foo.path}/${ChildB.path}` },
+ ],
}
)
})
name: 'nested',
path: '/foo',
params: {},
- matched: [Foo, { ...Nested, path: `${Foo.path}` }],
+ matched: [Foo as any, { ...Nested, path: `${Foo.path}` }],
}
)
})
path: '/foo',
params: {},
matched: [
- Foo,
+ Foo as any,
{ ...Nested, path: `${Foo.path}` },
{ ...NestedNested, path: `${Foo.path}` },
],
path: '/foo/nested/a',
params: {},
matched: [
- Foo,
+ Foo as any,
{ ...Nested, path: `${Foo.path}/${Nested.path}` },
{
...NestedChildA,
path: '/foo/nested/a',
params: {},
matched: [
- Foo,
+ Foo as any,
{ ...Nested, path: `${Foo.path}/${Nested.path}` },
{
...NestedChildA,
path: '/foo/nested/a',
params: {},
matched: [
- Foo,
+ Foo as any,
{ ...Nested, path: `${Foo.path}/${Nested.path}` },
{
...NestedChildA,
path: '/foo/nested/a/b',
params: { p: 'b', n: 'a' },
matched: [
- Foo,
+ Foo as any,
{
...NestedWithParam,
path: `${Foo.path}/${NestedWithParam.path}`,
path: '/foo/nested/b/a',
params: { p: 'a', n: 'b' },
matched: [
- Foo,
+ Foo as any,
{
...NestedWithParam,
path: `${Foo.path}/${NestedWithParam.path}`,
name: 'nested',
path: '/nested',
params: {},
- matched: [Parent, { ...Nested, path: `/nested` }],
+ matched: [Parent as any, { ...Nested, path: `/nested` }],
}
)
})
name: 'nested',
path: '/parent/nested',
params: {},
- matched: [Parent, { ...Nested, path: `/parent/nested` }],
+ matched: [Parent as any, { ...Nested, path: `/parent/nested` }],
}
)
})
export interface RouteRecordViewLoose
extends Pick<
RouteRecordMultipleViews,
- 'path' | 'name' | 'components' | 'children' | 'meta' | 'beforeEnter'
+ 'path' | 'name' | 'meta' | 'beforeEnter'
> {
leaveGuards?: any
instances: Record<string, any>
enterCallbacks: Record<string, Function[]>
props: Record<string, _RouteRecordProps>
aliasOf: RouteRecordViewLoose | undefined
+ children?: RouteRecordViewLoose[]
+ components: Record<string, RouteComponent> | null | undefined
}
// @ts-expect-error we are intentionally overriding the type
defineComponent,
PropType,
ref,
+ unref,
ComponentPublicInstance,
VNodeProps,
getCurrentInstance,
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed(() => props.route || injectedRoute.value)
- const depth = inject(viewDepthKey, 0)
+ const injectedDepth = inject(viewDepthKey, 0)
+ // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children
+ // that are used to reuse the `path` property
+ const depth = computed<number>(() => {
+ let initialDepth = unref(injectedDepth)
+ const { matched } = routeToDisplay.value
+ let matchedRoute: RouteLocationMatched | undefined
+ while (
+ (matchedRoute = matched[initialDepth]) &&
+ !matchedRoute.components
+ ) {
+ initialDepth++
+ }
+ return initialDepth
+ })
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
- () => routeToDisplay.value.matched[depth]
+ () => routeToDisplay.value.matched[depth.value]
)
- provide(viewDepthKey, depth + 1)
+ provide(
+ viewDepthKey,
+ computed(() => depth.value + 1)
+ )
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)
return () => {
const route = routeToDisplay.value
const matchedRoute = matchedRouteRef.value
- const ViewComponent = matchedRoute && matchedRoute.components[props.name]
+ const ViewComponent = matchedRoute && matchedRoute.components![props.name]
// we need the value at the time we render because when we unmount, we
// navigated to a different location so the value is different
const currentName = props.name
) {
// TODO: can display if it's an alias, its props
const info: RouterViewDevtoolsContext = {
- depth,
+ depth: depth.value,
name: matchedRoute.name,
path: matchedRoute.path,
meta: matchedRoute.meta,
*/
export const viewDepthKey = /*#__PURE__*/ PolySymbol(
__DEV__ ? 'router view depth' : 'rvd'
-) as InjectionKey<number>
+) as InjectionKey<Ref<number> | number>
/**
* Allows overriding the router instance returned by `useRouter` in tests. r
aliasOf: undefined,
beforeEnter: record.beforeEnter,
props: normalizeRecordProps(record),
+ // @ts-expect-error: record.children only exists in some cases
children: record.children || [],
instances: {},
leaveGuards: new Set(),
enterCallbacks: {},
components:
'components' in record
- ? record.components || {}
- : { default: record.component! },
+ ? record.components || null
+ : record.component && { default: record.component },
}
}
/**
* {@inheritDoc RouteRecordMultipleViews.components}
*/
- components: RouteRecordMultipleViews['components']
+ components: RouteRecordMultipleViews['components'] | null | undefined
/**
* {@inheritDoc _RouteRecordBase.components}
*/
const guards: Array<() => Promise<void>> = []
for (const record of matched) {
+ if (__DEV__ && !record.components && !record.children.length) {
+ warn(
+ `Record with path "${record.path}" is either missing a "component(s)"` +
+ ` or "children" property.`
+ )
+ }
for (const name in record.components) {
let rawComponent = record.components[name]
if (__DEV__) {
? resolved.default
: resolved
// replace the function with the resolved component
- record.components[name] = resolvedComponent
+ // cannot be null or undefined because we went into the for loop
+ record.components![name] = resolvedComponent
// __vccOpts is added by vue-class-component and contain the regular options
const options: ComponentOptions =
(resolvedComponent as any).__vccOpts || resolvedComponent
reactive,
unref,
computed,
+ ref,
} from 'vue'
import { RouteRecord, RouteRecordNormalized } from './matcher/types'
import {
import { LocationQuery, LocationQueryRaw } from '../query'
import { PathParserOptions } from '../matcher'
-import { Ref, ComponentPublicInstance, Component } from 'vue'
+import { Ref, ComponentPublicInstance, Component, DefineComponent } from 'vue'
import { RouteRecord, RouteRecordNormalized } from '../matcher/types'
import { HistoryState } from '../history/common'
import { NavigationFailure } from '../errors'
export interface RouteLocationMatched extends RouteRecordNormalized {
// components cannot be Lazy<RouteComponent>
- components: Record<string, RouteComponent>
+ components: Record<string, RouteComponent> | null | undefined
}
/**
/**
* Allowed Component in {@link RouteLocationMatched}
*/
-export type RouteComponent = Component
+export type RouteComponent = Component | DefineComponent
/**
* Allowed Component definitions in route records provided by the user
*/
* @example `/users/:id` matches `/users/1` as well as `/users/posva`.
*/
path: string
+
/**
* Where to redirect if the route is directly matched. The redirection happens
* before any navigation guard and triggers a new navigation with the new
* target location.
*/
redirect?: RouteRecordRedirectOption
- /**
- * Array of nested routes.
- */
- children?: RouteRecordRaw[]
+
/**
* Aliases for the record. Allows defining extra paths that will behave like a
* copy of the record. Allows having paths shorthands like `/users/:id` and
* `/u/:id`. All `alias` and `path` values must share the same params.
*/
alias?: string | string[]
+
/**
* Name for the route record.
*/
name?: RouteRecordName
+
/**
* Before Enter guard specific to this record. Note `beforeEnter` has no
* effect if the record has a `redirect` property.
beforeEnter?:
| NavigationGuardWithThis<undefined>
| NavigationGuardWithThis<undefined>[]
+
/**
* Arbitrary data attached to the record.
*/
props?: _RouteRecordProps
}
+/**
+ * Route Record defining one single component with a nested view.
+ */
+export interface RouteRecordSingleViewWithChildren extends _RouteRecordBase {
+ /**
+ * Component to display when the URL matches this route.
+ */
+ component?: RawRouteComponent | null | undefined
+ components?: never
+
+ /**
+ * Array of nested routes.
+ */
+ children: RouteRecordRaw[]
+
+ /**
+ * Allow passing down params as props to the component rendered by `router-view`.
+ */
+ props?: _RouteRecordProps
+}
+
/**
* Route Record defining multiple named components with the `components` option.
*/
*/
components: Record<string, RawRouteComponent>
component?: never
+
+ /**
+ * Allow passing down params as props to the component rendered by
+ * `router-view`. Should be an object with the same keys as `components` or a
+ * boolean to be applied to every component.
+ */
+ props?: Record<string, _RouteRecordProps> | boolean
+}
+
+/**
+ * Route Record defining multiple named components with the `components` option and children.
+ */
+export interface RouteRecordMultipleViewsWithChildren extends _RouteRecordBase {
+ /**
+ * Components to display when the URL matches this route. Allow using named views.
+ */
+ components?: Record<string, RawRouteComponent> | null | undefined
+ component?: never
+
+ children: RouteRecordRaw[]
+
/**
* Allow passing down params as props to the component rendered by
* `router-view`. Should be an object with the same keys as `components` or a
export type RouteRecordRaw =
| RouteRecordSingleView
+ | RouteRecordSingleViewWithChildren
| RouteRecordMultipleViews
+ | RouteRecordMultipleViewsWithChildren
| RouteRecordRedirect
/**