Skip to content
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

Component guards not working when defined on mixins #454

Open
AgileInteractive opened this issue Sep 7, 2020 · 23 comments
Open

Component guards not working when defined on mixins #454

AgileInteractive opened this issue Sep 7, 2020 · 23 comments
Labels
external This depends on an external dependency but is kept opened to track it help wanted Extra attention is needed

Comments

@AgileInteractive
Copy link

Version

4.0.0-beta.9

Reproduction link

https://jsfiddle.net/ukeagtjm/

Steps to reproduce

const Mixin = {
    beforeRouteEnter(to, from, next) {
        console.log('****** Before route enter (mixin)');
        next();
    }
};

...

mixins:[Mixin]

What is expected?

beforeRouteEnter() to run and log from mixin

What is actually happening?

beforeRouteEnter() is never executed

@posva
Copy link
Member

posva commented Sep 7, 2020

It wasn't intentionally removed but I will have to check a way to apply the mixins from components and its extends option

@posva posva added external This depends on an external dependency but is kept opened to track it help wanted Extra attention is needed labels Sep 11, 2020
@dmitrystas
Copy link

the same for beforeRouteUpdate and beforeRouteLeave events

@n10000k
Copy link

n10000k commented Nov 17, 2020

On the same topic has anything changed with beforeEnter because I do a simple console log, it's hitting it x2 and also doesn't work properly with Vuex getters anymore, define a const within beforeEnter = game over.

@posva posva changed the title beforeRouteEnter not supposed to work in mixin anymore? Component guards not working when defined on mixins Nov 27, 2020
@vuejs vuejs deleted a comment from RealAlphabet Dec 7, 2020
@webigorkiev
Copy link

Are there any possibilities or some workaround for solving this issue?

@webigorkiev
Copy link

webigorkiev commented Jun 30, 2021

Router hooks like beforeRouteLeave, beforeRouteUpdate, beforeRouteEnter do not resolve in this. $options.
This can be fixed if vue-router provides a strategy for resolveMergedOptions (internalOptionMergeStrats)

app.config.optionMergeStrategies.beforeRouteEnter = mergeAsArray;
app.config.optionMergeStrategies.beforeRouteUpdate = mergeAsArray;
app.config.optionMergeStrategies.beforeRouteLeave= mergeAsArray;

If you do this in this.$options everything resolves, but still doesn't work (only the method from the component is executed, not from the mixin)

@BARNZ
Copy link

BARNZ commented Jul 27, 2021

Bummer! Ran into this issue today as well. My use-case is to have my in-component guards placed into a page mixin for easy reuse across multiple similar page components.

This worked just fine in vue router 3. Either this is simply a regression or such a use case is no longer supported? Seems odd that it would be the latter case when mixins are still a supported feature in vue 3?

@dpmango
Copy link

dpmango commented Aug 5, 2021

Also experiencing the same issue.
Moreover, seems to be beforeRouteEnter, etc also not working at component level.

For example using in App.vue console output is empty

...
  data: () => ({}),
  beforeRouteEnter(from, to, next) {
    console.log('beforeRouteEnter') // not getting triggered
    next()
  },
  computed: {
...

@LinusBorg
Copy link
Member

@dpmango That's not supposed to work. Route guards only work when used in route components, not just any components.

@TothingWay
Copy link

Will the problem be fixed?

@jsweber
Copy link

jsweber commented Nov 18, 2021

You can use my solution
step one:copy this code as a javascript file

mergeRouterGuard.js

export const NeedMergeGuards = ['beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave']

export function mergeFuncUtil (nextHook, prevHook) {
    return function (to, from, next) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const ctx = this
        function reduceNext () {
            return prevHook.bind(ctx)(to, from, next)
        }
        return nextHook.bind(ctx)(to, from, reduceNext)
    }
}
/** execution order:  the guard in component is executed last one
 * @param distOption
 * {
        name: 'test',
        mixins: [myMixin],
        beforeRouteEnter (to, from, next) { next(this => {}) },
        beforeRouteUpdate (to: any, from: any, next: Function) { next() },
        beforeRouteLeave (to, from, next) { next() }}
    }
 * @param customMixins
    [
        {
            beforeRouteEnter: Function
        },
        {
            beforeRouteEnter: Function
        }
    ]
 * @param needMergeHooks ['beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave']
 * @returns
 * {
        name: 'test',
        mixins: [myMixin],
        beforeRouteEnter (to, from, next) { allNext(this => {}) },
        beforeRouteUpdate (to, from, next) { allNext() },
        beforeRouteLeave (to, from, next) { allNext() }
 * }
 */
export const mergeRouterGuard = (
    distOption,
    customMixins,
    needMergeGuards = NeedMergeGuards
) => {
    needMergeGuards.forEach((mergeGuard) => {
        const customMergeGuards = customMixins
            .filter(customMixin => customMixin[mergeGuard])
            .map(customMixin => customMixin[mergeGuard])

        if (customMergeGuards.length > 0) {
            const customFunc = customMergeGuards.reduce(function (mergeFunc, customMergeGuard) {
                const fn = mergeFuncUtil(mergeFunc, customMergeGuard)
                return fn
            })
            const finalHook = !distOption[mergeGuard] ? customFunc
                : mergeFuncUtil(customFunc, distOption[mergeGuard])

            distOption[mergeGuard] = function (...args) {
                return finalHook.bind(this)(...args)
            }
        }
    })

    return distOption
}

step two:
import mergeRouterGuard into component

step three: use in component, give an example

Test.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { myMixin } from './myMixin'
import { myMixin2 } from './myMixin2'
import { mergeRouterGuard } from '../mergeRouterGuard'

const mixins = [ myMixin, myMixin2 ]

// vue option
const option = {
    name: 'test',
    data () {
        return {
            msg: 'test'
        }
    },
    mixins:,           //  important !!!!!!!,  mergeRouteGuard only merge router guard
    beforeRouteEnter (to, from, next) {
        console.log('beforeRouteEnter')
        next((...args) => console.log(args))
    },
    beforeRouteUpdate (to, from, next) {
        console.log('---test.vue---------from test.vue-----test.vue-----')
        console.log(this)
        next((...args) => console.log(args))
    },
    beforeRouteLeave (to, from, next) {
        console.log('beforeRouteLeave')
        next((...args) => console.log(args))
    }
}
// merge router guard
const mergeRouterGuardOption = mergeRouterGuard(option, mixins)

export default defineComponent(mergeRouterGuardOption)
</script>

if you has bug, can connect me

@Thanayaby

This comment has been minimized.

@turbobuilt
Copy link

turbobuilt commented Dec 30, 2021

Hey everybody, I found a solution. I hope it helps everybody out. In your mixin, you can create a function called created which will run immediately after the component is created. Then, you can add in the hook by inspecting the $route variable on this:

const mixin = {
    created(){
        let route = this.$route.matched[0];
        if(!route) 
            return;
        route.leaveGuards = new Set([function(to, from, next){ 
            console.log("to", to, "from", from, "next", next);
            next();
        }]);
    }
}

chme added a commit to chme/forked-daapd that referenced this issue Jan 2, 2022
vue-router does not support navigation guards in mixins: <vuejs/router#454>

For now remove the mixin and duplicate the code in every component (keep this branch rebaseable).
In the future, this is a candidate for using the composition api: <https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards>
@darrinmn9
Copy link

darrinmn9 commented Feb 10, 2022

Now that Vue3 is now the default, would someone from the vue-router core team be willing to speak on this issue? This seems to be the only breaking change mentioned in the migration guide with no reasonable upgrade path. The migration guide even links to this open issue to "track its support". It seems some people on this thread have found reasonable workarounds, but will support ever added to vue-router core?

Also, its okay to say "no we wont support it". That's a fine response, but it would be great to get clarity and update the migration guide to mention this.

chme added a commit to chme/forked-daapd that referenced this issue Feb 12, 2022
vue-router does not support navigation guards in mixins: <vuejs/router#454>

For now remove the mixin and duplicate the code in every component (keep this branch rebaseable).
In the future, this is a candidate for using the composition api: <https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards>
@tsiotska
Copy link

Faced same issue, i use vue-property-decorator and firstly thought that issue lies in it.
Btw, fixed it moving logic to another function in mixin that invokes navigation guard from component.

// Component
 beforeRouteLeave(to, from, next) {
   this.onLeaveGuard(to, from, next)
 }
 // Mixin
 onLeaveGuard(to, from, next) {
    console.log('mixin hook');
    if (this.isDataChanged) {
      this.toggleOnLeaveModalVisibility({ to, next });
    } else {
      next()
    }
  }

@abellion
Copy link

If the goal of using mixins is to extract the logic of your navigation guards, maybe this library can serve as a workaround : https://github.com/abellion/vue-router-shield (disclaimer : I'm the creator of this library)

@leboeuf
Copy link

leboeuf commented Aug 31, 2022

The workaround we found was to import the mixin with a spread operator instead. This method requires only a single line change in the components that use the mixin.

navigationGuardsMixin.js

export default {
    beforeRouteEnter(to, from, next) {
        next(vm => {
            // Your logic that has access to the component instance
        });
    },
    beforeRouteUpdate(to, from, next) {
        // Your logic
        next();
    },
    beforeRouteLeave(to, from, next) {
        // Your logic
        next();
    }
}

YourComponent.vue

import navigationGuardsMixin from '@/mixins/navigationGuardsMixin';

export default {
    mixins: [navigationGuardsMixin], // old (remove this line)
    ...navigationGuardsMixin,        // new (add this line)
    // The rest of your component here
};

MetRonnie added a commit to MetRonnie/cylc-ui that referenced this issue Feb 2, 2023
@vedmant
Copy link

vedmant commented Feb 28, 2023

So it's still not fixed? I'm migrating my app and beforeRouteLeave doesn't from from mixins. What's official migration guide for this?

I used following workaround for now:

  mixins: [formPageMixin],

  beforeRouteLeave (...args) {
    return formPageMixin.beforeRouteLeave.apply(this, ...args)
  },

MetRonnie added a commit to MetRonnie/cylc-ui that referenced this issue Mar 3, 2023
@stratdev3
Copy link

a couple of hours trying to find why's not working since migrating the all projet from vue2 to vue3. Yeah it's in the migration guide but flooded in the content... Yet, mixins are an extremly usefull feature.

Thanks @leboeuf for the workaround

@gdutwyg
Copy link

gdutwyg commented Oct 19, 2023

So it's still not fixed? I'm migrating my app and beforeRouteLeave doesn't from from mixins. What's official migration guide for this?

I used following workaround for now:

  mixins: [formPageMixin],

  beforeRouteLeave (...args) {
    return formPageMixin.beforeRouteLeave.apply(this, ...args)
  },

it should be return formPageMixin.beforeRouteLeave.apply(this, args), that's right

@CCBaxter84
Copy link

You can also do it this way -- just define the nav guard as an object containing a method where both are named for the nav guard like so:

import mixin from "./mixin.js"
import beforeRouteLeave from "./routeGuard.js"
export default {
  mixins: [mixin],
  ...beforeRouteLeave
}
const beforeRouteLeave = {
  beforeRouteLeave(to, from, next) {
    // logic goes here
  }
}

@tbl0605
Copy link

tbl0605 commented May 6, 2024

a couple of hours trying to find why's not working since migrating the all projet from vue2 to vue3. Yeah it's in the migration guide but flooded in the content... Yet, mixins are an extremly usefull feature.

Thanks @leboeuf for the workaround

Hi, first of all, thank you for this great library! :)
This is the only issue I really have to complain about for now. So, will this (big) missing feature ever be fixed?

@jacobg
Copy link

jacobg commented Jul 23, 2024

Hey everybody, I found a solution. I hope it helps everybody out. In your mixin, you can create a function called created which will run immediately after the component is created. Then, you can add in the hook by inspecting the $route variable on this:

const mixin = {
    created(){
        let route = this.$route.matched[0];
        if(!route) 
            return;
        route.leaveGuards = new Set([function(to, from, next){ 
            console.log("to", to, "from", from, "next", next);
            next();
        }]);
    }
}

I think you need to use:

const route = this.$route.matched.slice(-1)[0]

@tbl0605
Copy link

tbl0605 commented Sep 20, 2024

Hi,
I'm adding my own comment (and solution) to workaround this issue, maybe it could be useful to other people.
I tried some of the solutions given and they worked fine as long as the mixins don't use beforeRouteEnter(to, from, next) with a callback function:

beforeRouteEnter (to, from, next) {
  next(vm => {
    // access to component instance via `vm`
  })
}

The v3.x documentation about navigation guards doesn't say much about multiple callback function calls, but to make it short:
when a beforeRouteEnter(to, from, next) in-component guard uses a callback function, that guard's navigation is confirmed but the callback function is stacked and called later, after all other beforeRouteEnter() in-component guards have successfully confirmed their navigation.

Example 1:

export default {
  mixins: [
    // Mixin 1, called first
    {
      beforeRouteEnter(to, from, next) {
        next();
      }
    },
    // Mixin 2, called second
    {
      beforeRouteEnter(to, from, next) {
        next(
          // Call delayed after mixin 4
          function callback1(vm) {}
        );
      }
    },
    // Mixin 3, called in third
    {
      beforeRouteEnter(to, from, next) {
        next(
          // Called delayed after mixin 4 and after function callback1
          function callback2(vm) {}
        );
      }
    },
    // Mixin 4, called in fourth
    {
      beforeRouteEnter(to, from, next) {
        next();
      }
    }
  ]
};

Example 2:

export default {
  mixins: [
    // Mixin 1, called first
    {
      beforeRouteEnter(to, from, next) {
        next();
      }
    },
    // Mixin 2, called second
    {
      beforeRouteEnter(to, from, next) {
        next(
          // Never called, because mixin 4 didn't confirm the navigation
          function callback1(vm) {}
        );
      }
    },
    // Mixin 3, called in third
    {
      beforeRouteEnter(to, from, next) {
        next(
          // Never called, because mixin 4 didn't confirm the navigation
          function callback2(vm) {}
        );
      }
    },
    // Mixin 4, called in fourth
    {
      beforeRouteEnter(to, from, next) {
        next(false);
      }
    }
  ]
};

Here is my solution:

mergeMixinGuards.js:

// Taken from https://github.com/vuejs/router/blob/main/packages/router/src/types/typeGuards.ts
function isRouteLocation(route) {
  return typeof route === 'string' || (route && typeof route === 'object');
}

function mergeGuards(to, from, next, context, guards, callbacks = []) {
  guards.shift().call(context, to, from, nextArg => {
    // See guardToPromiseFn() in https://github.com/vuejs/router/blob/main/packages/router/src/navigationGuards.ts
    if (nextArg === false || nextArg instanceof Error || isRouteLocation(nextArg)) {
      next(nextArg);
      return;
    }
    if (typeof nextArg === 'function') {
      callbacks.push(nextArg);
    }
    if (guards.length > 0) {
      mergeGuards(to, from, next, context, guards, callbacks);
      return;
    } else if (callbacks.length > 0) {
      next(vm => callbacks.forEach(callback => callback(vm)));
      return;
    }
    next();
  });
}

export function mergeMixinGuards(optionsApi) {
  if (!optionsApi?.mixins || optionsApi.mixins.length == 0) {
    return optionsApi;
  }
  const allApis = [...optionsApi.mixins, optionsApi];
  for (const guardName of ['beforeRouteEnter', 'beforeRouteUpdate', 'beforeRouteLeave']) {
    const guards = [];
    for (const api of allApis) {
      if (api[guardName]) {
        guards.push(api[guardName]);
      }
    }
    if (guards.length == 0) {
      continue;
    }
    optionsApi[guardName] =
      guards.length == 1
        ? guards[0]
        : function (to, from, next) {
            mergeGuards(to, from, next, this, [...guards]);
          };
  }
  return optionsApi;
}

And simply wrap your component with mergeMixinGuards(...) without any further changes:

import { mergeMixinGuards } from './mergeMixinGuards';
export default mergeMixinGuards({
  name: 'MyComponent',
  mixins: [...],
  beforeRouteEnter(to, from, next) {
    next(vm => {
      // Callback function (mixins can have some too)
    });
  },
  ...
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
external This depends on an external dependency but is kept opened to track it help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests