Vuex input with v-model not reactive - javascript

I try to explain it as simple as possible. I have something like this. Simple Vue root, Vuex store and input with v-model inside navbar id.
That input is not reactive... Why?!
HTML
<div id="navbar">
<h2>#{{ test }}</h2>
<input v-model="test" />
</div>
store.js
import Vuex from 'vuex'
export const store = new Vuex.Store({
state: {
test: 'test'
},
getters: {
test (state) {
return state.test
}
}
})
Vue Root
import { store } from './app-store.js'
new Vue({
el: '#navbar',
store,
computed: {
test () {
return this.$store.getters.test
}
}
})

You're binding to a computed property. In order to set a value on a computed property you need to write get and set methods.
computed:{
test:{
get(){ return this.$store.getters.test; },
set( value ){ this.$store.commit("TEST_COMMIT", value );}
}
}
And in your store
mutations:{
TEST_COMMIT( state, payload ){
state.test=payload;
}
}
Now when you change the value of the input bound to test, it will trigger a commit to the store, which updates its state.

You don't want to use v-model for that. Instead, use #input="test" in your input field and in the your methods hook:
test(e){
this.$store.dispatch('setTest', e.target.value)
}
Inside your Vuex store:
In mutations:
setTest(state, payload){
state.test = payload
},
In actions:
setTest: (context,val) => {context.commit('setTest', val)},
The input should now be reactive and you should see the result in #{{test}}
Here is an example of how I handle user input with Vuex: http://codepen.io/anon/pen/gmROQq

You can easily use v-model with Vuex (with actions/mutations firing on each change) by using my library:
https://github.com/yarsky-tgz/vuex-dot
<template>
<form>
<input v-model="name"/>
<input v-model="email"/>
</form>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
...takeState('user')
.expose(['name', 'email'])
.commit('editUser') // payload example: { name: 'Peter'}
.map()
}
}
</script>

The computed property is one-way. If you want two-way binding, make a setter as the other answers suggest or use the data property instead.
import { store } from './app-store.js'
new Vue({
el: '#navbar',
store,
// computed: {
// test() {
// return this.$store.getters.test;
// }
// }
data: function() {
return { test: this.$store.getters.test };
}
});
But a setter is better to validate input value.

Related

Vue 3, call $emit on variable change

So I'm trying to build a component in Vue 3 that acts as a form, and in order for the data to be processed by the parent I want it to emit an object with all the inputs on change. The issue I'm having is that I don't seem to be able to call $emit from within watch() (probably because of the context, but I also don't see why the component-wide context isn't passed by default, and it doesn't accept this). I also cannot call any method because of the same reason.
I do see some people using the watch: {} syntax but as I understand it that is deprecated and it doesn't make a whole lot of sense to me either.
Here's a minimal example of what I'm trying to accomplish. Whenever the input date is changed, I want the component to emit a custom event.
<template>
<input
v-model="date"
name="dateInput"
type="date"
>
</template>
<script>
import { watch, ref } from "vue";
export default {
name: 'Demo',
props: {
},
emits: ["date-updated"],
setup() {
const date = ref("")
watch([date], (newVal) => {
// $emit is undefined
console.log(newVal);
$emit("date-updated", {newVal})
// watchHandler is undefined
watchHandler(newVal)
})
return {
date
}
},
data() {
return {
}
},
mounted() {
},
methods: {
watchHandler(newVal) {
console.log(newVal);
$emit("date-updated", {newVal})
}
},
}
</script>
Don't mix between option and composition api in order to keep the component consistent, the emit function is available in the context parameter of the setup hook::
<template>
<input
v-model="date"
name="dateInput"
type="date"
>
</template>
<script>
import { watch, ref } from "vue";
export default {
name: 'Demo',
props: {},
emits: ["date-updated"],
setup(props,context) {// or setup(props,{emit}) then use emit directly
const date = ref("")
watch(date, (newVal) => {
context.emit("date-updated", {newVal})
})
return {
date
}
},
}
</script>
if you want to add the method watchHandler you could define it a plain js function like :
...
watch(date, (newVal) => {
context.emit("date-updated", {newVal})
})
function watchHandler(newVal) {
console.log(newVal);
context.emit("date-updated", {newVal})
}
...

Vue 3.0 How to assign a prop to a ref without changing the prop

I'm sending from the parent component a prop: user. Now in the child component I want to make a copy of it without it changing the prop's value.
I tried doing it like this:
export default defineComponent({
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const user = ref(props.apiUser);
return { user };
}
});
But then if I change a value of the user object it also changes the apiUser prop. I thought maybe using Object.assign would work but then the ref isn't reactive anymore.
In Vue 2.0 I would do it like this:
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
return {
user: {}
}
},
mounted() {
this.user = this.apiUser;
// Now I can use this.user without changing this.apiUser's value.
}
};
Credits to #butttons for the comment that lead to the answer.
const user = reactive({ ...props.apiUser });
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const userCopy = toRef(props, 'apiUser')
}
With the composition API we have the toRef API that allows you to create a copy from any source reactive object. Since the props object is a reactive, you use toRef() and it won't mutate your prop.
This is what you looking for: https://vuejs.org/guide/components/props.html#one-way-data-flow
Create data where you add the prop to
export default {
props: ['apiUser'],
data() {
return {
// user only uses this.apiUser as the initial value;
// it is disconnected from future prop updates.
user: this.apiUser
}
}
}
Or if you use api composition:
import {ref} from "vue";
const props = defineProps(['apiUser']);
const user = ref(props.apiUser);
You also may want to consider using computed methods (see also linked doc section from above) or v-model.
Please note that the marked solution https://stackoverflow.com/a/67820271/2311074 is not working. If you try to update user you will see a readonly error on the console. If you don't need to modify user, you may just use the prop in the first place.
As discussed in comment section, a Vue 2 method that I'm personally fond of in these cases is the following, it will basically make a roundtrip when updating a model.
Parent (apiUser) ->
Child (clone apiUser to user, make changes, emit) ->
Parent (Set changes reactively) ->
Child (Automatically receives changes, and creates new clone)
Parent
<template>
<div class="parent-root"
<child :apiUser="apiUser" #setUserData="setUserData" />
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of child component etc.)
export default {
data() {
apiUser: {
id: 'e134',
age: 27
}
},
methods: {
setUserData(payload) {
this.$set(this.apiUser, 'age', payload);
}
}
}
Child
<template>
<div class="child-root"
{{ apiUser }}
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of components etc.)
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
user: null
},
watch: {
apiUser: {
deep: true,
handler() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
}
}
},
mounted() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
},
methods: {
// Whatever function catching the changes you want to do
setUserData(payload) {
this.$emit('setUserData', this.user);
}
}
}
Apologies for any miss types

Replace a computed getter/setter with mapState and mapMutations

So, I am syncing a computed value to a component and setting it with a computed setter when it syncs back from the component.
My question is: Is it possible to replace a computed getter/setter with mapState and mapMutations or how would one achieve this in a more compact way?
<template>
<SomeComponent :value.sync="globalSuccess"></SomeComponent>
</template>
export default {
//...
computed: {
globalSuccess: {
get() {
return this.$store.state.globalSuccess;
},
set(val) {
this.$store.commit("globalSuccess", val);
}
}
}
}
I tried replacing it like this:
export default {
//...
computed: {
...mapState(["globalSuccess"]),
...mapMutations(["globalSuccess"]),
}
}
But unfortunately mapMutations(["globalSuccess"]) maps this.globalSuccess(value) to this.$store.commit('globalSuccess', value) according to the documentation of vuex.
But since my computed value gets set with globalSuccess = true internally through :value.sync in the template and not this.globalSuccess(true), globalSuccess will never be set to true.
Any idea how this could be possible? Or am I stuck using computed values with getter and setter?
So I just found out about this vuex module https://github.com/maoberlehner/vuex-map-fields which I installed as described on there:
// store.js
import { getField, updateField } from 'vuex-map-fields';
Vue.use(Vuex);
export default new Vuex.Store({
getters: {
getField,
//...
},
mutations: {
updateField,
//...
},
});
And then I made use of mapFields function:
// App.vue
export default {
//...
computed: {
...mapFields(["globalSuccess"]),
}
}
Which apparently dynamically maps to a computed setter and getter exactly as I wanted it:
export default {
//...
computed: {
globalSuccess: {
get() {
return this.$store.state.globalSuccess;
},
set(val) {
this.$store.commit("globalSuccess", val);
}
}
}
}
Here's a syntax I use:
export default {
//...
computed: {
globalSuccess: {
...mapState({ get: 'globalSuccess' }),
...mapMutations({ set: 'globalSuccess' }),
},
},
}
No additional dependencies needed. If you use it a lot, I suppose you could create a helper for it, but it is pretty neat as it is.

How can I reactively update a value in a component from a store value?

I have two components and a basic store as per the docs here: https://v2.vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch.
I want to make it so that when I type into an input the value in a different component is updated by using the store.
Basic example here.
App.vue
<template>
<div id="app">
<h1>Store Demo</h1>
<BaseInputText /> Value From Store: {{ test }}
</div>
</template>
<script>
import BaseInputText from "./components/BaseInputText.vue";
import { store } from "../store.js";
export default {
// This should reactively changed as per the input
computed: {
test: function() {
return store.state.test;
}
},
components: {
BaseInputText
}
};
</script>
BaseInput.vue
<template>
<input type="text" class="input" v-model="test" />
</template>
<script>
import { store } from "../store.js";
export default {
data() {
return {
test: store.state.test
};
},
// When the value changes update the store
watch: {
test: function(newValue) {
store.setTest(newValue);
}
}
};
</script>
store.js
export const store = {
debug: true,
state: {
test: "hi"
},
setTest(newValue) {
if (this.debug) console.log("Set the test field with:", newValue);
this.state.test = newValue;
}
};
I want to make it so that when I type a string into the input the test variable in App.vue is updated. I'm trying to understand how the store pattern works. I'm aware of how to use props.
I also have a working copy here: https://codesandbox.io/s/loz79jnoq?fontsize=14
Updated
2.6.0+
To make store reactive use Vue.observable (added in in 2.6.0+)
store.js
import Vue from 'vue'
export const store = Vue.observable({
debug: true,
state: {
test: 'hi'
}
})
BaseInputText.vue
<input type="text" class="input" v-model="state.test">
...
data() {
return {
state: store.state
};
},
before 2.6.0
store.js
import Vue from 'vue'
export const store = new Vue({
data: {
debug: true,
state: {
test: 'hi'
}
}
})
BaseInputText.vue
<input type="text" class="input" v-model="state.test">
...
data() {
return {
state: store.state
};
}
Old answer
From documentation However, the difference is that computed properties are cached based on their reactive dependencies.
The store is not reactive
Change to
App.vue
data() {
return {
state: store.state
};
},
computed: {
test: function() {
return this.state.test;
}
},
It looks bad but I don't see another way to make it work

Vue: How to use component prop inside mapFields

I have general component and vuex store. For easy two-way binding I use vuex-map-fields. On component side it has mapFields method which creates get&set with mutations.
I want to pass namespace from vuex module with props but it seems to be impossible.
<my-component namespace="ns1" />
// my-component code
export default {
props: ["namespace"],
computed: {
...mapFields(??this.namespace??, ["attr1", "attr2"])
}
}
Of course, there is no way to use this in such way so we don't have access to props. How can I specify namespace as prop in such case?
The problem (as you probably gathered) is that computed properties are constructed before this is available, but you can get around it by deferring resolution of the this.namespace property until the computed property is called (which won't happen until component construction is finished).
The concept is based on this post Generating computed properties on the fly.
The basic pattern is to use a computed with get() and set()
computed: {
foo: {
get() { this.namespace...},
set() { this.namespace...},
}
}
but rather than type it all out in the component we can create a helper function based on the vuex-map-fields mapFields() function (see here for the original).
The normalizeNamespace() function that comes with vuex-map-fields does not support what we want to do, so we drop it and assume the namespace is always passed in (and that the store module uses the standard getField and updateField functions).
I have adapted one of the vuex-map-fields Codesandbox examples here.
Note the namespace is in data rather than props for conveniance, but props should work also.
Template
<template>
<div id="app">
<div>
<label>foo </label> <input v-model="foo" /> <span> {{ foo }}</span>
</div>
<br />
<div>
<label>bar </label> <input v-model="bar" /> <span> {{ bar }}</span>
</div>
</div>
</template>
Helper
<script>
const mapFields2 = (namespaceProp, fields) => {
return Object.keys(fields).reduce((prev, key) => {
const path = fields[key];
const field = {
get() {
const namespace = this[namespaceProp];
const getterPath = `${namespace}/getField`;
return this.$store.getters[getterPath](path);
},
set(value) {
const namespace = this[namespaceProp];
const mutationPath = `${namespace}/updateField`;
this.$store.commit(mutationPath, { path, value });
}
};
prev[key] = field;
return prev;
}, {});
};
export default {
name: "App",
data() {
return {
nsProp: "fooModule"
};
},
computed: {
...mapFields2("nsProp", { foo: "foo", bar: "bar" })
}
};
</script>
Store
import Vue from "vue";
import Vuex from "vuex";
import { getField, updateField } from "vuex-map-fields";
import App from "./App";
Vue.use(Vuex);
Vue.config.productionTip = false;
const store = new Vuex.Store({
modules: {
fooModule: {
namespaced: true,
state: {
foo: "initial foo value",
bar: "initail bar value"
},
getters: {
getField
},
mutations: {
updateField
}
}
}
});
new Vue({
el: "#app",
components: { App },
store,
template: "<App/>"
});

Categories