From: Eduardo San Martin Morote Date: Wed, 18 Mar 2020 23:08:27 +0000 (+0100) Subject: feat: invoke guards with the right context X-Git-Tag: v4.0.0-alpha.4~41 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7053413c93bc715d5c2179378367dc12f60a118d;p=thirdparty%2Fvuejs%2Frouter.git feat: invoke guards with the right context --- diff --git a/__tests__/RouterView.spec.ts b/__tests__/RouterView.spec.ts index b9397ab8..d728f829 100644 --- a/__tests__/RouterView.spec.ts +++ b/__tests__/RouterView.spec.ts @@ -24,7 +24,9 @@ const routes = createRoutes({ params: {}, hash: '', meta: {}, - matched: [{ components: { default: components.Home }, path: '/' }], + matched: [ + { components: { default: components.Home }, instances: {}, path: '/' }, + ], }, foo: { fullPath: '/foo', @@ -34,7 +36,9 @@ const routes = createRoutes({ params: {}, hash: '', meta: {}, - matched: [{ components: { default: components.Foo }, path: '/foo' }], + matched: [ + { components: { default: components.Foo }, instances: {}, path: '/foo' }, + ], }, nested: { fullPath: '/a', @@ -45,8 +49,8 @@ const routes = createRoutes({ hash: '', meta: {}, matched: [ - { components: { default: components.Nested }, path: '/' }, - { components: { default: components.Foo }, path: 'a' }, + { components: { default: components.Nested }, instances: {}, path: '/' }, + { components: { default: components.Foo }, instances: {}, path: 'a' }, ], }, nestedNested: { @@ -58,9 +62,9 @@ const routes = createRoutes({ hash: '', meta: {}, matched: [ - { components: { default: components.Nested }, path: '/' }, - { components: { default: components.Nested }, path: 'a' }, - { components: { default: components.Foo }, path: 'b' }, + { components: { default: components.Nested }, instances: {}, path: '/' }, + { components: { default: components.Nested }, instances: {}, path: 'a' }, + { components: { default: components.Foo }, instances: {}, path: 'b' }, ], }, named: { @@ -71,7 +75,9 @@ const routes = createRoutes({ params: {}, hash: '', meta: {}, - matched: [{ components: { foo: components.Foo }, path: '/' }], + matched: [ + { components: { foo: components.Foo }, instances: {}, path: '/' }, + ], }, withParams: { fullPath: '/users/1', @@ -84,6 +90,8 @@ const routes = createRoutes({ matched: [ { components: { default: components.User }, + + instances: {}, path: '/users/:id', props: true, }, @@ -100,6 +108,8 @@ const routes = createRoutes({ matched: [ { components: { default: components.WithProps }, + + instances: {}, path: '/props/:id', props: { id: 'foo', other: 'fixed' }, }, @@ -117,6 +127,8 @@ const routes = createRoutes({ matched: [ { components: { default: components.WithProps }, + + instances: {}, path: '/props/:id', props: to => ({ id: Number(to.params.id) * 2, other: to.query.q }), }, @@ -129,7 +141,13 @@ describe('RouterView', () => { function factory(route: RouteLocationNormalizedLoose, props: any = {}) { const router = { - currentRoute: ref(markNonReactive({ ...route })), + currentRoute: ref( + markNonReactive({ + ...route, + // reset the instances everytime + matched: route.matched.map(match => ({ ...match, instances: {} })), + }) + ), } const { app, el } = mount( diff --git a/__tests__/guards/component-beforeRouteLeave.spec.ts b/__tests__/guards/component-beforeRouteLeave.spec.ts index 99d4c913..f8867679 100644 --- a/__tests__/guards/component-beforeRouteLeave.spec.ts +++ b/__tests__/guards/component-beforeRouteLeave.spec.ts @@ -179,6 +179,11 @@ describe('beforeRouteLeave', () => { await p.catch(err => {}) // catch the navigation abortion expect(currentRoute.fullPath).toBe('/guard') }) + + it.todo('invokes with the component context') + it.todo('invokes with the component context with named views') + it.todo('invokes with the component context with nested views') + it.todo('invokes with the component context with nested named views') }) }) }) diff --git a/__tests__/guards/component-beforeRouteUpdate.spec.ts b/__tests__/guards/component-beforeRouteUpdate.spec.ts index 211decbf..f832179c 100644 --- a/__tests__/guards/component-beforeRouteUpdate.spec.ts +++ b/__tests__/guards/component-beforeRouteUpdate.spec.ts @@ -65,6 +65,11 @@ describe('beforeRouteUpdate', () => { await p expect(router.currentRoute.value.fullPath).toBe('/guard/foo') }) + + it.todo('invokes with the component context') + it.todo('invokes with the component context with named views') + it.todo('invokes with the component context with nested views') + it.todo('invokes with the component context with nested named views') }) }) }) diff --git a/__tests__/matcher/records.spec.ts b/__tests__/matcher/records.spec.ts index 0c078861..278452f6 100644 --- a/__tests__/matcher/records.spec.ts +++ b/__tests__/matcher/records.spec.ts @@ -12,6 +12,7 @@ describe('normalizeRouteRecord', () => { aliasOf: undefined, components: { default: {} }, leaveGuards: [], + instances: {}, meta: {}, name: undefined, path: '/home', @@ -34,6 +35,7 @@ describe('normalizeRouteRecord', () => { children: [{ path: '/child' }], components: { default: {} }, leaveGuards: [], + instances: {}, meta: { foo: true }, name: 'name', path: '/home', @@ -55,6 +57,7 @@ describe('normalizeRouteRecord', () => { aliasOf: undefined, components: {}, leaveGuards: [], + instances: {}, meta: { foo: true }, name: 'name', path: '/redirect', @@ -77,6 +80,7 @@ describe('normalizeRouteRecord', () => { children: [{ path: '/child' }], components: { one: {} }, leaveGuards: [], + instances: {}, meta: { foo: true }, name: 'name', path: '/home', @@ -95,6 +99,7 @@ describe('normalizeRouteRecord', () => { aliasOf: undefined, components: {}, leaveGuards: [], + instances: {}, meta: {}, name: undefined, path: '/redirect', diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 90d3b4a9..f9023308 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -30,6 +30,7 @@ export interface RouteRecordViewLoose 'path' | 'name' | 'components' | 'children' | 'meta' | 'beforeEnter' > { leaveGuards?: any + instances: Record props?: RouteRecordCommon['props'] aliasOf: RouteRecordViewLoose | undefined } @@ -53,6 +54,7 @@ export interface MatcherLocationNormalizedLoose { redirectedFrom?: Partial meta: any matched: Partial[] + instances: Record } declare global { diff --git a/playground/views/ComponentWithData.vue b/playground/views/ComponentWithData.vue index b5b1d5d1..a1598ac8 100644 --- a/playground/views/ComponentWithData.vue +++ b/playground/views/ComponentWithData.vue @@ -12,7 +12,7 @@ import { getData, delay } from '../api' export default defineComponent({ name: 'ComponentWithData', async setup() { - const data = reactive({ other: null }) + const data = reactive({ other: 'old' }) data.fromApi = await getData() // TODO: add sample with onBeforeRouteUpdate() diff --git a/src/components/View.ts b/src/components/View.ts index 2efcb593..3414a9dd 100644 --- a/src/components/View.ts +++ b/src/components/View.ts @@ -6,15 +6,16 @@ import { PropType, computed, InjectionKey, - Ref, + ref, + ComponentPublicInstance, + ComputedRef, } from 'vue' -import { RouteRecordNormalized } from '../matcher/types' import { routeKey } from '../injectKeys' -import { RouteComponent } from '../types' +import { RouteLocationMatched } from '../types' // TODO: make it work with no symbols too for IE export const matchedRouteKey = Symbol() as InjectionKey< - Ref + ComputedRef > export const View = defineComponent({ @@ -31,13 +32,16 @@ export const View = defineComponent({ const depth: number = inject('routerViewDepth', 0) provide('routerViewDepth', depth + 1) - const matchedRoute = computed(() => route.value.matched[depth]) - const ViewComponent = computed( + const matchedRoute = computed( + () => route.value.matched[depth] as RouteLocationMatched | undefined + ) + const ViewComponent = computed( () => matchedRoute.value && matchedRoute.value.components[props.name] ) const propsData = computed(() => { - const { props } = matchedRoute.value + // propsData only gets called if ViewComponent.value exists and it depends on matchedRoute.value + const { props } = matchedRoute.value! if (!props) return {} if (props === true) return route.value.params @@ -46,9 +50,22 @@ export const View = defineComponent({ provide(matchedRouteKey, matchedRoute) + const viewRef = ref() + + function onVnodeMounted() { + // if we mount, there is a matched record + matchedRoute.value!.instances[props.name] = viewRef.value + // TODO: trigger beforeRouteEnter hooks + } + return () => { return ViewComponent.value - ? h(ViewComponent.value as any, { ...propsData.value, ...attrs }) + ? h(ViewComponent.value as any, { + ...propsData.value, + ...attrs, + onVnodeMounted, + ref: viewRef, + }) : null } }, diff --git a/src/matcher/index.ts b/src/matcher/index.ts index e4fe9642..9a1888ff 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -284,6 +284,7 @@ export function normalizeRouteRecord( props: record.props || false, meta: record.meta || {}, leaveGuards: [], + instances: {}, aliasOf: undefined, } } diff --git a/src/matcher/types.ts b/src/matcher/types.ts index 34153c36..be6f6636 100644 --- a/src/matcher/types.ts +++ b/src/matcher/types.ts @@ -13,6 +13,8 @@ export interface RouteRecordNormalized { meta: Exclude props: Exclude beforeEnter: RouteRecordMultipleViews['beforeEnter'] - leaveGuards: NavigationGuard[] + leaveGuards: NavigationGuard[] + // TODO: should be ComponentPublicInstance but breaks Immutable type + instances: Record aliasOf: RouteRecordNormalized | undefined } diff --git a/src/router.ts b/src/router.ts index 54cd0b56..a482e92f 100644 --- a/src/router.ts +++ b/src/router.ts @@ -74,7 +74,7 @@ export interface Router { push(to: RouteLocation): Promise replace(to: RouteLocation): Promise - beforeEach(guard: NavigationGuard): ListenerRemover + beforeEach(guard: NavigationGuard): ListenerRemover afterEach(guard: PostNavigationGuard): ListenerRemover onError(handler: ErrorHandler): ListenerRemover @@ -94,7 +94,7 @@ export function createRouter({ }: RouterOptions): Router { const matcher = createRouterMatcher(routes, {}) - const beforeGuards = useCallbacks() + const beforeGuards = useCallbacks>() const afterGuards = useCallbacks() const currentRoute = ref( START_LOCATION_NORMALIZED @@ -275,6 +275,9 @@ export function createRouter({ for (const guard of record.leaveGuards) { guards.push(guardToPromiseFn(guard, to, from)) } + + // free the references + record.instances = {} } // run the queue of per route beforeRouteLeave guards diff --git a/src/types/index.ts b/src/types/index.ts index b42faf5a..dd7bd201 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { LocationQuery, LocationQueryRaw } from '../utils/query' import { PathParserOptions } from '../matcher/path-parser-ranker' -import { markNonReactive, ComponentOptions } from 'vue' +import { markNonReactive, ComponentOptions, ComponentPublicInstance } from 'vue' import { RouteRecordNormalized } from '../matcher/types' export type Lazy = () => Promise @@ -102,8 +102,9 @@ export interface RouteLocationNormalized { // } // TODO: type this for beforeRouteUpdate and beforeRouteLeave +// TODO: support arrays export interface RouteComponentInterface { - beforeRouteEnter?: NavigationGuard + beforeRouteEnter?: NavigationGuard /** * Guard called when the router is navigating away from the current route * that is rendering this component. @@ -111,7 +112,7 @@ export interface RouteComponentInterface { * @param from RouteLocation we are navigating from * @param next function to validate, cancel or modify (by redirectering) the navigation */ - beforeRouteLeave?: NavigationGuard + beforeRouteLeave?: NavigationGuard /** * Guard called whenever the route that renders this component has changed but * it is reused for the new route. This allows you to guard for changes in params, @@ -120,7 +121,7 @@ export interface RouteComponentInterface { * @param from RouteLocation we are navigating from * @param next function to validate, cancel or modify (by redirectering) the navigation */ - beforeRouteUpdate?: NavigationGuard + beforeRouteUpdate?: NavigationGuard } // TODO: allow defineComponent export type RouteComponent = (Component | ReturnType) & @@ -137,7 +138,7 @@ export interface RouteRecordCommon { | Record | ((to: RouteLocationNormalized) => Record) // TODO: beforeEnter has no effect with redirect, move and test - beforeEnter?: NavigationGuard | NavigationGuard[] + beforeEnter?: NavigationGuard | NavigationGuard[] meta?: Record // TODO: only allow a subset? // TODO: RFC: remove this and only allow global options @@ -223,7 +224,7 @@ export interface NavigationGuardCallback { export type NavigationGuardNextCallback = (vm: any) => any -export interface NavigationGuard { +export interface NavigationGuard { ( this: V, // TODO: we could maybe add extra information like replace: true/false diff --git a/src/utils/guardToPromiseFn.ts b/src/utils/guardToPromiseFn.ts index 7d1f7c9f..0d20bb8d 100644 --- a/src/utils/guardToPromiseFn.ts +++ b/src/utils/guardToPromiseFn.ts @@ -14,12 +14,21 @@ import { NavigationError, NavigationRedirectError, } from '../errors' +import { ComponentPublicInstance } from 'vue' export function guardToPromiseFn( - guard: NavigationGuard, + guard: NavigationGuard, to: RouteLocationNormalized, - from: RouteLocationNormalizedResolved - // record?: RouteRecordNormalized + from: RouteLocationNormalizedResolved, + instance?: undefined +): () => Promise +export function guardToPromiseFn< + ThisType extends ComponentPublicInstance | undefined +>( + guard: NavigationGuard, + to: RouteLocationNormalized, + from: RouteLocationNormalizedResolved, + instance: ThisType ): () => Promise { return () => new Promise((resolve, reject) => { @@ -52,6 +61,6 @@ export function guardToPromiseFn( } } - guard(to, from, next) + guard.call(instance, to, from, next) }) } diff --git a/src/utils/index.ts b/src/utils/index.ts index 6167d396..ec4c0a20 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -43,11 +43,16 @@ export function extractComponentsGuards( // replace the function with the resolved component record.components[name] = resolvedComponent const guard = resolvedComponent[guardType] - return guard && guardToPromiseFn(guard, to, from)() + return ( + // @ts-ignore: the guard matcheds the instance type + guard && guardToPromiseFn(guard, to, from, record.instances[name])() + ) }) } else { const guard = rawComponent[guardType] - guard && guards.push(guardToPromiseFn(guard, to, from)) + guard && + // @ts-ignore: the guard matcheds the instance type + guards.push(guardToPromiseFn(guard, to, from, record.instances[name])) } } } diff --git a/yarn.lock b/yarn.lock index 726f6a0f..77e84119 100644 --- a/yarn.lock +++ b/yarn.lock @@ -680,7 +680,7 @@ "@vue/compiler-core" "3.0.0-alpha.9" "@vue/shared" "3.0.0-alpha.9" -"@vue/compiler-sfc@3.0.0-alpha.9": +"@vue/compiler-sfc@latest": version "3.0.0-alpha.9" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.0-alpha.9.tgz#5d28d9d18fd0c4fb7fbe0e08f85f333fd7ecc8b1" integrity sha512-Wr4O0J/lO4Q5Li6RfhZFZNIuYlBkmhk6UxxgCWdW1iPko3/C/oI9/k2SBSiRQcGCE+J5N3l/x1elYlq77YHvHA== @@ -8737,7 +8737,7 @@ vue-loader@next: merge-source-map "^1.1.0" source-map "^0.6.1" -vue@^3.0.0-alpha.9: +vue@next: version "3.0.0-alpha.9" resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.0-alpha.9.tgz#f84b6b52caf6753a8cefda370bd6bbd298b5b06a" integrity sha512-zZrfbchyCQXF/+9B5fD4djlqzZ2XB39MxDzTaJKfuMjs/CgD2CTDiEVrOcP9HwMjr48cpARiFH13vvl4/F73RA==