I'm building out a vuetify/nuxt frontend for the first time, and I've moved my v-navigation-drawer component out of the default.vue layout, and into it's own component, so that it can be reused in multiple layouts.
The activator for this drawer still remains in the default.vue component, so I added a sidebar state to vuex:
export const state = () => ({
baseurl: 'http://example.com/api/',
sidebar: false,
authenticated: false,
token: null,
user: null,
})
The mutator for the sidebar looks like so:
export const mutations = {
toggleSidebar(state) {
state.sidebar = !state.sidebar;
}
}
This works perfectly when opening the drawer, but because the drawer is dismissed via clicking the overlay, or clicking off of sidebar (if you've turned the overlay off) vuex throws a huge error:
How can I make this work correctly through vuex?
Instead of binding the drawer's model directly to $store.state.sidebar, use a computed setter in the drawer component. Note that you must pass in the new value from the drawer itself, don't just toggle whatever's already in the store.
<template>
<v-navigation-drawer v-model="drawer" ...>
</template>
<script>
export default {
computed: {
drawer: {
get () {
return this.$store.state.sidebar
},
set (val) {
this.$store.commit('sidebar', val)
}
}
}
}
</script>
// vuex mutation
sidebar (state, val) {
state.sidebar = val
}
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter
https://vuex.vuejs.org/en/forms.html
Another option is to bind the prop and event separately
<template>
<v-navigation-drawer :value="$store.state.sidebar" #input="$store.commit('sidebar', $event)" ...>
</template>
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
Another solution is to use vuex-map-fields package which enables two-way data binding for states saved in a Vuex store.
It makes the code clear, readable more than the normal way (as in the accepted answer).
Basic example:
in your store file
// Import the `getField` getter and the `updateField`
// mutation function from the `vuex-map-fields` module.
import { getField, updateField } from 'vuex-map-fields';
export const state = () => ({
baseurl: 'http://example.com/api/',
sidebar: false,
authenticated: false,
token: null,
user: null,
})
export const getters = {
// Add the `getField` getter to the
// `getters` of your Vuex store instance.
getField,
}
export const mutations = {
// Add the `updateField` mutation to the
// `mutations` of your Vuex store instance.
updateField,
}
in your component
template>
<v-navigation-drawer v-model="sidebar" ...>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
// The `mapFields` function takes an array of
// field names and generates corresponding
// computed properties with getter and setter
// functions for accessing the Vuex store.
...mapFields([
'baseurl',
'sidebar',
// etc...
]),
}
}
</script>
for more details, you can check its githab page
Related
Have started to play around with Vuex and am a bit confused.
It triggers the action GET_RECRUITERS everytime I load the component company.vue thus also making an api-call.
For example if I open company.vue => navigate to the user/edit.vue with vue-router and them go back it will call the action/api again (The recruiters are saved in the store accordinly to Vue-dev-tools).
Please correct me if I'm wrong - It should not trigger the action/api and thus resetting the state if I go back to the page again, correct? Or have I missunderstood the intent of Vuex?
company.vue
<template>
<card>
<select>
<option v-for="recruiter in recruiters"
:value="recruiter.id">
{{ recruiter.name }}
</option>
</select>
</card>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
mounted() {
this.$store.dispatch("company/GET_RECRUITERS")
},
computed: mapGetters({
recruiters: 'company/recruiters'
}),
}
</script>
company.js
import axios from 'axios'
// state
export const state = {
recruiters: [],
}
// getters
export const getters = {
recruiters: state => {
return state.recruiters
}
}
// actions
export const actions = {
GET_RECRUITERS(context) {
axios.get("api/recruiters")
.then((response) => {
console.log('API Action GET_RECRUITERS')
context.commit("GET_RECRUITERS", response.data.data)
})
.catch(() => { console.log("Error........") })
}
}
// mutations
export const mutations = {
GET_RECRUITERS(state, data) {
return state.recruiters = data
}
}
Thanks!
That's expected behavior, because a page component is created/mounted again each time you route back to it unless you cache it. Here are a few design patterns for this:
Load the data in App.vue which only runs once.
Or, check that the data isn't already loaded before making the API call:
// Testing that your `recruiters` getter has no length before loading data
mounted() {
if(!this.recruiters.length) {
this.$store.dispatch("company/GET_RECRUITERS");
}
}
Or, cache the page component so it's not recreated each time you route away and back. Do this by using the <keep-alive> component to wrap the <router-view>:
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
I have a Sidebar vue component that contains navigation drawer created using vuetify, it uses v-model to manage the state of the drawer, but state of drawer is stored in vuex, so that other components can modify drawer state.
Therefore, v-model (in the sidebar) watches computed property showDrawer, but as soon as I define setter for the computed property showDrawer the page does not load, because the action in the setter keeps firing, why is this the case, what am I doing wrong?
I have tried using v-bind and #input instead of v-model for the computed property showDrawer but the same problem persists
Sidebar.vue
<template>
<v-navigation-drawer v-model="showDrawer" class="sfm-sidebar" clipped dark disable-resize-watcher app width="290px">
</v-navigation-drawer>
</template>
<script>
export default {
computed: {
showDrawer: {
get() {
return this.$store.state.showDrawer;
},
set() {
this.$store.dispatch("toggleDrawer");
}
}
}
}
</script>
I guess it will get stuck in an infinite loop!
Try to toggle state manually:
<template>
<v-navigation-drawer v-model="showDrawer" class="sfm-sidebar" clipped dark disable-resize-watcher app width="290px">
</v-navigation-drawer>
</template>
<script>
export default {
computed: {
showDrawer: {
get() {
return this.$store.state.showDrawer;
},
set(value) {
if (this.$store.state.showDrawer !== value)
this.$store.dispatch("updateDrawer", value);
}
}
}
}
</script>
Then in store.js:
mutations: {
updateDrawer(state, payload) {
state.showDrawer = !!payload
}
}
(I'm new to vue and nuxt).
I currently have a <HeaderImage> component in my layouts/default.vue and would like to have each page to pass a different image url to that component.
Right now I'm using vuex $store for that purpose (but would love if there were a simpler way to pass the data), but I'm trying to figure out where in my pages/xyz.vue I should be using the mutation this.$store.commit('headerImg/setHeaderImage', 'someImage.jpg')
All of the examples I can find only use mutations on user events.
What you are trying to do probably doesn't have a particularly simple solution and how I would do it is use a store state element that is set by the component when it is loaded. The component would commit a mutation in the store that alters the state element. The layout would then use that state element through a getter to set the image url. Here is how I'd code that. In the store state i'd have an array of class names, let's call it 'headState', and an element that would be assigned one of those class names, called 'headStateSelect:
//store/index.js
state: {
headState: ['blue', 'red', 'green'],
headStateSelect : ''
}
In your component you can use fetch, or async fetch to commit a mutation that will set 'headStateSelect' with one of the 'headState' elements.
//yourComponent.vue
async fetch ({ store, params }) {
await store.commit('SET_HEAD', 1) //the second parameter is to specify the array position of the 'headState' class you want
}
and store:
//store/index.js
mutations: {
SET_HEAD (state, data) {
state.headStateSelect = state.headState[data]
}
}
In the store we should also have a getter that returns the 'headStateSelect' so our layout can easily get it.
getters: {
head(state) {
return state.headStateSelect
}
}
finally, in the layout we can use the computed property to get our getter:
//layouts/default.vue
computed: {
headElement() {
return this.$store.getters.head
}
}
and the layout can use the computed property to set a class like so:
//layouts/default.vue
<template>
<div :class="headElement">
</div>
</template>
The div in the layout will now be set with the class name 'red' (ie. store.state.headState[1]) and you can have a .red css class in your layout file that styles it however you want, including with a background image.
For now I've settled on creating it like this:
~/store/header.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = () => ({
headerImage: 'default.jpg'
})
const mutations = {
newHeaderImage(state, newImage) {
state.headerImage = newImage
}
}
export default {
namespaced: true,
state,
mutations
}
``
~/layouts/default.vue
<template>
<div id="container">
<Header />
<nuxt />
</div>
</template>
<script>
import Header from '~/components/Header'
export default {
components: {
Header
}
}
</script>
``
~/components/Header.vue
<template>
<header :style="{ backgroundImage: 'url(' + headerImage + ')'}" class="fixed">
<h1>Header Text</h1>
</header>
</template>
<script>
computed: {
var image = this.$store.state.header.headerImage
return require('~/assets/img/' + image)
}
</script>
``
~/pages/customHeader.vue
<template>
<main>
...
</main>
</template>
<script>
export default {
head() {
this.$store.commit('header/newHeaderImage', 'custom-header.jpg')
return {
title: this.title
}
}
}
</script>
But something feels off about putting the mutation in head() Is that correct?
And the next issue I am facing is how to return the header to default.jpg if a page doesn't change the state (which makes me think this is all the wrong approach).
I want to show some data in the menu-bar, that needs to be fetched remotely (http get call) to be correctly displayed. When my application loads, the store wasn't initialized yet. Where should I do that?
This is what I have right now. nodeInfo is an empty object, as long as no data is fetched.
navigation component
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
...
<div class="navbar-end">
<span class="navbar-item">
<div v-if="nodeInfo.latestSolidSubtangleMilestoneIndex">
{{nodeInfo.latestSolidSubtangleMilestoneIndex}} / {{nodeInfo.latestMilestoneIndex}}
</div>
<div v-else>
Node seems offline!
</div>
</span>
</div>
</div>
</nav>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'Menu',
computed: {
...mapGetters(['nodeInfo']) // Only the getters, no actions called to initialize them.
}
};
</script>
<style scoped>
</style>
store:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import axios from 'axios';
const iri_ip = '192.168.1.199';
const iri_port = '14265';
const state = {
token: null,
loading: false,
nodeInfo: {}
};
const mutations = {
SET_NODE_INFO(state, info) {
state.nodeInfo = info;
}
};
const actions = {
fetchNodeInfo({commit}) {
axios(createIriRequest('getNodeInfo')).then(response => {
console.log(response.data);
commit('SET_NODE_INFO', response.data);
});
}
};
const getters = {
token: state => state.token,
loading: state => state.loading,
nodeInfo: state => state.nodeInfo
};
const loginModule = {
state,
mutations,
actions,
getters
};
function createIriRequest(command) {
return {
url: `http://${iri_ip}:${iri_port}`,
data: {'command': command},
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-IOTA-API-Version': '1'
}
};
}
export default new Vuex.Store({
modules: {
loginModule
}
});
The naming doesn't make much sense at the moment. But would I need to call the "actions" from the create() method of the menu component? That would somehow be weird. It would be cool if my store could somehow make the initial http calls itself without needing to be triggered. I don't even know how to call an action just like that from the create() part.
Have a look at the vue.js lifecycle diagram here: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram and read on the the lifecycle hooks here: https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks.
It will hep you considerably in understanding when and where to add the stores dispatch method. this.$store.dispatch('fetchNodeInfo')
In Short:
Created hook:
Instance has been created, all the data observation, computed properties, methods, watch/event callbacks have been set up but the $el property isn't available yet.
Mounted hook:
Vue instance has been mounted, where el is replaced by the newly created vm.$el. el being the instance creation via new Vue({...}).
For your reading pleasure:
Lifecycle hooks: http://devdocs.io/vue~2-api-options-lifecycle-hooks/
#Bert was right. I added the dispatch method to the created() method of my component.
export default {
name: 'Menu',
created() {
this.$store.dispatch('fetchNodeInfo');
},
...
}
My parent component like this :
<template>
...
<search-result/>
...
</template>
<script>
import {mapActions} from 'vuex'
...
export default {
...
props: ['category'],
mounted() {
this.updateCategory(this.category)
},
methods: {
...mapActions(['updateCategory']),
}
}
</script>
My child component like this :
<template>
...
</template>
<script>
import {mapGetters} from 'vuex'
...
export default {
...
mounted() {
console.log(this.getCategory)
},
computed: {
...mapGetters(['getCategory'])
},
}
</script>
My modules vuex to send data between components like this :
import { set } from 'vue'
...
// initial state
const state = {
category: null
}
// getters
const getters = {
getCategory: state => state.category
}
// actions
const actions = {
updateCategory ({commit}, payload) {
commit(types.UPDATE_CATEGORY, payload)
}
}
// mutations
const mutations = {
[types.UPDATE_CATEGORY] (state, payload) {
state.category = payload
}
}
export default {
state,
getters,
actions,
mutations
}
I try like that, but it does not works
If the code executed, the result of console.log(this.getCategory) in the child component is null
For example category prop in the parent component = 7. Should the result of console.log(this.getCategory) in the child component = 7
Why it does not work? Why the result is null?
Note :
I can just send the category data via prop. But here I do not want to use prop. I want to send data via vuex store. So I want to set and get data only via vuex store
Child's mounted hook is executed before parent's mounted hook. ( why? See this link)
console.log(this.getCategory) happens before this.updateCategory(this.category).
Therefore, you get null in the console.
If you put console.log(this.getCategory) in updated hook, you would be getting the right value in the console later on.
Jacob goh has pointed out the problem.
To solve this issue you can make use of vm.$nextTick() in the child component's mounted hook to ensure that the entire view has been rendered and the parent's mounted hook is called.
<template>
...
</template>
<script>
import {mapGetters} from 'vuex'
...
export default {
...
mounted() {
this.$nextTick(() => {
console.log(this.getCategory);
})
},
computed: {
...mapGetters(['getCategory'])
},
}
</script>
Here is the working fiddle
You can learn more about why use vm.nextTick() here: Vue updates the DOM asynchronously