Vuex toggle array item boolean value - vue.js

For some reason I can not find a working solution to toggle checkbox of an item from Vuex state array. I got it working to a point where I am able to display todo items, but whenever I try to commit and call a toggle mutation, it doesn't work as expected, nothing changes.
If I set done = true, it works, but not toggling it. Any idea?
View:
<template>
<div class="todos">
<ul>
<li v-for="todo in $store.state.todos" :key="todo.id">
<input
type="checkbox"
v-model="todo.done"
#change="$store.commit('toggle', todo.id)"
/>
<h3>{{ todo.title }}</h3>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>
<style></style>
State:
import { createStore } from 'vuex';
import Todo from '#/types/Todo';
import { State } from 'vue';
const store = createStore({
state() {
return {
todos: [] as Todo[],
};
},
mutations: {
add(state: State, todo: Todo) {
state.todos.push(todo);
},
toggle(state: State, todoId: number) {
const index = state.todos.findIndex((todo) => todo.id === todoId);
state.todos[index].done = !state.todos[index].done;
},
},
});
export default store;

Try updating the memory reference of the todos array by cloning the state.todos array and deconstructing the todo object.
state.todos = state.todos.map((todo) => ({...todo, done: todoId === todo.id ? !todo.done : todo.done }));

Related

Async loading child component doesn't trigger v-if

Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !
A little bit of context
I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.
On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.
I made it works well on static elements :
For example, here the plus button is shown as expected.
However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :
Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :
And that's what I expect once the data are loaded.
There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?
So and the code ?
First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :
Auth.js
export default {
computed: {
IsAdmin() {
return true;
}
},
}
Then we have a first component :
LCSkills.js
<template>
<div class="skills-container">
<h2 v-if="skills">{{ $t('skills') }}</h2>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
<LCModal ref="addModal"></LCModal>
<div class="skills" v-if="skills">
<LCSkillCategory
v-for="category in skills"
:key="category"
:category="category"
/>
</div>
</div>
</template>
<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkills',
components: {
LCSkillCategory,
LCAdmin,
LCModal,
},
computed: mapState({
skills: (state) => state.career.skills,
}),
mixins: [Auth],
};
</script>
<style scoped>
...
</style>
This component load each skills category with the LCSkillCategory component when the data is present in the store.
LCSkillCategory.js
<template>
<div class="skillsCategory">
<h2 v-if="category">{{ name }}</h2>
<LCAdmin
v-if="IsAdmin && category"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
<div v-if="category">
<LCSkill
v-for="skill in category.skills"
:key="skill"
:skill="skill"
/>
</div>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
<LCModal ref="addSkillModal"></LCModal>
</div>
</template>
<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkillCategory',
components: { LCSkill, LCAdmin, LCModal },
props: ['category'],
mixins: [Auth],
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.category,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
And so each category load a LCSkill component for each skill of this category.
<template>
<div class="skill-item">
<img :src="img(skill.icon.hash, 30, 30)" />
<p>{{ name }}</p>
<LCAdmin
v-if="IsAdmin"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
</div>
</template>
<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
export default {
name: 'LCSkill',
mixins: [LCImageRendering, Auth],
props: ['skill'],
components: { LCAdmin, LCModal },
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.skill,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
Then, the component with the button that is added everywhere :
LCAdmin.js
<template>
<div class="lc-admin">
<button v-if="addModal" #click="addModal.openModal()">
<i class="fas fa-plus"></i>
</button>
<button v-if="editModal" #click="editModal.openModal()">
<i class="fas fa-edit"></i>
</button>
<button v-if="deleteModal" #click="deleteModal.openModal()">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
<script>
export default {
name: 'LCAdmin',
props: ['addModal', 'editModal', 'deleteModal'],
};
</script>
Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...
By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !
EDIT :: ADDED The Store Code
Store Career Module
import { getCareer, getSkills } from '../../services/CareerService';
const state = () => {
// eslint-disable-next-line no-unused-labels
careerPath: [];
// eslint-disable-next-line no-unused-labels
skills: [];
};
const actions = {
async getCareerPath ({commit}) {
getCareer().then(response => {
commit('setCareerPath', response);
}).catch(err => console.log(err));
},
async getSkills ({commit}) {
getSkills().then(response => {
commit('setSkills', response);
}).catch(err => console.log(err));
}
};
const mutations = {
async setCareerPath(state, careerPath) {
state.careerPath = careerPath;
},
async setSkills(state, skills) {
state.skills = skills;
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
Career Service
export async function getCareer() {
const response = await fetch('/api/career');
return await response.json();
}
export async function getSkills() {
const response = await fetch('/api/career/skill');
return await response.json();
}
Then App.vue, created() :
created() {
this.$store.dispatch('config/getConfigurations');
this.$store.dispatch('certs/getCerts');
this.$store.dispatch('career/getSkills');
this.$store.dispatch('projects/getProjects');
},
Clues
It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)
Which give me this result :
Problem is that refs are not reactive
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
See simple demo below...
const vm = new Vue({
el: "#app",
components: {
MyComponent: {
props: ['modalRef'],
template: `
<div>
Hi!
<button v-if="modalRef">Click!</button>
</div>`
}
},
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component :modal-ref="$refs.modal"></my-component>
<div ref="modal">I'm modal placeholder</div>
</div>
The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...

Vuex- Page is not updating after commit changes made to state

I am trying to make a simple project which has 2 Vue pages add.vue and read.vue. If we add a message in add.vue(using commit to mutate teh state), results should be displayed in read.vue , using Vuex store. (I am using Nuxt)
store/index.js file=>
export const state = () => ({
messages:[]
})
export const mutations={
addMessage:(state, comment)=> {
state.messages.push(comment)
}
}
add.vue file
<template>
<div class="addContainer">
<b-form #submit.prevent="handleSubmit">
<b-form-group id="input-group-2" label="Comment:" label-for="input-2">
<b-form-input
id="input-2"
v-model="comment"
required
placeholder="Enter Your Comment"
></b-form-input>
</b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
</b-form>
</div>
</template>
<script>
import Header from '~/components/Header'
import {mapActions} from 'vuex'
import {mapState} from 'vuex'
export default {
data() {
return {
comment:''
}
},
computed:{
...mapState([
'messages'
])
},
methods:{
handleSubmit(){
this.$store.commit('addMessage', this.comment)
console.log('Messages is '+this.messages )
this.comment = ''
}
},
components:{
Header
}
}
</script>
read.vue
<template>
<div>
<p>Read Messages</p>
<ul>
<li v-for="(msg, index) in messages" :key="index">
<b>{{ msg}}</b>
<br>
{{ messages}}
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
data() {
return {}
},
computed:{
...mapState({
messages:state=>state.messages
})
}
}
</script>
In Vue component , I can see messages array inside state changes after adding a message , but same changes are not reflected in read page.
Add getters to your store:
export const getters= {
getMessages: state => state.messages
}
and then use the getters in your component instead of accessing the state directly:
// read.vue
<script>
import {mapGetters} from 'vuex'
export default {
data() {
return {}
},
computed:{
...mapGetters({
messages: 'getMessages',
})
}
}
</script>

Watch doesn't get fire/trigger when global property change

I’m very new to Vue and I begin with Vue 3. I was trying to migrate a template from Vue 2 to Vue 3 so I can start with my project.
I have this plugin file.
Sidebar\Index.ts
import SidebarPlugComp from './SidebarPlugComp.vue'
import SidebarLinkPlugComp from './SidebarLinkPlugComp.vue'
// tiny internal plugin store
const SidebarStore = {
showSidebar: false,
sidebarLinks: [],
displaySidebar (value: boolean) {
this.showSidebar = value
}
}
const SidebarPlugin = {
install (app: any) {
app.config.globalProperties.$sidebar = SidebarStore
app.component('side-bar-plug-comp', SidebarPlugComp)
app.component('sidebar-link-plug-comp', SidebarLinkPlugComp)
}
}
export default SidebarPlugin
Also I have a BaseTopNavLay layout file so I can toggle the sidebar with handleSidebarToggle onclick button method
<template>
\\...
<div class="navbar-toggle d-inline" :class="{toggled: $sidebar.showSidebar}">
<button type="button"
class="navbar-toggler"
aria-label="Navbar toggle button"
#click.prevent="handleSidebarToggle">
<span class="navbar-toggler-bar bar1"></span>
<span class="navbar-toggler-bar bar2"></span>
<span class="navbar-toggler-bar bar3"></span>
</button>
\\ ...
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ModalComp } from '../components'
export default defineComponent({
name: 'BaseTopNavLay',
components: {
ModalComp
},
// ...
methods: {
handleSidebarToggle (): void {
this.$sidebar.displaySidebar(!this.$sidebar.showSidebar)
},
handleHideSideBar (): void {
this.$sidebar.displaySidebar(false)
},
}
})
</script>
And here is the watch in the App.vue file
<template>
<component :is="this.$route.meta.layout || 'div'">
<router-view />
</component>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Application',
methods: {
toggleNavOpen () {
console.log('here')
let root = document.getElementsByTagName('html')[0]
root.classList.toggle('nav-open')
}
},
/*watch: {
'$sidebar.showSidebar': function(newVal, oldVal) {
console.log(newVal, oldVal)
}
}*/
mounted () {
//#ts-ignore
this.$watch('$sidebar.showSidebar', this.toggleNavOpen)
}
})
</script>
Wherever I test the var this.$sidebar.showSidebar I can access to its value properly. Also, the onclick method is changing the SidebarStore object in Sidebar\Index.ts plugin file.
Can anyone give me a hint what am I missing here? Why the watch doesn't get fired. Thanks in advance.
The problem is that you have not made your $sidebar reactive, and a watch needs to use a reactive variable.
You can keep the store where you have it, but I'd put it into a separate file (store.js) and import where needed, no need to put it on app.config.globalProperties.$sidebar (but that might be a personal preference
// store.js
// using reactive (all values)
export const SidebarStore = Vue.reactive({
showSidebar: false,
sidebarLinks: [],
})
// or using ref (one for each)
// export const showSidebar = Vue.ref(false);
export const displaySidebar = (value: boolean) => {
SidebarStore.showSidebar.value = value;
}
this will make SidebarStore and displaySidebar available anywhere in your code
use like this
<template>
\\...
<div class="navbar-toggle d-inline" :class="{toggled: $sidebar.showSidebar}">
<button type="button"
class="navbar-toggler"
aria-label="Navbar toggle button"
#click.prevent="handleSidebarToggle">
<span class="navbar-toggler-bar bar1"></span>
<span class="navbar-toggler-bar bar2"></span>
<span class="navbar-toggler-bar bar3"></span>
</button>
\\ ...
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ModalComp } from '../components'
import { SidebarStore, displaySidebar } from '../store'
export default defineComponent({
name: 'BaseTopNavLay',
components: {
ModalComp
},
// ...
methods: {
handleSidebarToggle (): void {
displaySidebar(!SidebarStore.showSidebar)
},
handleHideSideBar (): void {
displaySidebar(false)
},
}
})
</script>

vuex module mode in nuxtjs

I'm trying to implement a todo list using modules mode in the vuex store in nuxtjs but get the error this.$store.todo is undefined and cant find much about this relating to nuxt
Can anyone assist please I have
store index.js
export const state = () => ({
})
export const mutations = {
}
store todo.js
export const state = () => ({
todos: [],
})
export const mutations = {
mutations ...
}
export const actions = {
actions ...
}
export const getters = {
getters ...
}
index.vue page
<template>
<div>
<h2>Todos:</h2>
<p> Count: {{ doneTodosCount }} </p>
<ul v-if="todos.length > 0">
<li v-for="(todo, i) in todos" :key="i">
...
</li>
</ul>
<p v-else>Done!</p>
<div class="add-todo">
<input type="text" v-model="newTodoText">
<button #click="add">Add todo</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
export default {
name: 'app',
data () {
return {
newTodoText: ""
}
},
created () {
this.$store.todo.dispatch('loadData')
},
computed: {
...mapState(['todos', ]),
...mapGetters([ 'doneTodosCount', 'doneTodos'])
},
methods: {
toggle (todo) {
this.$store.todo.dispatch('toggleTodo', todo)
},
}
}
</script>
From what i read I thought this should work but doesn't
I should add it all works fine if i don't use modules mode and just have a single index.js setup
Many Thanks
You need to call it differently
this.$store.dispatch('todo/toggleTodo', todo)
Also better to call it in fetch method, not created

vue, vuex, vue-i18n change language button event

Why changing mutation do not update page in new language?
main.js : here I implemented vue-i18n with vue:
import Vue from 'vue'
import VueRouter from 'vue-router'
import VueResource from 'vue-resource'
import VueI18n from 'vue-i18n'
import locales from './locales'
import router from './router'
import store from './store'
import App from './App'
Vue.use(VueRouter)
Vue.use(VueResource)
Vue.use(VueI18n, store)
Vue.http.interceptors.push((request, next) => {
console.log('sending request: ', request)
next(response => {
console.log('response: ', response)
})
})
Vue.config.debug = true
Vue.config.lang = 'fa'
Object.keys(locales).forEach(lang => {
Vue.locale(lang, locales[lang])
})
const app = new Vue({
el: '#app',
router,
VueI18n,
store,
render: h => h(App)
})
app.$mount('#app')
App.vue: Then used two buttons to change language:
<template>
<div id="app">
<h2>{{ $t('example', '#store.state.culture') }}</h2>
<p>{{ count }}</p>
<p>
<button #click="increment()">+</button>
<button #click="decrement()">-</button>
</p>
<p>culture: {{ culture }}</p>
<p>
<button #click=' changeCulture("en") '>English</button>
<button #click=' changeCulture("fa") '>پارسی</button>
</p>
<input type="text" v-model="newUserName">
<button #click="handleAddUserButton()">add</button>
<div>
<router-link to="/page1">Go to page1</router-link>
<router-link to="/page2">Go to page2</router-link>
</div>
<transition name="fade" mode="out-in">
<keep-alive>
<router-view></router-view>
</keep-alive>
</transition>
<img src="./assets/logo.png">
<hello></hello>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
// import App from './main.js'
import Hello from 'components/Hello'
export default {
name: 'app',
components: {
Hello
},
data () {
return {
newUserName: ''
}
},
computed: {
...mapGetters([
'count',
'culture'
])
},
methods: {
...mapActions([
'increment',
'decrement',
'exampleGetFirebaseData',
'examplePostFirebaseData',
'changeCulture'
]),
handleAddUserButton () {
const user = {
name: this.newUserName
}
this.examplePostFirebaseData(user)
.then(resp => {
// console.log('resp: ', resp)
})
.catch(error => {
console.log('catch error: ', error)
})
},
handleError () {
}
},
beforeMount () {
this.exampleGetFirebaseData()
.then(resp => {
// console.log('resp: ', resp)
})
.catch(error => {
this.handleError(error)
// console.log('catch error: ', error)
})
}
}
</script>
sotre > culture.js: Then using store, getters, actions and mutation to change langauge,
const state = {
locales: ['en', 'fa'],
culture: 'en'
}
const getters = {
culture: state => state.culture
}
const actions = {
async changeCulture ({ commit }, playload) {
commit('CHANGE', playload)
}
}
import App from '../../main.js'
const mutations = {
CHANGE (state, payload) {
if (state.locales.indexOf(payload) !== -1) {
state.culture = payload
} else state.culture = 'en'
console.log(App.i18n)
}
}
export default {
state,
getters,
actions,
mutations
}
I checked using vue development tools in chrome and culture is changed but the problem is that title of {{ $t("example")}} do not change as mutation change.
I know doubt something basic is wrong in my code, may you please help.
Many Thanks in advance.
Becasue this is not a valid expression for i18n
<h2>{{ $t('example', '#store.state.culture') }}</h2>
If you want to translate the text en and fa do this
<h2>{{ $t(culture) }}</h2>
OR
<h2>{{ $t(`namespace:${culture}`) }}</h2>
If you want to use namespace