Skip to content

wip: vapor mode #2509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"preview": "vite preview --port 4173"
},
"dependencies": {
"vue": "~3.5.13"
"vue": "https://pkg.pr.new/vue@280bc48"
},
"devDependencies": {
"@types/node": "^20.17.31",
Expand Down
2 changes: 1 addition & 1 deletion packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,6 @@
"rollup-plugin-analyzer": "^4.0.0",
"rollup-plugin-typescript2": "^0.36.0",
"vite": "^5.4.18",
"vue": "~3.5.13"
"vue": "https://pkg.pr.new/vue@280bc48"
}
}
2 changes: 1 addition & 1 deletion packages/router/src/RouterLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,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
89 changes: 89 additions & 0 deletions packages/router/src/VaporRouterLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { routerKey } from './injectionSymbols'
import {
_RouterLinkI,
getLinkClass,
type RouterLinkProps,
useLink,
} from './RouterLink'
import { RouteLocationRaw } from './typed-routes'
import {
computed,
createComponentWithFallback,
createDynamicComponent,
defineVaporComponent,
inject,
PropType,
reactive,
} from 'vue'

export const VaporRouterLinkImpl = /*#__PURE__*/ defineVaporComponent({
name: 'RouterLink',
// @ts-ignore
compatConfig: { MODE: 3 },
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,
},

useLink,

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(() => {
const children = slots.default && slots.default(link)
return props.custom
? () => children
: () =>
createComponentWithFallback(
'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],
},
{
default: () => children,
}
)
})
},
})

export const VaporRouterLink: _RouterLinkI = VaporRouterLinkImpl as any
204 changes: 204 additions & 0 deletions packages/router/src/VaporRouterView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {
inject,
provide,
PropType,
ref,
unref,
ComponentPublicInstance,
VNodeProps,
computed,
AllowedComponentProps,
ComponentCustomProps,
watch,
VNode,
createTemplateRefSetter,
createComponent,
createDynamicComponent,
defineVaporComponent,
type VaporComponent,
type VaporSlot,
} 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: 'RouterView',
// #674 we manually inherit them
inheritAttrs: false,
props: {
name: {
type: String as PropType<string>,
default: 'default',
},
route: Object as PropType<RouteLocationNormalizedLoaded>,
},

// Better compat for @vue/compat users
// https://github.com/vuejs/router/issues/1315
// @ts-ignore
compatConfig: { MODE: 3 },

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()

return createDynamicComponent(() => {
if (!ViewComponent.value) {
return () =>
normalizeSlot(slots.default, {
Component: ViewComponent.value,
route: routeToDisplay.value,
})
}

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

return (
normalizeSlot(slots.default, {
Component: component,
route: routeToDisplay.value,
}) || component
)
}
})
},
})

function normalizeSlot(slot: VaporSlot | undefined, data: any) {
if (!slot) return null
return slot(data)
}

// 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[]
}
}
}
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,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
12 changes: 7 additions & 5 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1259,11 +1259,13 @@ export function createRouter(options: RouterOptions): Router {
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)

app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
if (!app.vapor) {
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
}

// this initial navigation is only necessary on client, on server it doesn't
// make sense because it will create an extra unnecessary navigation and could
Expand Down
Loading
Loading