Conditionally import a component in Vue Router - vue.js

I'd like to conditionnaly import a component in the vue router. Here is what I have for the moment:
children: [
{
path: ':option',
component: () => import('../components/Option1.vue'),
},
],
Depending on what :option is, I want to import a different component (Option1.vue, Option2.vue, etc.). I know I could put several children but i actually need the option variable in my parent component (I make tests if the route has an option).
How would it be possible to do that?
Thanks in advance :)

You can create a loader component containing a dynamic component instead of doing conditional routing. In the loader, you'll conditionally lazy load the option component based on the route param. Not only is this easier when routing, you also don't have to manually import anything, and only options that are used will be imported.
Step 1. Route to the option loader component
router
{
path: ':option',
component: () => import('../components/OptionLoader.vue'),
}
Step 2. In that option loader template, use a dynamic component which will be determined by a computed called optionComponent:
OptionLoader.vue
<template>
<component :is="optionComponent" />
</template>
Step 3. Create a computed that lazy loads the current option
OptionLoader.vue
export default {
computed: {
optionComponent() {
return () => import(`#/components/Option${this.$route.params.option}.vue`);
}
}
}
This will load the component called "Option5.vue", for example, when the option route param is 5. Now you have a lazy loaded option loader and didn't have to manually import each option.
Edit: OP has now indicated that he's using Vue 3.
Vue 3
For Vue 3, change the computed to use defineAsyncComponent:
OptionsLoader.vue
import { defineAsyncComponent } from "vue";
computed: {
optionComponent() {
return defineAsyncComponent(() =>
import(`#/components/Option${this.$route.params.option}.vue`)
);
}
}

Here is something that works in VueJS3:
<template>
<component :is="userComponent"/>
</template>
<script>
import { defineAsyncComponent } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export default {
computed: {
userComponent() {
const route = useRoute();
const router = useRouter();
const components = {
first: 'Option1',
second: 'Option2',
third: 'OtherOption',
fourth: 'DefaultOption',
};
if (components[route.params.option]) {
return defineAsyncComponent(() => import(`./options/${components[route.params.option]}.vue`));
}
router.push({ path: `/rubrique/${route.params.parent}`, replace: true });
return false;
},
},
};
</script>
Source: https://v3-migration.vuejs.org/breaking-changes/async-components.html
And it's possible to get an error message like this one for the line with "return":
Syntax Error: TypeError: Cannot read property 'range' of null
In that case, it means you probably want to migrate from babel-eslint to #babel/eslint-parser (source: https://babeljs.io/blog/2020/07/13/the-state-of-babel-eslint#the-present)

Related

What are the differences between vue-hooks and vue-property-decorator?

I want to separate my logic from the component in vue.
What I have in mind is this:
One file for HTML template, the data pass as props.
Another file that has a lot of functions and getters that gets the data from the store/API.
So after I search a lot I understand I need something like "hooks in reacts".
What I find is u3u/vue-hooks.
My question is what are the benefits to use this idea/library? cause it seems like I do the same with and without vue-hooks.
for example:
foo.ts:
import { computed, defineComponent } from '#vue/composition-api';
export default defineComponent({
name: 'foo',
props: {
icon: {
type: String,
default: '',
},
},
setup(props) {
const iconName = computed(() => props.icon);
return {
iconName,
};
},
});
and foo.vue:
<template>
<div>{{iconName}}</div>
</template>
<script lang="ts" src="./foo.ts">
</script>
<style lang="scss">
</style>
So until here, I separate the logic from the component and I can choose which file to attach the view.
But I can do it with the class as well.
just to change the foo.ts file to:
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Foo extends Vue {}
I would like it if anyone explains to me - if hooks are the way to separate the logic from the UI? and should I use vue-hooks to do it?

How to use vuex action in function

I'm new to Vue, so it's likely I misunderstand something. I want to call a vuex action inside a local function in App.vue like so:
<template>
<div id="app">
<button #click="runFunction(1)">Test</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default{
data() { return { } },
methods: {
...mapActions(['doAction']),
buttonClicked: (input) => { runFunction(input) }
}
}
function runFunction(input){
doAction({ ID: input });
}
</script>
The action calls a mutation in store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
IDs: []
},
mutations: {
doAction: (state, id) => { state.IDs.push(id) }
},
actions: {
doAction: ({ commit }, id) => { commit('doAction', id) }
}
})
I also have a main.js that sets up the vue:
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
The error I'm getting is:
ReferenceError: doAction is not defined
at runFunction
How can I call the mapped action inside a function? Version is Vue 2.6.10
There are several problems with defining runFunction as a 'local function':
function runFunction(input){
doAction({ ID: input });
}
Firstly, this is just a normal JavaScript function and the usual scoping rules apply. doAction would need to be defined somewhere that this function can see it. There is no magic link between this function and the component defined in App.vue. The function will be accessible to code in the component, such as in buttonClicked, but not the other way around.
The next problem is that it won't be available within your template. When you write runTemplate(1) in your template that's going to be looking for this.runTemplate(1), trying to resolve it on the current instance. Your function isn't on the current instance. Given your template includes #click="runFunction(1)" I'm a little surprised you aren't seeing a console error warning that the click handler is undefined.
mapActions accesses the store by using the reference held in this.$store. That reference is created when you add the store to your new Vue({store}). The store may appear to be available by magic but it's really just this.$store, where this is the current component.
It isn't really clear why you're trying to write this function outside of the component. The simplest solution is to add it to the methods. It'll then be available to the template and you can access doAction as this.doAction.
To keep it as a separate function you'd need to give it some sort of access to the store. Without knowing why you want it to be separate in the first place it's unclear how best to achieve that.
Of course it is not defined outside your instance .... you have to import the exported store from store.js on your function component :
<script>
import { mapActions } from 'vuex'
import store from 'store.js'
export default{
data() { return { } },
methods: {
...mapActions(['doAction']),
buttonClicked: (input) => { runFunction(input) }
}
}
function runFunction(input){
store.commit({ ID: input });
}
</script>

Sharing data between components in vue.js

I got an array of data in one component which I want to access in another component but cannot get it right
My idea was to just import component one in component two and thought I could access the data in that way but it didnt work.
here is what I got so far ...
Component 1:
export default {
data() {
return {
info: [
{
id: 1,
title: "Title One"
},
{
id: 2,
title: "Title Two"
},
Component 2:
<template>
<div>
<div v-for="item in info" v-bind:key="item.id">
<div>{{ item.title }} </div>
</div>
</div>
</template>
<script>
import ComponentOne from "../views/ComponentOne ";
export default {
components: {
ComponentOne
}, But after this I am a bit lost
Can anyone point my to the right direction it would be very much appreciated!
In order to access shared data, the most common way is to use Vuex. I'll get you going with the super basics with a module system as it does take a little reading.
npm install vuex --save
Create new folder called store in the src directory.
src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import example from './modules/example'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
example // replace with whatever you want to call it
}
})
src/main.js
// add to your imports
import store from './store/index'
...
// Change your Vue instance init
new Vue({
router,
store, // <--- this bit is the thing to add
render: h => h(App)
}).$mount('#app')
/src/store/modules/example.js
// initial state
const state = {
info: []
}
// getters
const getters = {}
// actions
const actions = {
}
// mutations
const mutations = {
set (state, newState) {
state.info.splice(0)
state.info.push.apply(state.info, newState)
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
To update the store when you get info, from any component you can use this.$store.commit('example/set', infoArray) where the first parameter follows the pattern of module name/mutation function name, and the second parameter is the 'new state' that you want updated.
To access the data from the store, you can access it from your components as a computed property:
computed: {
info () {
return this.$store.state.example.info
}
}
Obviously you can use getters and actions and other stuff, but this will get you going and you can read up and modify the Vuex store once you get comfortable and understand how it works.
Let's say if you do not want to use any other state management like vuex then you can share with the use of mixins.
Well, you can achieve it with the use of Vue.mixins.
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixins will be “mixed” into the component’s own options.
Mixins official docs
Hope this helps!

Vuex module state undefined

I'm a little confused over vuex modules.
I have a Vue component, which I show on a search page:
<template>
<div v-if="filtersPanelActive">
Filters panel
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: {
...mapState(["filtersPanelActive"])
},
methods: {}
};
</script>
I then have a store.js
import Vue from "vue";
import Vuex from "vuex";
import searchPage from "./modules/searchPage.js";
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
artPage,
legalsPage,
searchPage
}
});
and a search page store:
const searchPage = {
state: {
filtersPanelActive: false
},
actions: {},
mutations: {
toggleFilters(state) {
state.filtersPanelActive = !state.filtersPanelActive;
}
},
getters: {}
};
export default searchPage;
But nothing is quite working... In my vuex dev tools I have the following:
{
"artPage":{
"startDate":false,
"endDate":false,
"confirmBooking":false
},
"legalsPage":{
"filesUploaded":[
]
},
"searchPage":{
"filtersPanelActive":false
}
}
But when I inspect the Vue component it shows the following:
Why do I get undefined? I'm slightly overwhelmed by vuex, namespaces, modules etc, so feel free to over explain...
In another component I use
this.$store.commit("toggleFilters");
And its worth noting that that 'works' – as in the vuex state changes from true to false, however my mapState still isn't pulling through anything. So the module seems to be loaded correctly.
2018-06-05 MINOR UPDATE: Updated the mapState code to something slightly simpler.
Two things to add to your code to enable namespaces and to make sure you are referencing the right module.
In your searchPage module, add the namespaced: true property to the export as such:
const searchPage = {
namespaces: true,
state: { ... },
...
}
This enables namespacing for the module and makes it self contained and reusable.
Then in your search page component change your mapState to this:
computed: {
...mapState('searchPage', [ 'filtersPanelActive' ])
}
This will map the component variable filtersPanelActive to your searchPage module state variable filtersPanelActive. Your v-if="filtersPanelActive" line in your template should work now.
Let me know if this fixes things for you and if it doesn't please let me know and I will update my answer.

Best Practice for Reacting to Params Changes with Vue Router

When using Vue Router with routes like /foo/:val you have to add a watcher to react for parameter changes. That results in somewhat annoying duplicate code in all views that have parameters in the URL.
This could look like the following example:
export default {
// [...]
created() {
doSomething.call(this);
},
watch: {
'$route' () {
doSomething.call(this);
}
},
}
function doSomething() {
// e.g. request API, assign view properties, ...
}
Is there any other way to overcome that? Can the handlers for created and $route changes be combined? Can the reuse of the component be disabled so that the watcher would not be necessary at all? I am using Vue 2, but this might be interesting for Vue 1, too.
One possible answer that I just found thanks to a GitHub issue is the following.
It is possible to use the key attribute that is also used for v-for to let Vue track changes in the view. For that to work, you have to add the attribute to the router-view element:
<router-view :key="$route.fullPath"></router-view>
After you add this to the view, you do not need to watch the $route anymore. Instead, Vue.js will create a completely new instance of the component and also call the created callback.
However, this is an all-or-nothing solution. It seems to work well on the small application that I am currently developing. But it might have effects on performance in another application. If you really want to disable the reuse of the view for some routes only, you can have a look at setting the key's value based on the route. But I don't really like that approach.
I used this variant without :key prop on router-view component.
routes.js:
{
path: 'url/:levels(.*)',
name: ROUTES.ANY_LEVEL,
props: true,
component: (): PromiseVue => import('./View.vue'),
},
view.vue
<template>
<MyComponent :config="config" />
</template>
---*****----
<script>
data: () => ({ config: {} }),
methods: {
onConfigurationChanged(route) {
const { params } = route
if (params && params.levels) {
this.config = // some logic
} else {
this.config = null
}
},
},
beforeRouteUpdate(to, from, next) {
this.onConfigurationChanged(to)
next()
},
}
</script>
Inside the component, I use the config as a property. In my case, reactivity is preserved and the component is updated automatically from parameter changes inside the same URL.
Works on Vue 2
vue3 and script setup:
watch(route, () => { fetch()})
in import:
import { watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()