Skip to content
Draft
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: 1 addition & 2 deletions packages/docs/.vitepress/config/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ export const sharedConfig = defineConfig({
],

footer: {
copyright:
'Copyright © 2014-present Evan You, Eduardo San Martin Morote',
copyright: 'Copyright © 2014-present Evan You, Eduardo San Martin Morote',
message: 'Released under the MIT License.',
},

Expand Down
10 changes: 7 additions & 3 deletions packages/experiments-playground/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ const r_home = normalizeRouteRecord({
name: 'home',
path: new MatcherPatternPathStatic('/'),
query: [
new MatcherPatternQueryParam('pageArray', 'p', 'array', PARAM_PARSER_INT, [
1,
]),
new MatcherPatternQueryParam(
'pageArray',
'p',
'array',
PARAM_PARSER_INT,
[1]
),
new MatcherPatternQueryParam('page', 'p', 'value', PARAM_PARSER_INT, 1),
QUERY_PATTERN_MATCHER,
],
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"preview": "vite preview --port 4173"
},
"dependencies": {
"vue": "~3.6.0-alpha.2"
"vue": "~3.6.0-alpha.6"
},
"devDependencies": {
"@types/node": "^24.7.2",
Expand Down
9 changes: 4 additions & 5 deletions packages/router/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ export function nextNavigation(router: Router | EXPERIMENTAL_Router) {
})
}

export interface RouteRecordViewLoose
extends Pick<
RouteRecordMultipleViews,
'path' | 'name' | 'meta' | 'beforeEnter'
> {
export interface RouteRecordViewLoose extends Pick<
RouteRecordMultipleViews,
'path' | 'name' | 'meta' | 'beforeEnter'
> {
leaveGuards?: any
updateGuards?: any
instances: Record<string, any>
Expand Down
2 changes: 1 addition & 1 deletion packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,6 @@
"tsdown": "^0.15.7",
"tsup": "^8.5.0",
"vite": "^7.1.10",
"vue": "~3.6.0-alpha.2"
"vue": "https://pkg.pr.new/vue@13320fd"
}
}
2 changes: 1 addition & 1 deletion packages/router/src/RouterLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ function getOriginalPath(record: RouteRecord | undefined): string {
* @param globalClass
* @param defaultClass
*/
const getLinkClass = (
export const getLinkClass = (
propClass: string | undefined,
globalClass: string | undefined,
defaultClass: string
Expand Down
6 changes: 4 additions & 2 deletions packages/router/src/RouterView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ export interface RouterViewProps {
route?: RouteLocationNormalized
}

export interface RouterViewDevtoolsContext
extends Pick<RouteLocationMatched, 'path' | 'name' | 'meta'> {
export interface RouterViewDevtoolsContext extends Pick<
RouteLocationMatched,
'path' | 'name' | 'meta'
> {
depth: number
}

Expand Down
91 changes: 91 additions & 0 deletions packages/router/src/VaporRouterLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { routerKey } from './injectionSymbols'
import {
_RouterLinkI,
getLinkClass,
type RouterLinkProps,
useLink,
} from './RouterLink'
import { RouteLocationRaw } from './typed-routes'
import {
computed,
createKeyedFragment,
createPlainElement,
defineVaporComponent,
inject,
PropType,
reactive,
} from 'vue'

export const VaporRouterLinkImpl = defineVaporComponent({
name: 'VaporRouterLink',
props: {
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
required: true,
},
replace: Boolean,
activeClass: String,
// inactiveClass: String,
exactActiveClass: String,
custom: Boolean,
ariaCurrentValue: {
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
default: 'page',
},
viewTransition: Boolean,
},

setup(props, { slots, attrs }) {
const link = reactive(useLink(props))
const { options } = inject(routerKey)!

const elClass = computed(() => ({
[getLinkClass(
props.activeClass,
options.linkActiveClass,
'router-link-active'
)]: link.isActive,
// [getLinkClass(
// props.inactiveClass,
// options.linkInactiveClass,
// 'router-link-inactive'
// )]: !link.isExactActive,
[getLinkClass(
props.exactActiveClass,
options.linkExactActiveClass,
'router-link-exact-active'
)]: link.isExactActive,
}))

const render = computed(() => {
if (props.custom && slots.default) {
return () => slots.default(link)
}
return () =>
createPlainElement(
'a',
{
'aria-current': () =>
link.isExactActive ? props.ariaCurrentValue : null,
href: () => link.href,
// this would override user added attrs but Vue will still add
// the listener, so we end up triggering both
onClick: () => link.navigate,
class: () => elClass.value,
$: [() => attrs],
},
slots
)
})

return createKeyedFragment(
() => render.value,
() => render.value()
)
},
})

// @ts-ignore
VaporRouterLinkImpl.useLink = useLink

export const VaporRouterLink: _RouterLinkI = VaporRouterLinkImpl as any
199 changes: 199 additions & 0 deletions packages/router/src/VaporRouterView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {
inject,
provide,
PropType,
ref,
unref,
ComponentPublicInstance,
VNodeProps,
computed,
AllowedComponentProps,
ComponentCustomProps,
watch,
VNode,
createTemplateRefSetter,
createComponent,
createKeyedFragment,
defineVaporComponent,
type VaporComponent,
} from 'vue'
import type { RouteLocationNormalizedLoaded } from './typed-routes'
import type { RouteLocationMatched } from './types'
import {
matchedRouteKey,
viewDepthKey,
routerViewLocationKey,
} from './injectionSymbols'
import { assign } from './utils'
import { isSameRouteRecord } from './location'
import type { RouterViewProps, RouterViewDevtoolsContext } from './RouterView'

export type { RouterViewProps, RouterViewDevtoolsContext }

export const VaporRouterViewImpl = /*#__PURE__*/ defineVaporComponent({
name: 'VaporRouterView',
// #674 we manually inherit them
inheritAttrs: false,
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},

setup(props, { attrs, slots }) {
const injectedRoute = inject(routerViewLocationKey)!
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
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.value]
)

provide(
viewDepthKey,
computed(() => depth.value + 1)
)
provide(matchedRouteKey, matchedRouteRef)
provide(routerViewLocationKey, routeToDisplay)

const viewRef = ref<ComponentPublicInstance>()

// watch at the same time the component instance, the route record we are
// rendering, and the name
watch(
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
([instance, to, name], [oldInstance, from]) => {
// copy reused instances
if (to) {
// this will update the instance for new instances as well as reused
// instances when navigating to a new route
to.instances[name] = instance
// the component instance is reused for a different route or name, so
// we copy any saved update or leave guards. With async setup, the
// mounting component will mount before the matchedRoute changes,
// making instance === oldInstance, so we check if guards have been
// added before. This works because we remove guards when
// unmounting/deactivating components
if (from && from !== to && instance && instance === oldInstance) {
if (!to.leaveGuards.size) {
to.leaveGuards = from.leaveGuards
}
if (!to.updateGuards.size) {
to.updateGuards = from.updateGuards
}
}
}

// trigger beforeRouteEnter next callbacks
if (
instance &&
to &&
// if there is no instance but to and from are the same this might be
// the first visit
(!from || !isSameRouteRecord(to, from) || !oldInstance)
) {
;(to.enterCallbacks[name] || []).forEach(callback =>
callback(instance)
)
}
},
{ flush: 'post' }
)

const ViewComponent = computed(() => {
const matchedRoute = matchedRouteRef.value
return matchedRoute && matchedRoute.components![props.name]
})

// props from route configuration
const routeProps = computed(() => {
const route = routeToDisplay.value
const currentName = props.name
const matchedRoute = matchedRouteRef.value
const routePropsOption = matchedRoute && matchedRoute.props[currentName]
return routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
})

const setRef = createTemplateRefSetter()

const render = computed(() => {
if (!ViewComponent.value) {
return () =>
slots.default
? slots.default({
Component: ViewComponent.value,
route: routeToDisplay.value,
})
: []
}

return () => {
const component = createComponent(
ViewComponent.value as VaporComponent,
{
$: [() => assign({}, routeProps.value, attrs)],
}
)
setRef(component, viewRef)

return slots.default
? slots.default({
Component: component,
route: routeToDisplay.value,
})
: component
}
})
return createKeyedFragment(
() => routeToDisplay.value,
() => render.value()
)
},
})

// export the public type for h/tsx inference
// also to avoid inline import() in generated d.ts files
/**
* Component to display the current route the user is at.
*/
export const VaporRouterView = VaporRouterViewImpl as unknown as {
new (): {
$props: AllowedComponentProps &
ComponentCustomProps &
VNodeProps &
RouterViewProps

$slots: {
default?: ({
Component,
route,
}: {
Component: VNode
route: RouteLocationNormalizedLoaded
}) => VNode[]
}
}
}
6 changes: 4 additions & 2 deletions packages/router/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ export interface NavigationFailure extends Error {
*
* @internal
*/
export interface NavigationRedirectError
extends Omit<NavigationFailure, 'to' | 'type'> {
export interface NavigationRedirectError extends Omit<
NavigationFailure,
'to' | 'type'
> {
type: ErrorTypes.NAVIGATION_GUARD_REDIRECT
to: RouteLocationRaw
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { MatcherPatternPath } from './matcher-pattern'
* matcher.build({ pathMatch: '/123' }) // '/team/123'
* ```
*/
export class MatcherPatternPathStar
implements MatcherPatternPath<{ pathMatch: string }>
{
export class MatcherPatternPathStar implements MatcherPatternPath<{
pathMatch: string
}> {
private path: string
constructor(path: string = '') {
this.path = path.toLowerCase()
Expand Down
Loading
Loading