]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: invoke guards with the right context
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 18 Mar 2020 23:08:27 +0000 (00:08 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 18 Mar 2020 23:08:27 +0000 (00:08 +0100)
14 files changed:
__tests__/RouterView.spec.ts
__tests__/guards/component-beforeRouteLeave.spec.ts
__tests__/guards/component-beforeRouteUpdate.spec.ts
__tests__/matcher/records.spec.ts
__tests__/utils.ts
playground/views/ComponentWithData.vue
src/components/View.ts
src/matcher/index.ts
src/matcher/types.ts
src/router.ts
src/types/index.ts
src/utils/guardToPromiseFn.ts
src/utils/index.ts
yarn.lock

index b9397ab8fb66856856ade12761c1f35d1f8816cf..d728f829aa31f71b389d1958e60b0c37843ee98c 100644 (file)
@@ -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(
index 99d4c913f757785960ab9dab568ab624d77ec2eb..f88676792c727d203fd2fa360430e1a9d75def86 100644 (file)
@@ -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')
     })
   })
 })
index 211decbfcfee3acb0ce417d95cc17dcd690960d4..f832179c23f5ce698a5974f4d30bd38953d60184 100644 (file)
@@ -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')
     })
   })
 })
index 0c07886164e8aff28eba3aa4e835206a5b18c0cd..278452f6d8211c68a681a9edfc806c293c091a15 100644 (file)
@@ -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',
index 90d3b4a9525b9215c0ce5dfff91c619e621e098b..f9023308e5a4bf3b1865f93ca85bdb64e0ccd028 100644 (file)
@@ -30,6 +30,7 @@ export interface RouteRecordViewLoose
     'path' | 'name' | 'components' | 'children' | 'meta' | 'beforeEnter'
   > {
   leaveGuards?: any
+  instances: Record<string, any>
   props?: RouteRecordCommon['props']
   aliasOf: RouteRecordViewLoose | undefined
 }
@@ -53,6 +54,7 @@ export interface MatcherLocationNormalizedLoose {
   redirectedFrom?: Partial<MatcherLocationNormalized>
   meta: any
   matched: Partial<RouteRecordViewLoose>[]
+  instances: Record<string, any>
 }
 
 declare global {
index b5b1d5d148d488985660e63cceff636e3fc34948..a1598ac8f01df8fb8952502066dd3d656642f167 100644 (file)
@@ -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()
index 2efcb593e4d5d8cd56ea19960da282ec3ddf42dd..3414a9ddbc565bd8533090d1406cc6bb567e11df 100644 (file)
@@ -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<RouteRecordNormalized>
+  ComputedRef<RouteLocationMatched | undefined>
 >
 
 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<RouteComponent | undefined>(
+    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<ComponentPublicInstance>()
+
+    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
     }
   },
index e4fe9642c61bca36f991cc53c87dbe045bbef818..9a1888ffc59e2f231670139f256eb80298b37d51 100644 (file)
@@ -284,6 +284,7 @@ export function normalizeRouteRecord(
     props: record.props || false,
     meta: record.meta || {},
     leaveGuards: [],
+    instances: {},
     aliasOf: undefined,
   }
 }
index 34153c368c60b2bb9ab6c2aafc3cd0c95cc11e48..be6f66363d3480e76267598e431443e624c0d1e1 100644 (file)
@@ -13,6 +13,8 @@ export interface RouteRecordNormalized {
   meta: Exclude<RouteRecordMultipleViews['meta'], void>
   props: Exclude<RouteRecordCommon['props'], void>
   beforeEnter: RouteRecordMultipleViews['beforeEnter']
-  leaveGuards: NavigationGuard[]
+  leaveGuards: NavigationGuard<undefined>[]
+  // TODO: should be ComponentPublicInstance but breaks Immutable type
+  instances: Record<string, {} | undefined | null>
   aliasOf: RouteRecordNormalized | undefined
 }
index 54cd0b568d10518a6f7e3c621eec5392386182eb..a482e92f48ec0268868f296c55b36accda5505e8 100644 (file)
@@ -74,7 +74,7 @@ export interface Router {
   push(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
   replace(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
 
-  beforeEach(guard: NavigationGuard): ListenerRemover
+  beforeEach(guard: NavigationGuard<undefined>): 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<NavigationGuard>()
+  const beforeGuards = useCallbacks<NavigationGuard<undefined>>()
   const afterGuards = useCallbacks<PostNavigationGuard>()
   const currentRoute = ref<RouteLocationNormalizedResolved>(
     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
index b42faf5aff517f91a5aad1afdaaa9bbb710ce29d..dd7bd2016ad56a287367d8505b84950641ccd30a 100644 (file)
@@ -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<T> = () => Promise<T>
@@ -102,8 +102,9 @@ export interface RouteLocationNormalized {
 // }
 
 // TODO: type this for beforeRouteUpdate and beforeRouteLeave
+// TODO: support arrays
 export interface RouteComponentInterface {
-  beforeRouteEnter?: NavigationGuard<void>
+  beforeRouteEnter?: NavigationGuard<undefined>
   /**
    * 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<void>
+  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<void>
+  beforeRouteUpdate?: NavigationGuard
 }
 
 // TODO: allow defineComponent export type RouteComponent = (Component | ReturnType<typeof defineComponent>) &
@@ -137,7 +138,7 @@ export interface RouteRecordCommon {
     | Record<string, any>
     | ((to: RouteLocationNormalized) => Record<string, any>)
   // TODO: beforeEnter has no effect with redirect, move and test
-  beforeEnter?: NavigationGuard | NavigationGuard[]
+  beforeEnter?: NavigationGuard<undefined> | NavigationGuard<undefined>[]
   meta?: Record<string | number | symbol, any>
   // 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<V = void> {
+export interface NavigationGuard<V = ComponentPublicInstance> {
   (
     this: V,
     // TODO: we could maybe add extra information like replace: true/false
index 7d1f7c9ffa53a84e7a10aacfe936e3857f770c46..0d20bb8d67595729d9c7928384075f349328c630 100644 (file)
@@ -14,12 +14,21 @@ import {
   NavigationError,
   NavigationRedirectError,
 } from '../errors'
+import { ComponentPublicInstance } from 'vue'
 
 export function guardToPromiseFn(
-  guard: NavigationGuard,
+  guard: NavigationGuard<undefined>,
   to: RouteLocationNormalized,
-  from: RouteLocationNormalizedResolved
-  // record?: RouteRecordNormalized
+  from: RouteLocationNormalizedResolved,
+  instance?: undefined
+): () => Promise<void>
+export function guardToPromiseFn<
+  ThisType extends ComponentPublicInstance | undefined
+>(
+  guard: NavigationGuard<ThisType>,
+  to: RouteLocationNormalized,
+  from: RouteLocationNormalizedResolved,
+  instance: ThisType
 ): () => Promise<void> {
   return () =>
     new Promise((resolve, reject) => {
@@ -52,6 +61,6 @@ export function guardToPromiseFn(
         }
       }
 
-      guard(to, from, next)
+      guard.call(instance, to, from, next)
     })
 }
index 6167d396a7c09a9132eb151f1e8bc007f57cd851..ec4c0a20eccfdbefee379ac2c66c969e22fcd639 100644 (file)
@@ -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]))
       }
     }
   }
index 726f6a0f4a10b35c51a3ffd4f11eff61648280b8..77e841197f15bdf4e5e62e5eea24ae2bcb3edd96 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@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==