I'm trying to bind user input from a form to a state in my vuex store.
The state looks like this:
customers: [
{firstName: "", lastName: "", age: ""},
{firstName: "", lastName: "", age: ""},
{firstName: "", lastName: "", age: ""}
]
I've tried to use v-model on a computed property that invokes get and set method. I found an explanation here.
This works perfectly for an object, but unfortunately there is no explanation how to use this on an array of objects.
I'm looking for something like this:
computed: {
firstName: {
get () {
return this.$store.state.customers[i].firstName
},
set (value) {
this.$store.commit('changeFirstname', {value, index})
}
}
}
But obviously this didn't work, because I can't pass the index to the computed property.
Has anybody a solution for this? Is this a good use case for a deep watcher?
This is my first question, please let me know if I forget something or done something wrong, so that I can improve my asking. Thanks!
Vuex can be a bit of a pain when your data isn't flat. See https://forum.vuejs.org/t/vuex-best-practices-for-complex-objects/10143 for more on that.
One way you could approach this is to ditch v-model and use :value and #input directly. I've simulated a store in my example but hopefully it's clear what's going on. You can easily grab the index of the customer if you'd find that easier than passing around the object itself.
const storeCustomers = Vue.observable([
{firstName: "A", lastName: "", age: ""},
{firstName: "B", lastName: "", age: ""},
{firstName: "C", lastName: "", age: ""}
])
function storeGetCustomers () {
return storeCustomers
}
function storeUpdateCustomerFirstName (customer, value) {
customer.firstName = value
}
new Vue({
el: '#app',
computed: {
customers () {
return storeGetCustomers()
}
},
methods: {
onFirstNameInput (customer, value) {
storeUpdateCustomerFirstName(customer, value)
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<div v-for="customer in customers">
<input :value="customer.firstName" #input="onFirstNameInput(customer, $event.target.value)">
</div>
<p>
{{ customers }}
</p>
</div>
An alternative that retains the v-model would be to introduce a separate component to hold each customer. In general when using v-for for anything involving editing it's worth considering introducing a new component.
In this example there's an asymmetry in that the customers are read from the store in the parent and written to the store in the child. I find that a little uncomfortable. Perhaps if each customer had an id then that id could be passed to the child as a prop instead of the customer object, leaving the child to get and set the individual customer it's interested in.
const storeCustomers = Vue.observable([
{firstName: "A", lastName: "", age: ""},
{firstName: "B", lastName: "", age: ""},
{firstName: "C", lastName: "", age: ""}
])
function storeGetCustomers () {
return storeCustomers
}
function storeUpdateCustomerFirstName (customer, value) {
customer.firstName = value
}
const child = {
props: ['customer'],
template: `
<div>
<input v-model="firstName">
</div>
`,
computed: {
firstName: {
get () {
return this.customer.firstName
},
set (value) {
storeUpdateCustomerFirstName(this.customer, value)
}
}
}
}
new Vue({
el: '#app',
components: {
child
},
computed: {
customers () {
return storeGetCustomers()
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<child v-for="customer in customers" :customer="customer"></child>
<p>
{{ customers }}
</p>
</div>
To your question about deep watchers, I believe what you're describing would involve mutating the state outside the store, then having a watcher let the store know about it. There wouldn't actually be anything for the store to do as the state would already have been changed. Obviously this would violate 'the rules' about only mutating store state inside the store's mutations.
Related
I'm trying to pass an array of object to a childComponent as prop but when I add an object in it, it doesn't render. (Note: I'm working on vuejs 2.6)
I suppose it has a link with the "monitoring" of the items of the array and not the array itself? Stuff is that if I do not pass the prop and use the default value instead, it's working perfectly. I think I'm missing something here. Could someone help me ?
By curiosity is this kind of behavior still stand with vue3js ?
As you can see below:
App.vue:
<template>
<div id="app">
<Card
v-for="user in users"
:key="user.userId"
:userId="user.userId"
:username="getUsernameFromUserId(user.userId)"
:links="getUserLinksFromUserId(user.userId)"
/>
</div>
</template>
<script>
import Card from "./components/Card.vue";
export default {
name: "App",
components: {
Card,
},
data: function () {
return {
users: [
{ userId: 1, name: "Bob" },
{ userId: 2, name: "Alice" },
{ userId: 3, name: "Eliot" },
],
links: [
{ userId: 1, link: "hello->world" },
{ userId: 1, link: "world->!" },
{ userId: 3, link: "hello->back" },
{ userId: 4, link: "hello->you" },
],
};
},
methods: {
getUsernameFromUserId: function (userId) {
return this.users.filter((obj) => obj.userId == userId)?.[0]?.name ?? "Not found";
},
getUserLinksFromUserId: function (userId) {
return this.links.filter((obj) => obj.userId == userId);
},
},
};
</script>
Card.vue
<template>
<div class="card">
<h1>{{ username }}</h1>
<button #click="addLink">Add One link</button><br><br>
<span v-if="links.length == 0">No links</span>
<div class="links">
<Link v-for="link in links" :key="links.indexOf(link)" :link="link"></Link>
</div>
</div>
</template>
<script>
import Link from '../components/Link'
export default {
components:{Link},
props: {
userId: Number,
username: String,
links: { type: Array, default: () => [], required: false },
},
methods:{
addLink: function(){
this.links.push({
userId: this.userId,
link: 'newlink->cool'
});
}
}
}
</script>
Link.vue
<template>
<div>
<span>UserId: {{ this.link.userId }} Link: {{ this.link.link }</span>
</div>
</template>
<script>
export default {
props: {
link: { type: Object, default: () => [], required: false },
},
};
</script>
This is a bad way to work with props
Note: do not focus on Dev Tools too much as it can be "buggy" at times - especially if you use Vue in a wrong way. Focus on your app output
Your Card.vue component is modifying (push) a prop, which is not recommended but it sort of works if the prop is object/Array and you do not replace it, just modify it's content (as you do)
But in your case, the values passed to props are actually generated by a method! The getUserLinksFromUserId method is generating a new array every time it is called, and this array is NOT reactive. So by pushing to it, your component will not re-render and what is worse, parent's links array is not changed at all! (on top of that - if App.vue ever re-renders, it will generate new arrays, pass it to pros and your modified arrys will be forgoten)
So intead of modifying links prop in Card.vue, just emit an event and do the modification in App.vue
I have a frozen list of non-frozen data, the intent being that the container is not reactive but the elements are, so that an update to one of the N things does not trigger dependency checks against the N things.
I have a computed property that returns a sorted version of this list. But Vue sees the reactive objects contained within the frozen list, and any change to an element results in triggering the sorted computed prop. (The goal is to only trigger it when some data about the sort changes, like direction, or major index, etc.)
The general concept is:
{
template: someTemplate,
data() {
return {
list: Object.freeze([
Vue.observable(foo),
Vue.observable(bar),
Vue.observable(baz),
Vue.observable(qux)
])
}
},
computed: {
sorted() {
return [...this.list].sort(someOrdering);
}
}
}
Is there a Vue idiom for this, or something I'm missing?
...any change to an element results in triggering the sorted computed prop
I have to disagree with that general statement. Look at the example below. If the list is sorted by name, clicking "Change age" does not trigger recompute and vice versa. So recompute is triggered only if property used during previous sort is changed
const app = new Vue({
el: "#app",
data() {
return {
list: Object.freeze([
Vue.observable({ name: "Foo", age: 22}),
Vue.observable({ name: "Bar", age: 26}),
Vue.observable({ name: "Baz", age: 32}),
Vue.observable({ name: "Qux", age: 52})
]),
sortBy: 'name',
counter: 0
}
},
computed: {
sorted() {
console.log(`Sort executed ${this.counter++} times`)
return [...this.list].sort((a,b) => {
return a[this.sortBy] < b[this.sortBy] ? -1 : (a[this.sortBy] > b[this.sortBy] ? 1 : 0)
});
}
}
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<div id="app">
<div>Sorted by: {{ sortBy }}</div>
<hr>
<button #click="list[0].name += 'o' ">Change name</button>
<button #click="list[0].age += 1 ">Change age</button>
<hr>
<button #click="sortBy = 'name'">Sort by name</button>
<button #click="sortBy = 'age'">Sort by age</button>
<hr>
<div v-for="item in sorted" :key="item.name">{{ item.name }} ({{item.age}})</div>
</div>
I am very new to VueJS.
From what I've seen there is probably an elegant answer to this. I have a table of records. Clicking on one of them opens a modal and loads that row/record. My code looks like this (made easier to read):
Javascript
app = new Vue({
el: '#app',
data: {
records: [], //keys have no significance
focusRecord: { //this object will in the modal to edit, initialize it
id: '',
firstname: '',
lastname: ''
},
focusRecordInitial: {}
},
created: function(){
//load values in app.records via ajax; this is working fine!
app.records = ajax.response.data; //this is pseudo code :)
},
methods: {
loadRecord: function(record){
app.focusRecord = record; // and this works
app.focusRecordInitial = record;
}
}
});
Html
<tr v-for="record in records">
<td v-on:click="loadRecord(record)">{{ record.id }}</td>
<td>{{ record.firstname }} {{ record.lastname }}</td>
</tr>
What I'm trying to do is really simple: detect if focusRecord has changed after it has been loaded into the modal from a row/record. Ideally another attribute like app.focusRecord.changed that I can reference. I'm thinking this might be a computed field which I'm learning about, but again with Vue there may be a more elegant way. How would I do this?
What you need is to use VueJS watchers : https://v2.vuejs.org/v2/guide/computed.html#Watchers
...
watch : {
focusRecord(newValue, oldValue) {
// Hey my value just changed
}
}
...
Here is another way to do it, however I didn't know what's refers "focusRecordInitial"
new Vue({
el: '#app',
data() {
return {
records: [],
focusRecordIndex: null
}
},
computed : {
focusRecord() {
if (this.focusRecordIndex == null) return null
if (typeof this.records[this.focusRecordIndex] === 'undefined') return null
return this.records[this.focusRecordIndex]
}
},
watch : {
focusRecord(newValue, oldValue) {
alert('Changed !')
}
},
created() {
this.records = [{id: 1, firstname: 'John', lastname: 'Doe'}, {id: 2, firstname: 'Jane', lastname: 'Doe'}, {id: 3, firstname: 'Frank', lastname: 'Doe'}]
},
methods : {
loadRecord(index) {
this.focusRecordIndex = index
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<table>
<tr v-for="(record, i) in records">
<td><button #click="loadRecord(i)">{{ record.id }}</button></td>
<td>{{ record.firstname }} {{ record.lastname }}</td>
</tr>
</table>
{{focusRecord}}
</div>
Vue Watchers
Vue provides a more generic way to react to data changes through the watch option. This is most useful when you want to perform asynchronous or expensive operations in response to changing data.
Vue JS Watchers
You can do something like this:
app = new Vue({
el: '#app',
data: {
records: [], //keys have no significance
focusRecord: { //this object will in the modal to edit, initialize it
id: '',
firstname: '',
lastname: ''
},
focusRecordInitial: {}
},
created: function(){
//load values in app.records via ajax; this is working fine!
app.records = ajax.response.data; //this is pseudo code :)
},
methods: {
loadRecord: function(record){
app.focusRecord = record; // and this works
app.focusRecordInitial = record;
}
},
watch: {
loadRecord: function () {
alert('Record changed');
}
}
});
You can also check out: Vue JS Computed Properties
You can set up a watcher to react to data changes as:
watch: {
'focusRecord': function(newValue, oldValue) {
/* called whenever there is change in the focusRecord
property in the data option */
console.log(newValue); // this is the updated value
console.log(oldValue); // this is the value before changes
}
}
The key in the watch object is the expression you want to watch for the changes.
The expression is nothing but the dot-delimited paths of the property you want to watch.
Example:
watch: {
'focusRecord': function(newValue, oldValue) {
//do something
},
'focusRecord.firstname': function(newValue, oldValue){
//watch 'firstname' property of focusRecord object
}
}
I would like to know how to render two html elements that reference different levels in the same object.
Let's say I have a global deep object:
var sports = [{
sportName: 'soccer',
teams: [{
teamName: 'TeamA'
},
{
teamName: 'TeamB'
}
]
},
{
sportName: 'basketball',
teams: [{
teamName: 'TeamC'
},
{
teamName: 'TeamD'
}
]
}
]
Now, I want to two unordered lists that represent the different levels in the hierarchy of that object.
<ul id="sports">
<li v-for:"sport in sports">
<span>{{ sport.sportName }}</span>
</li>
</ul>
<script>
var sportsList = new Vue({
el:'#sports',
data: {
sports: sports
}
})
</script>
Here is the other list, in a completely different part of the app:
<ul id="teams">
<li v-for:"team in teams">
<span>{{ team.teamName }}</span>
</li>
</ul>
<script>
var sportsList = new Vue({
el:'#teams',
data: {
teams: sports[sportName].teams
}
})
</script>
My questions are these:
1) Will rendering two seperate instances of the different levels in the sports data object still result in data reactivity in each of those instances?
2) I've noticed that as soon as I instantiate the first list (sports), the get and set for the nested items (teams) are stored in the prototype... which leads me on to the next question. Does it make sense to instantiate a second Vue instance for teams, when it has already been instantiated in the first? I'm finding it difficult to navigate deeper objects within Vue :(
I think the best approach is to use a computed, rather than trying to copy parts of an array to data, which can cause you to end up in a bit of a mess. So in your case I can see you're trying to get teams for a sport, so I would set up a computed that returns teams for a sport stored in data:
computed: {
teams() {
return this.sports.filter(sport => {
return this.sportName === sport.sportName
})[0].teams;
}
},
Now all you need to do is set a data property called sportName which that filter can react to, so the full view model looks like this:
var app = new Vue({
el: '#app',
computed: {
teams() {
return this.sports.filter(sport => {
return this.sportName === sport.sportName
})[0].teams;
}
},
methods: {
setSport(name) {
this.sportName = name;
}
},
data: {
sportName: 'basketball',
sports: [{
sportName: 'soccer',
teams: [{
teamName: 'TeamA'
}, {
teamName: 'TeamB'
}]
}, {
sportName: 'basketball',
teams: [{
teamName: 'TeamC'
}, {
teamName: 'TeamD'
}]
}]
}
});
And here's the JSFiddle: https://jsfiddle.net/hgw2upzf/
I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent