In the code below I'm 2 way binding the output of a textarea into a p element, once from the component's internal state and once from Vuex. The Vuex state does show the initial value, but the value doesn't update as I add or delete text (as it does correctly with the 1st textarea bound to the internal data). What is the difference that is causing this issue?
Component code:
<template>
<div>
<div>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="internal_state"></textarea>
<p> {{ internal_state }}</p>
<hr>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="this.$store.state.vuex_state"></textarea>
<p> {{ this.$store.state.vuex_state }}</p>
<hr>
</div>
</div>
</template>
<script>
export default {
name: 'WriteArea',
data () {
return {
internal_state: ''
}
},
methods: {
}
}
</script>
Vuex code:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
strict: true,
state: {
counter: 0,
vuex_state: 'starting string'
},
getters: {
vuex_getter1: (state) => {
return state.vuex_string
}
}
})
Vuex state should be updated via a mutation. See the documentation for this exact problem. Solution is not to use v-model, but instead to bind to the :value of the textarea and then have a custom event to mutate the Vuex state on input:
https://vuex.vuejs.org/en/forms.html
<input :value="message" #input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
The other option is to create a setter and getter in the same computed property:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
You can try to use mine library for 2 way binding vuex problem solution
https://github.com/yarsky-tgz/vuex-dot
Example:
<template>
<input v-model="name"/>
<input v-model="email"/>
<button #click.stop="step++">next</button>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
step: takeState('wizard.step')
.commit('setWizardStep')
.map(),
...takeState('user')
.expose(['name', 'email'])
.commit('editUser')
.map()
}
}
</script>
store/index.js
export default new Vuex.Store({
state: {
wizard: {
step: 1
},
user: {
name: 'John',
email: 'john#doe.com'
}
},
mutations: {
setWizardStep(state, step) {
state.wizard.step = step;
},
editUser(state, patch) {
state.user = Object.assign({}, state.user, patch);
}
}
});
Using: vuex-map-fields
from vuex-map-fields repo:
Enable two-way data binding for form fields saved in a Vuex store.
Vuex Store
import Vue from 'vue'
import Vuex from 'vuex'
import { getField, updateField } from 'vuex-map-fields';
Vue.use(Vuex)
export const store = new Vuex.Store({
strict: true,
state: {
counter: 0,
vuex_state: 'starting string'
},
getters: {
getField, // Add the `getField` getter to the `getters` of your Vuex store.
vuex_getter1: (state) => {
return state.vuex_string
}
}
mutations: {
updateField, // Add the `getField` getter to the `getters` of your Vuex store.
}
})
Component code:
<template>
<div>
<div>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="internal_state"></textarea>
<p> {{ internal_state }}</p>
<hr>
<textarea name="textarea1" id="txtid" cols="40" rows="30" v-model="vuex_state"></textarea>
<p> {{ vuex_state }}</p>
<hr>
</div>
</div>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
name: 'WriteArea',
data () {
return {
internal_state: ''
}
},
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([
'vuex_state ',
]),
},
}
</script>
Related
I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}
So i have a Vuex Store setup which returns me headers and desserts. Inside my desserts i have a property setup named display which is initially false. I have a component Row which i am importing inside my parent component named Table. Row Components accepts a couple of props i.e. Name and Display. The display prop is what is returned from desserts from the vuex store. I am trying to add a mutation such that on Click of the icon in my Row component, the display.laptop can be toggled to true and false. I have setup the toggleLaptopDisplay but i keep getting cannot read property laptop of undefined.
Please look at the complete CodeSandbox.
Here is the complete code:
The Vuex Store:-
export default new Vuex.Store({
state: {
headers: [{ text: "Dessert (100g serving)", value: "name" }],
desserts: [
{ name: "Lollipop", display: { laptop: true } },
{ name: "Marshamallow", display: { laptop: false } }
]
},
getters: {
getHeaders: state => state.headers,
getDesserts: state => state.desserts
},
mutations: {
toggleLaptopDisplay(state) {
state.desserts.display.laptop = !state.desserts.display.laptop;
}
}
});
This is the Table Component:-
<template>
<v-data-table :headers="getHeaders" :items="getDesserts" hide-actions select-all item-key="name">
<template v-slot:headers="props">
<tr>
<th v-for="header in props.headers" :key="header.text">{{ header.text }}</th>
</tr>
</template>
<template v-slot:items="props">
<tr>
<Row :name="props.item.name" :display="props.item.display"/>
</tr>
</template>
</v-data-table>
</template>
<script>
import { mapGetters } from "vuex";
import Row from "./Row";
export default {
components: {
Row
},
data() {
return {};
},
computed: {
...mapGetters({
getHeaders: "getHeaders",
getDesserts: "getDesserts"
})
}
};
</script>
This is the Row component:-
<template>
<div>
{{name}}
<v-icon #click="toggleLaptopDisplay" :color="display.laptop ? 'info': '' ">smartphone</v-icon>
</div>
</template>
<script>
import { mapMutations } from "vuex";
export default {
props: {
name: String,
display: Object
},
methods: {
...mapMutations({
toggleLaptopDisplay: "toggleLaptopDisplay"
})
}
};
</script>
Any help will be appreciated. Thank you :)
Few things to achieve what you want:
In your Row.vue add a method to select appropriate element:
<v-icon #click="toggleChange" :color="display.laptop ? 'info': '' ">smartphone</v-icon>
then in methods, create a method that will pass name of the element as payload:
methods: {
toggleChange() {
this.toggleLaptopDisplay(this.name)
},
...mapMutations({
toggleLaptopDisplay: "toggleLaptopDisplay"
})
}
finally, in store.js, use the payload and mutate selected element:
mutations: {
toggleLaptopDisplay(state, payload) {
state.desserts.find(dessert => dessert.name === payload).display.laptop = !state.desserts.find(dessert => dessert.name === payload).display.laptop
}
}
Edited Example
I have two components. Child component emits an 'input' event when it's value changed and parent component takes this value with v-model. I'm testing ChildComponent. I need to write a test with Vue-test-utils to verify it works.
ParentComponent.vue:
<template>
<div>
<child-component v-model="search"></child-component>
<other-component></other-component>
...
</div>
</template>
ChildComponent.vue:
<template>
<input :value="value" #change="notifyChange($event.target.value)"></input>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
#Component
export default class ChildComponent extends Vue {
#Prop({ default: '' }) readonly value!: string
notifyChange(value: string) {
this.$emit('input', value)
}
}
</script>
child-component.spec.ts:
describe('ChildComponent', () => {
let wrapper: any
before(() => {
wrapper = VueTestUtils.shallowMount(ChildComponent, {})
})
it(`Should emit 'input' event when value change`, () => {
const rootWrapper = VueTestUtils.shallowMount(ParentComponent)
wrapper.vm.value = 'Value'
wrapper.findAll('input').at(0).trigger('change')
assert.isTrue(!!rootWrapper.vm.search)
})
})
I didn't write the exact same code but the logic is like this.
My components work properly. 'child-component.spec.ts' doesn't work. I need to write a test for it.
TESTED.
This is a simple way of testing an emit, if someone is looking for this.
in your test describe write this.
describe('Close Button method', () => {
it('emits return false if button is clicked', (done) => {
wrapper.find('button').trigger('click')
wrapper.vm.$nextTick(() => {
wrapper.vm.closeModal() //closeModal is my method
expect(wrapper.emitted().input[0]).toEqual([false]) //test if it changes
done()
})
})
})
my vue comp
<div v-if="closeButton == true">
<button
#click="closeModal()"
>
...
</button>
</div>
my props in vue comp
props: {
value: {
type: Boolean,
default: false
},
my methods in vue comp
methods: {
closeModal() {
this.$emit('input', !this.value)
}
}
Here's an example that will help you:
Child Component.vue
<template>
<div>
<h2>{{ numbers }}</h2>
<input v-model="number" type="number" />
<button #click="$emit('number-added', Number(number))">
Add new number
</button>
</div>
</template>
<script>
export default {
name: "ChildComponent",
props: {
numbers: Array
},
data() {
return {
number: 0
};
}
};
</script>
Parent Component.vue
<template>
<div>
<ChildComponent
:numbers="numbers"
#number-added="numbers.push($event)"
/>
</div>
</template>
<script>
import ChildComponent from "./ChildComponent";
export default {
name: "ParentComponent",
data() {
return {
numbers: [1, 2, 3]
};
},
components: {
ChildComponent
}
};
</script>
Follow this article: https://medium.com/fullstackio/managing-state-in-vue-js-23a0352b1c87
I hope this will help you.
in your parent component listen to the emitted event "input"
<template>
<div>
<child-component #input="get_input_value" v-model="search"></child-component>
<other-component></other-component>
...
</div>
</template>
and in your script add method get_input_value()
<script>
...
methods:
get_input_value(value){
console.log(value)
}
</script>
I'm new to Vue and I wanted to learn Veux by building a simple CRUD application with firebase. So far I've been able to figure things out (though if you see something badly coded, I would appreciate any feedback) but I can't seem to figure out how to remove an item. The main problem is that I can't reference it properly. I'm getting [object Object] in my reference path but when I log it I get the correct id.
Firebase Flow:
So I'm making a reference to 'items', Firebase generates a unique key for each item and I added in an id to be able to reference it, though I could also reference it by the key.
I've been able to do this without using Veux and just component state but I've been trying for hours to figure out what I'm doing wrong.
I'm also getting this error:
Store.js
import Vue from 'vue'
import Vuex from 'vuex'
import database from './firebase'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
items: []
},
mutations: {
RENDER_ITEMS(state) {
database.ref('items').on('value', snapshot => {
state.items = snapshot.val()
})
},
ADD_ITEM(state, payload) {
state.items = payload
database.ref('items').push(payload)
},
REMOVE_ITEM(index, id) {
database.ref(`items/${index}/${id}`).remove()
}
},
// actions: {
// }
})
Main.vue
<template>
<div class="hello">
<input type="text" placeholder="name" v-model="name">
<input type="text" placeholder="age" v-model="age">
<input type="text" placeholder="status" v-model="status">
<input type="submit" #click="addItem" />
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
{{ item.age }}
{{ item.status }}
<button #click="remove(index, item.id)">Remove</button>
</li>
</ul>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import uuid from 'uuid'
export default {
name: 'HelloWorld',
created() {
this.RENDER_ITEMS(this.items)
},
data() {
return {
name: '',
age: '',
status: '',
id: uuid(),
}
},
computed: {
...mapState([
'items'
])
},
methods: {
...mapMutations([
'RENDER_ITEMS',
'ADD_ITEM',
'REMOVE_ITEM'
]),
addItem() {
const item = {
name: this.name,
age: this.age,
status: this.status,
id: this.id
}
this.ADD_ITEM(item)
this.name = ''
this.age = ''
this.status = ''
},
remove(index, id) {
console.log(index, id)
this.REMOVE_ITEM(index, id)
}
}
}
</script>
The first argument to your mutation is always the state.
In your initial code:
REMOVE_ITEM(index, id) {
database.ref(`items/${index}/${id}`).remove()
}
index is the state object, which is why you are getting [object Object] in the url.
To fix your issue, pass an object to your mutation and change it to:
REMOVE_ITEM(state, {index, id}) {
database.ref(`items/${index}/${id}`).remove()
}
And when you are calling your mutation with the remove method, pass an object as well:
remove(index, id) {
console.log(index, id)
// pass an object as argument
// Note: {index, id} is equivalent to {index: index, id: id}
this.REMOVE_ITEM({index, id})
}
I'm passing store data (Vuex) as a property of component but it's giving me mutation errors even though I'm not changing the data.
Edit: Codepen illustrating error: https://codesandbox.io/s/v8onvz427l
Input
<template>
<div>
<input type="text" class="form-control" ref="input" />
<div style="padding-top: 5px">
<button #click="create" class="btn btn-primary btn-small">Create</button>
</div>
{{ example }}
</div>
</template>
<script>
import store from "#/store"
export default {
props: {
"example": {
}
},
data() {
return {
store
}
},
methods: {
create() {
store.commit("general_set_creation_name", {name: this.$refs.input.value})
}
}
}
</script>
Modal.vue
<template src="./Modal.html"></template>
<script>
import $ from 'jquery'
import store from '#/store'
export default {
props: {
"id": String,
"height": {
type: String,
default: "auto"
},
"width": {
type: String,
default: "40vw"
},
"position": {
type: String,
default: "absolute"
},
"component": {
default: null
},
"global": {
default: true
}
},
data () {
return {
store: store
}
},
computed: {
body () {
return store.state.General.modal.body
},
props () {
return store.state.General.modal.props
},
title () {
return store.state.General.modal.title
},
},
methods: {
close_modal (event) {
if (event.target === event.currentTarget) {
this.$refs.main.style.display = "none"
}
}
}
}
</script>
<style scoped lang="scss" src="./Modal.scss"></style>
Modal.html
<div
:id="id"
class="main"
ref="main"
#click="close_modal"
>
<div ref="content" class="content" :style="{minHeight: height, minWidth: width, position}">
<div ref="title" class="title" v-if="title">
{{ title }}
</div>
<hr v-if="title" />
<div ref="body" class="body">
<slot></slot>
<component v-if="global" :is="body" v-bind="props"></component>
</div>
</div>
</div>
Changing store data with this line in a third component:
store.commit("general_set_modal", {body: Input, title: "New "+page, props: {example: "example text 2"})
I'm quite sure you should not put a vue component on the state. If you are supposed to do that then I don't think the creators of vuex understand how an event store works.
In the documentation it also says you need to initialize your state with values and you don't do that.
Your sandbox works fine when removing the vue component from the state (state should contain data but vue components are objects with both data and behavior).
index.js in store:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
modal: {
body: {},
title: "",//det it to something
props: {}
},
creationName: null
},
mutations: {
general_set_creation_name(state, payload) {
state.creationName = payload.name;
},
general_set_modal(state, payload) {
state.modal.title = payload.title;
state.modal.props = payload.props;
console.log("we are fine here");
}
},
strict: process.env.NODE_ENV !== "production"
});
For whatever reason, changing the way I import the class removes the warning
const test = () => import('./Test')
Details:
https://forum.vuejs.org/t/getting-vuex-mutation-error-when-im-only-reading-the-data/27655/11