Skip to content

Commit d5f389c

Browse files
committed
wip: vapor mode
1 parent 36aed4f commit d5f389c

File tree

6 files changed

+629
-37
lines changed

6 files changed

+629
-37
lines changed

packages/router/src/RouterLink.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ function getOriginalPath(record: RouteRecord | undefined): string {
448448
* @param globalClass
449449
* @param defaultClass
450450
*/
451-
const getLinkClass = (
451+
export const getLinkClass = (
452452
propClass: string | undefined,
453453
globalClass: string | undefined,
454454
defaultClass: string
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { routerKey } from './injectionSymbols'
2+
import {
3+
_RouterLinkI,
4+
getLinkClass,
5+
type RouterLinkProps,
6+
useLink,
7+
} from './RouterLink'
8+
import { RouteLocationRaw } from './typed-routes'
9+
import {
10+
computed,
11+
createComponentWithFallback,
12+
createDynamicComponent,
13+
defineVaporComponent,
14+
inject,
15+
PropType,
16+
reactive,
17+
} from 'vue'
18+
19+
export const VaporRouterLinkImpl = /*#__PURE__*/ defineVaporComponent({
20+
name: 'RouterLink',
21+
// @ts-ignore
22+
compatConfig: { MODE: 3 },
23+
props: {
24+
to: {
25+
type: [String, Object] as PropType<RouteLocationRaw>,
26+
required: true,
27+
},
28+
replace: Boolean,
29+
activeClass: String,
30+
// inactiveClass: String,
31+
exactActiveClass: String,
32+
custom: Boolean,
33+
ariaCurrentValue: {
34+
type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
35+
default: 'page',
36+
},
37+
viewTransition: Boolean,
38+
},
39+
40+
useLink,
41+
42+
setup(props, { slots, attrs }) {
43+
const link = reactive(useLink(props))
44+
const { options } = inject(routerKey)!
45+
46+
const elClass = computed(() => ({
47+
[getLinkClass(
48+
props.activeClass,
49+
options.linkActiveClass,
50+
'router-link-active'
51+
)]: link.isActive,
52+
// [getLinkClass(
53+
// props.inactiveClass,
54+
// options.linkInactiveClass,
55+
// 'router-link-inactive'
56+
// )]: !link.isExactActive,
57+
[getLinkClass(
58+
props.exactActiveClass,
59+
options.linkExactActiveClass,
60+
'router-link-exact-active'
61+
)]: link.isExactActive,
62+
}))
63+
64+
return createDynamicComponent(() => {
65+
const children = slots.default && slots.default(link)
66+
return props.custom
67+
? () => children
68+
: () =>
69+
createComponentWithFallback(
70+
'a',
71+
{
72+
'aria-current': () =>
73+
link.isExactActive ? props.ariaCurrentValue : null,
74+
href: () => link.href,
75+
// this would override user added attrs but Vue will still add
76+
// the listener, so we end up triggering both
77+
onClick: () => link.navigate,
78+
class: () => elClass.value,
79+
$: [() => attrs],
80+
},
81+
{
82+
default: () => children,
83+
}
84+
)
85+
})
86+
},
87+
})
88+
89+
export const VaporRouterLink: _RouterLinkI = VaporRouterLinkImpl as any
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import {
2+
inject,
3+
provide,
4+
PropType,
5+
ref,
6+
unref,
7+
ComponentPublicInstance,
8+
VNodeProps,
9+
computed,
10+
AllowedComponentProps,
11+
ComponentCustomProps,
12+
watch,
13+
VNode,
14+
createTemplateRefSetter,
15+
createComponent,
16+
createDynamicComponent,
17+
defineVaporComponent,
18+
type VaporComponent,
19+
type VaporSlot,
20+
} from 'vue'
21+
import type { RouteLocationNormalizedLoaded } from './typed-routes'
22+
import type { RouteLocationMatched } from './types'
23+
import {
24+
matchedRouteKey,
25+
viewDepthKey,
26+
routerViewLocationKey,
27+
} from './injectionSymbols'
28+
import { assign } from './utils'
29+
import { isSameRouteRecord } from './location'
30+
import type { RouterViewProps, RouterViewDevtoolsContext } from './RouterView'
31+
32+
export type { RouterViewProps, RouterViewDevtoolsContext }
33+
34+
export const VaporRouterViewImpl = /*#__PURE__*/ defineVaporComponent({
35+
name: 'RouterView',
36+
// #674 we manually inherit them
37+
inheritAttrs: false,
38+
props: {
39+
name: {
40+
type: String as PropType<string>,
41+
default: 'default',
42+
},
43+
route: Object as PropType<RouteLocationNormalizedLoaded>,
44+
},
45+
46+
// Better compat for @vue/compat users
47+
// https://github.com/vuejs/router/issues/1315
48+
// @ts-ignore
49+
compatConfig: { MODE: 3 },
50+
51+
setup(props, { attrs, slots }) {
52+
const injectedRoute = inject(routerViewLocationKey)!
53+
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
54+
() => props.route || injectedRoute.value
55+
)
56+
const injectedDepth = inject(viewDepthKey, 0)
57+
// The depth changes based on empty components option, which allows passthrough routes e.g. routes with children
58+
// that are used to reuse the `path` property
59+
const depth = computed<number>(() => {
60+
let initialDepth = unref(injectedDepth)
61+
const { matched } = routeToDisplay.value
62+
let matchedRoute: RouteLocationMatched | undefined
63+
while (
64+
(matchedRoute = matched[initialDepth]) &&
65+
!matchedRoute.components
66+
) {
67+
initialDepth++
68+
}
69+
return initialDepth
70+
})
71+
const matchedRouteRef = computed<RouteLocationMatched | undefined>(
72+
() => routeToDisplay.value.matched[depth.value]
73+
)
74+
75+
provide(
76+
viewDepthKey,
77+
computed(() => depth.value + 1)
78+
)
79+
provide(matchedRouteKey, matchedRouteRef)
80+
provide(routerViewLocationKey, routeToDisplay)
81+
82+
const viewRef = ref<ComponentPublicInstance>()
83+
84+
// watch at the same time the component instance, the route record we are
85+
// rendering, and the name
86+
watch(
87+
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
88+
([instance, to, name], [oldInstance, from]) => {
89+
// copy reused instances
90+
if (to) {
91+
// this will update the instance for new instances as well as reused
92+
// instances when navigating to a new route
93+
to.instances[name] = instance
94+
// the component instance is reused for a different route or name, so
95+
// we copy any saved update or leave guards. With async setup, the
96+
// mounting component will mount before the matchedRoute changes,
97+
// making instance === oldInstance, so we check if guards have been
98+
// added before. This works because we remove guards when
99+
// unmounting/deactivating components
100+
if (from && from !== to && instance && instance === oldInstance) {
101+
if (!to.leaveGuards.size) {
102+
to.leaveGuards = from.leaveGuards
103+
}
104+
if (!to.updateGuards.size) {
105+
to.updateGuards = from.updateGuards
106+
}
107+
}
108+
}
109+
110+
// trigger beforeRouteEnter next callbacks
111+
if (
112+
instance &&
113+
to &&
114+
// if there is no instance but to and from are the same this might be
115+
// the first visit
116+
(!from || !isSameRouteRecord(to, from) || !oldInstance)
117+
) {
118+
;(to.enterCallbacks[name] || []).forEach(callback =>
119+
callback(instance)
120+
)
121+
}
122+
},
123+
{ flush: 'post' }
124+
)
125+
126+
const ViewComponent = computed(() => {
127+
const matchedRoute = matchedRouteRef.value
128+
return matchedRoute && matchedRoute.components![props.name]
129+
})
130+
131+
// props from route configuration
132+
const routeProps = computed(() => {
133+
const route = routeToDisplay.value
134+
const currentName = props.name
135+
const matchedRoute = matchedRouteRef.value
136+
const routePropsOption = matchedRoute && matchedRoute.props[currentName]
137+
return routePropsOption
138+
? routePropsOption === true
139+
? route.params
140+
: typeof routePropsOption === 'function'
141+
? routePropsOption(route)
142+
: routePropsOption
143+
: null
144+
})
145+
146+
return createDynamicComponent(() => {
147+
if (!ViewComponent.value) {
148+
return () =>
149+
normalizeSlot(slots.default, {
150+
Component: ViewComponent.value,
151+
route: routeToDisplay.value,
152+
})
153+
}
154+
155+
const setRef = createTemplateRefSetter()
156+
157+
return () => {
158+
const component = createComponent(
159+
ViewComponent.value as VaporComponent,
160+
{
161+
$: [() => assign({}, routeProps.value, attrs)],
162+
}
163+
)
164+
setRef(component, viewRef)
165+
166+
return (
167+
normalizeSlot(slots.default, {
168+
Component: component,
169+
route: routeToDisplay.value,
170+
}) || component
171+
)
172+
}
173+
})
174+
},
175+
})
176+
177+
function normalizeSlot(slot: VaporSlot | undefined, data: any) {
178+
if (!slot) return null
179+
return slot(data)
180+
}
181+
182+
// export the public type for h/tsx inference
183+
// also to avoid inline import() in generated d.ts files
184+
/**
185+
* Component to display the current route the user is at.
186+
*/
187+
export const VaporRouterView = VaporRouterViewImpl as unknown as {
188+
new (): {
189+
$props: AllowedComponentProps &
190+
ComponentCustomProps &
191+
VNodeProps &
192+
RouterViewProps
193+
194+
$slots: {
195+
default?: ({
196+
Component,
197+
route,
198+
}: {
199+
Component: VNode
200+
route: RouteLocationNormalizedLoaded
201+
}) => VNode[]
202+
}
203+
}
204+
}

packages/router/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ export type {
159159
UseLinkOptions,
160160
UseLinkReturn,
161161
} from './RouterLink'
162+
export { VaporRouterLink } from './VaporRouterLink'
162163
export { RouterView } from './RouterView'
164+
export { VaporRouterView } from './VaporRouterView'
163165
export type { RouterViewProps } from './RouterView'
164166

165167
export type { TypesConfig } from './config'

packages/router/src/router.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,11 +1259,13 @@ export function createRouter(options: RouterOptions): Router {
12591259
app.component('RouterLink', RouterLink)
12601260
app.component('RouterView', RouterView)
12611261

1262-
app.config.globalProperties.$router = router
1263-
Object.defineProperty(app.config.globalProperties, '$route', {
1264-
enumerable: true,
1265-
get: () => unref(currentRoute),
1266-
})
1262+
if (!app.vapor) {
1263+
app.config.globalProperties.$router = router
1264+
Object.defineProperty(app.config.globalProperties, '$route', {
1265+
enumerable: true,
1266+
get: () => unref(currentRoute),
1267+
})
1268+
}
12671269

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

0 commit comments

Comments
 (0)