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
Related
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()
}
How to create reactive() object with custom setter (debounced) like with customRef()
You can use computed inside reactive. Namely, you might want to use a Writable Computed (getter/setter):
import { ref, reactive, computed } from 'vue'
const whatever = ref('test')
const state = reactive({
myProp: computed({
get() {
console.log('myProp was read');
return whatever.value
},
set(val) {
console.log(`myProp was set to ${val}`)
whatever.value = val
}
})
})
Test:
const { createApp, ref, reactive, computed, toRefs } = Vue
const app = createApp({
setup() {
const whatever = ref('test')
const state = reactive({
myProp: computed({
get() {
console.log('myProp was read');
return whatever.value
},
set(val) {
console.log(`myProp was set to ${val}`)
whatever.value = val
}
}),
emitter: computed({
get() {
return true
},
set(val) {
console.log(`I was set to ${val} but I'm not changing value...`)
}
})
})
return { ...toRefs(state), whatever }
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.2.41/dist/vue.global.prod.js"></script>
<div id="app">
<input v-model="myProp" />
<pre v-text="{
whatever,
myProp,
emitter
}"></pre>
<button #click="emitter = !emitter">Click me!</button>
</div>
Notice the getter is only called once per change, not once for each place where it's used in <template>.
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 :)
I have a Vue component. This component has a computed prop & a watch:
const resetComponent = computed(()=>{
return store.state.filtros.room_amount
})
watch(resetComponent, () => {
if(resetComponent.value.compare == '>' && resetComponent.value.valor == '' ){
console.log('RESET COMPONENT')
}
})
My console.log('RESET COMPONENT') runs correctly, when it should.
But instead, i want re-render all my component, that is, return to its initial state. There's some way?
This is my full component
<template>
<FiltroCantidad :data="data" />
</template>
<script>
import SelectButton from "primevue/selectbutton";
import FiltroCantidad from "../utils/FiltroCantidad.vue";
import { computed, ref, watch } from "vue";
import { useStore } from 'vuex';
export default {
setup(props, context) {
const store = useStore()
const data = ref({
label: "Ambientes",
value: "room_amount",
action: "roomAmountAction",
});
const resetComponent = computed(()=>{
return store.state.filtros.room_amount
})
watch(resetComponent, () => {
if(resetComponent.value.compare == '>' && resetComponent.value.valor == '' ){
console.log('RESET COMPONENT')
}
})
return { data, resetComponent };
},
components: {
SelectButton,
FiltroCantidad,
},
};
</script>
One way to re-render the component is to apply a key attribute that changes:
<FiltroCantidad :data="data" :key="myKey" />
export default {
setup() {
//...
const myKey = ref(0)
watch(resetComponent, () => {
if(/* need to reset */) {
myKey.value++
}
})
return { myKey }
}
}
I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');