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
2 changes: 1 addition & 1 deletion packages/experiments-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"type-check": "vue-tsc --build"
},
"dependencies": {
"vue": "~3.6.0-alpha.2",
"vue": "~3.6.0-alpha.7",
"vue-router": "workspace:*"
},
"devDependencies": {
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.7"
},
"devDependencies": {
"@types/node": "^24.7.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,6 @@
"tsdown": "^0.17.2",
"tsup": "^8.5.0",
"vite": "^7.1.10",
"vue": "~3.6.0-alpha.2"
"vue": "~3.6.0-alpha.7"
}
}
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
85 changes: 85 additions & 0 deletions packages/router/src/VaporRouterLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { routerKey } from './injectionSymbols'
import {
_RouterLinkI,
getLinkClass,
type RouterLinkProps,
useLink,
} from './RouterLink'
import { RouteLocationRaw } from './typed-routes'
import {
computed,
createDynamicComponent,
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,
}))

return createDynamicComponent(() => {
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
)
})
},
})

// @ts-ignore
VaporRouterLinkImpl.useLink = useLink

export const VaporRouterLink: _RouterLinkI = VaporRouterLinkImpl as any
188 changes: 188 additions & 0 deletions packages/router/src/VaporRouterView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import {
inject,
provide,
PropType,
ref,
unref,
ComponentPublicInstance,
VNodeProps,
computed,
AllowedComponentProps,
ComponentCustomProps,
watch,
VNode,
createTemplateRefSetter,
createComponent,
defineVaporComponent,
type VaporComponent,
createDynamicComponent,
} 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 initComponent = () => {
if (!ViewComponent.value) return []
const instance = createComponent(ViewComponent.value as VaporComponent, {
$: [() => assign({}, routeProps.value, attrs)],
})
setRef(instance, viewRef)
return instance
}

if (slots.default) {
return slots.default({
// lazy initialization via getter (created on demand) for KeepAlive
get Component() {
return initComponent()
},
get route() {
return routeToDisplay.value
},
})
}
return createDynamicComponent(initComponent)
},
})

// 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[]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { MatcherPatternPath } from './matcher-pattern'
* ```
*/
export class MatcherPatternPathStar
implements MatcherPatternPath<{ pathMatch: string }>
implements
MatcherPatternPath<{
pathMatch: string
}>
{
private path: string
constructor(path: string = '') {
Expand Down
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ export type {
UseLinkOptions,
UseLinkReturn,
} from './RouterLink'
export { VaporRouterLink } from './VaporRouterLink'
export { RouterView } from './RouterView'
export { VaporRouterView } from './VaporRouterView'
export type { RouterViewProps } from './RouterView'

export type { TypesConfig } from './config'
Expand Down
5 changes: 4 additions & 1 deletion packages/router/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import Vue from '@vitejs/plugin-vue'

export default defineConfig({
resolve: {
alias: [],
alias: {
// cjs does not export vapor runtime, use esm instead.
vue: 'vue/dist/vue.esm-bundler.js',
},
},
define: {
__DEV__: true,
Expand Down
Loading
Loading