Vue Pinia: Toggle function toggles just once and not back - vue.js

I got a Pinia useStore, where I got a boolean variable and a toggle function:
export const useStore = defineStore('storeID', {
state: () => ({
eineVariable: true
}),
getters: {
getEineVariable(state) {
return state.eineVariable
}
},
actions: {
toggleFunktion() {
this.eineVariable = !this.eineVariable
console.log(this.eineVariable)
}
}
})
If I create this div with the clickevent to toggle the color of the div, it just loggs me one toggle from true to false, but not further.
import {useStore} from "../store/index.js"
const store = useStore()
const isActive = computed(() => {
return store.getEineVariable
})
const toggleBox = computed(() => {
return store.toggleFunktion()
})
<div class="piniaBox" :class="{ 'active': isActive}" #click="toggleBox"></div>
I really don't see my mistake. Could you guys please help?

A computed is supposed to return a value, it's a mistake to do side effects inside of it.
Should be:
const toggleBox = () => {
store.toggleFunktion()
}

Related

Jest: How I should change the mock data of Vuex in each test?

I've been working in a test where I need the data from Vuex. However, I'm having some problems, I need to change that data in each test in order to test the functionality of the component.
Here is my component:
<template>
<div id="cb-items-displayer" #click="textClick">
<span>(</span>
<p>{{ text }}</p>
<span>)</span>
</div>
</template>
<script lang="ts" setup>
import { capitalize } from '#/utils/capitalize'
import { ItemsDisplayer } from '#/models/ItemsDisplayer'
import { computed, PropType } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const props = defineProps({
type: {
type: String,
default: '',
},
menuType: {
type: String,
default: '',
},
items: {
type: Array as PropType<ItemsDisplayer[]>,
default: () => [],
}
})
const emit = defineEmits<{
(event: 'textClicked'): void
}>()
const text = computed(() => {
const param = props.menuType === 'radio' ? 'One' : 'Many'
console.log( "TYPEEE ", props.type, " ", param )
const itemsIds = store.getters['filters/get' + capitalize(props.type) + param]
console.log("ITEMSSS", JSON.stringify(itemsIds))
return getTextToShow(itemsIds)
})
const getTextToShow = (itemsIds: string) => {
//TODO - improve it
if (itemsIds === 'all') {
return 'all'
} else if (itemsIds.length === 0) {
return '-'
} else if (itemsIds.length === 1) {
return getName(itemsIds[0], props.items)
} else {
return itemsIds.length
}
}
const textClick = () => {
emit('textClicked')
}
const getName = (id: string, items: ItemsDisplayer[]) => {
const found: ItemsDisplayer = items.find((x) => x.id! === id) as ItemsDisplayer
console.log("GETNAME ", found.name)
return found?.name
}
</script>
And this is the test:
import { render, screen, click, waitFor } from '#tests/app-test-utils'
import ItemsDisplayer from './ItemsDisplayer.vue'
import { capitalize } from '#/utils/capitalize'
let mockStoreCommit: jest.Mock
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: [],
},
commit: mockStoreCommit,
}),
}))
describe('ItemsDisplayer', () => {
beforeEach(() => {
mockStoreCommit = jest.fn()
render(
ItemsDisplayer,
{},
{
props: {
type: 'categories',
menuType: 'checkbox',
items: [
{
box_templates:"",
id:"1",
name:"Culture"
},
{
box_templates:"",
id:"2",
name:"Economy"
},
{
box_templates:"",
id:"3",
name:"Education"
}
]},
}
)
})
it('renders the component', async() => {
await screen.getByText('-')
})
it('renders the component with one item', async() => {
//DON'T WORK HERE THERE SHOULD BE A CHANGE OF DATA IN THE MOCKED STORE IN ORDER TO WORK
await screen.getByText('Culture')
})
})
My problem is that I need to change the value of [filters/get${capitalize('categories')}Many] to ["1"] in the second test.
I tried several things in order to change the mocked data but they don't work. How can I change the mocked store data in each test?
Thanks!
You can achieve this by lazy loading your vue component:
Add jest.resetModules(); in the beforeEach to reset all of the imported modules before each test so they can be re-evaluated and re-mocked:
beforeEach(() => {
jest.resetModules();
In each unit test, you will first need to import the vue component using the require syntax as follows:
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
Then add the mock directly after the import with the [`filters/get${capitalize('categories')}Many`] value being set to whatever you want it to be:
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
I have noticed that you do your rendering in the beforeEach. Unfortunately because you import and mock your modules during the test, the rendering will need to be done after these have taken place - hence you will need to either move that logic into your unit test or extract it into another function which can be called from within the unit test.
Each unit test should look something like this:
it('renders the component', async() => {
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
// beforeEach logic here or a call to a function that contains it
await screen.getByText('-')
})

Use props in composables vue3

I am upgrading an app from vue 2 to vue 3 and I am having some issues with composables. I'd like to use props in the composable but it doesn't seem to be working. The code sample is pulled from a working component and works fine when I leave it in the component.
I assume defineProps isn't supported by composables, but then I am unclear how to handle it. When I pass the src in the parameters it loses its reactivity.
// loadImage.js
import { defineProps, onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage() {
let loadingImage = ref(true)
let showImage = ref(false)
const props = defineProps({
src: String,
})
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
Edit
This method worked for me, but the two methods mentioned in the comments below did not.
I have a new question here.
// loadImage.js
import { onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage(props) {
let loadingImage = ref(true)
let showImage = ref(false)
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
<script setup>
import { defineProps, toRef } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
const { loading, show } = useLoadImage(props)
</script>
According to official docs :
defineProps and defineEmits are compiler macros only usable inside <script setup>
You should pass the props as parameter without destructing them to not loose the reactivity :
export function useLoadImage(props) {
....
}
you can use useRef to pass specific props without losing reactivity
const imgRef = toRef(props, "img");
const { loding, show } = useLoadImage(imgRef);

My data variable not reflect to components props in vue

I have this code
<template>
<AppLayout :user="user">
<router-view :user="user" />
</AppLayout>
</template>
<script setup>
import LoginService from '#/services/LoginService';
import { inject, onMounted } from 'vue';
let user = null;
const $cookies = inject('$cookies');
async function getFuncionario() {
const publicToken = $cookies.get('PublicToken');
console.log(publicToken);
if (publicToken) {
await LoginService.getFuncionarioForMenu(publicToken)
.then((res) => {
user = res.data;
})
.catch((error) => {
console.log(error);
// redirect pagina de erro
});
console.log(user);
}
}
onMounted(() => {
getFuncionario();
});
</script>
I passed user variable like a props for my components
This "user" variable isn't updated after set my data to API:
user = res.data;
This variable does not reflect in my component
My component
<script>
import { markRaw } from 'vue';
const emptyLayout = 'EmptyLayout';
export default {
name: 'AppLayout',
data: () => ({
layout: emptyLayout,
}),
props: {
user: null,
},
watch: {
$route: {
immediate: true,
async handler(route) {
try {
const component = await import(`#/layouts/${route.meta.layout}.vue`);
this.layout = markRaw(component?.default || emptyLayout);
} catch (e) {
this.layout = emptyLayout;
}
},
},
},
};
</script>
Any ideas
I found the answer
I change my variable to
const user = ref(null);
this ref make a reflect in lifecycle
and I set the variable like this
user.value = res.data;

how to reset one of data in pinia state?

I have a design in setting page,every one of them hava reset button, now i using pinia to be store library.
I kown $reset is reset the whole pinia state,so,how to reset one of data in pinia state?
The typical way I do this:
const defaultState = {
foo: 'bar'
}
export const useFoo = defineStore('foo', {
state: () => ({ ...defaultState }),
actions: {
reset() {
Object.assign(this, defaultState);
}
}
})
You get the initial state and a reset() action which resets whatever state has to the initial. Obviously, you can pick and choose what you put in defaultState.
If you only want to reset one particular state prop, without touching anything else, just assign the default value to it:
useFoo().foo = 'bar';
If you find it useful, you can also have a generic update, where you can assign multiple values to state in one call:
actions: {
update(payload) {
Object.assign(this, payload)
}
}
Use it like:
useFoo().update({
foo: 'bar',
// add more props if needed...
});
Last, but not least, lodash's pick can be used to pick and choose what gets reset, from the defaultState values, without having to specify the actual values:
import { pick } from 'lodash-es';
const defaultState = {
foo: 'bar',
boo: 'far'
};
export const useFoo = defineStore('foo', {
state: () => ({ ...defaultState }),
actions: {
reset(keys) {
Object.assign(this, keys?.length
? pick(defaultState, keys)
: defaultState // if no keys provided, reset all
);
}
}
})
use it like:
useFoo().reset(['foo']);
This only resets foo to 'bar', but doesn't touch current value of boo.
To reset both (using the action above):
useFoo().reset(['foo', 'boo']);
...or useFoo().reset() or useFoo().reset([]), both of which reset all the state, because the keys?.length condition is falsey.
Here's a working example:
const { createPinia, defineStore, storeToRefs } = Pinia;
const { createApp, reactive, toRefs } = Vue;
const defaultState = {
foo: "bar",
boo: "far",
};
const useStore = defineStore("foobar", {
state: () => ({ ...defaultState }),
actions: {
reset(keys) {
Object.assign(
this,
keys?.length ? _.pick(defaultState, keys) : defaultState
);
},
},
});
const pinia = createPinia();
const app = createApp({
setup() {
const store = useStore();
const localState = reactive({
resetFoo: false,
resetBoo: false,
});
const resetStore = () => store.reset(
[
localState.resetFoo ? "foo" : null,
localState.resetBoo ? "boo" : null,
].filter((o) => o)
);
return { ...storeToRefs(store), ...toRefs(localState), resetStore };
},
});
app.use(pinia);
app.mount("#app");
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pinia/2.0.28/pinia.iife.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<div id="app">
<input v-model="foo" />
<input v-model="boo" />
<pre v-text="JSON.stringify({foo, boo}, null, 2)"></pre>
<hr>
<label>
<input type="checkbox" v-model="resetFoo" />ResetFoo</label>
<label>
<input type="checkbox" v-model="resetBoo" />ResetBoo</label>
<button #click="resetStore">Reset</button>
</div>
Above example doesn't reset one property to the default value when the property is already changed.
That's because the defaultState is reactive, you need to copy the defaultState so it's not reactive anymore.
import _pick from 'lodash.pick';
const defaultState = {
foo: 'bar',
};
export const useStore = defineStore('store', {
state: () => ({...defaultState}),
actions: {
reset(keys) {
Object.assign(this, keys?.length
? _pick(defaultState, keys)
: defaultState // if no keys provided, reset all
);
}
}
})
Use it like this
useStore().reset(['foo']);
This will now reset foo back to bar

Vuex + Jest + Composition API: How to check if an action has been called

I am working on a project built on Vue3 and composition API and writing test cases.
The component I want to test is like below.
Home.vue
<template>
<div>
<Child #onChangeValue="onChangeValue" />
</div>
</template>
<script lang="ts>
...
const onChangeValue = (value: string) => {
store.dispatch("changeValueAction", {
value: value,
});
};
</scirpt>
Now I want to test if changeValueAction has been called.
Home.spec.ts
...
import { key, store } from '#/store';
describe("Test Home component", () => {
const wrapper = mount(Home, {
global: {
plugins: [[store, key]],
},
});
it("Test onChangeValue", () => {
const child = wrapper.findComponent(Child);
child.vm.$emit("onChangeValue", "Hello, world");
// I want to check changeValueAction has been called.
expect(wrapper.vm.store.state.moduleA.value).toBe("Hello, world");
});
});
I can confirm the state has actually been updated successfully in the test case above but I am wondering how I can mock action and check if it has been called.
How can I do it?
I have sort of a similar setup.
I don't want to test the actual store just that the method within the component is calling dispatch with a certain value.
This is what I've done.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}
Hope this will help you further :)