I need to update some Array data in my VueJS component which is rendering as a list in the template via v-for.
When I update the whole Array, I see that the list updates in the DOM. But, if I update only an index, the list does not update.
Here are the two relevant methods:
methods: {
loadOnlyOneIndex: function() {
this.data[0] = {
title: 'Hello error',
slug: 'hello',
desc: 'will not work'
};
},
loadEverything: function() {
this.data = [{
title: 'Hello new title',
slug: 'hello',
desc: 'this will work'
}, {
title: 'Hello new title 2 !',
slug: 'hello2',
desc: 'really work'
}];
}
}
Here is a fiddle.
From the documentation:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
To get Vue to react to the change of an array's index, use the Vue.set() method.
In your case, you should use Vue.set(this.data, 0, newValue) in your loadOnlyOneIndex method.
Here's a working fiddle.
Every Vue instance also has an alias to the Vue.set method via vm.$set (where vm is the Vue instance).
So, you could also use this.$set(this.data, 0, newValue) in your loadOnlyOneIndex method.
This is helpful when using Single File Components, or anywhere where you don't have a direct reference to the Vue object.
Related
My Component:
props: {
answerOptions: Array
},
data: function() {
return {
selectedOption: null,
//selectedOption: {}, also not working
};
},
methods: {
select(option, i){
//this.selectedOption = this.answerOptions.splice(i, 1); not working
//Object.assign(this.selectedOption, this.answerOptions.splice(i, 1)); also not working
this.selectedOption = Object.assign({}, this.answerOptions.splice(i, 1)); //still not
working!
console.log(this.selectedOption) //prints a observable object with alle the correct values
console.log(this.selectedOption.anyAttribute) //prints undefined
}
edit:
thats where the select function is called:
<div
ref="option-elems"
v-on:click="select(option, i)"
v-for="option, i in answerOptions"
>
The answerOption array gets rendered via a for loop just fine. When the select() Method is called on one of the answerOption objects it removes it from the array and updates that rendered list in the ui correctly. I can even conditionally render (v-if="selectedOption") with the selectedOption Object. But for the love of god, im am not able to access any of its Attributes like so: {{ selectedOption.anyAttribute }}
What am i doing wrong here?
You need to get the object inside the array that is called "selectedOption". When you define this, you are still having the object wrapped in an array, so simply get its first item:
this.selectedOption = answers.splice(i, 1)[0];
Demo Here
I am trying to update an array inside of my array (nested array) with react-redux. I found a solution to how to do this but is there any easier way of doing this rather than passing multiple parameter to the action.
[types.HISTORY_UPDATE](state, action){
return {
...state,
budgets: [
...state.budgets.slice(0,action.id),
{
key: action.key,
id: action.idd,
name: action.name,
budgetType: action.budgetType,
startDate: action.startDate,
currency: action.currency,
amount: action.amount,
amountLeft: action.amountLeft,
rollOver: action.rollOver,
color: action.color,
iconName: action.iconName,
history: [
...state.budgets[action.id].history.slice(0,action.histId),
{
note: action.note,
amount: action.amount,
type: action.type,
date: action.date,
fullDate: action.fullDate,
hours: action.hours,
min: action.min,
month: action.month,
year: action.year
},
...state.budgets[action.id].history.slice(action.histId+1)
]
},
...state.budgets.slice(action.id+1)
]
}
},
and the action goes like this
export function updateHistory(id,key,idd,name,budgetType,startDate,currency,amount,amountLeft,rollOver,color,iconName,histId,........){
I don't want to spend time with passing multiple parameter like this while using react-redux and also while I tried to run my application on my phone sometimes it really slows the application. Is it because of the example above?
I would be really appreciated If you guys come up with a solution.
I typically do not store arrays in redux, since updating a single element really is a burden as you noticed. If the objects you have inside your array all have a unique id, you can easily convert that array to an object of objects. As key for each object you take that id.
const convertToObject = (array) => {
let items = {};
array.map((item) => {
items[item.id] = item;
});
return items;
};
In your action you simply just pass the item you want to update as payload, and you can update the redux store very easily. In this example below I am just updating the budgets object, but the same logic applies when you assign a history object to each budget.
[types.BUDGET_UPDATE](state, action){
const item = action.payload;
return {
...state,
budgets: {
...state.budgets,
[item.id]: item
}
}
}
And if you want an array somewhere in your component code, you just convert the redux store object back to an array:
const array = Object.values(someReduxStoreObject);
I have the following component tree:
App
List
ItemDetails
Widget1
...
the List component has a filters form, those are applied on button press
Widget1 has a button which applies another filter (by id) to the list, applying that one removes filters from the filter form and vice versa
the list is also live-updated via polling (later will be via WebSockets), so I have to separate v-model of the filter fields in List and the actually applied filters (those are in the store)
Currently, I've put the following state to the VueX store:
state: {
filters: {
// these come from the List's filter panel
result: '',
name: '',
date: '',
// this one comes from Widget1
id: '',
},
pagination: {
perPage: 10,
page: 0,
total: 0,
},
items: [],
selectedItem: null,
},
and both filters from the List and from the button in Widget1 are applied via dispatching the following action to the store:
applyFilters ({ state, commit, dispatch }, filters) {
if(filters.id) {
for(let filterName in state.filters)
if(filterName !== 'id')
filters[filterName] = '';
} else {
filters.id = '';
}
commit('setFilters', filters);
commit('setPage', 0);
dispatch('loadExaminations');
},
But here's the problem: the List component has its model of filter fields (on press of the button those are gathered and applyFilters is dispatched). This works well except when the id filter (from Widget1) is applied, the filter fields in the List component are not emptied.
One obvious option to handle this is to move the model of those filter fields to the store as well. That doesn't look that nice since for each field (that uses v-model) I have to create a computed (in the List component) and write a setter and a getter from the store. It seems nicer to send an event from applyFilters to List saying "empty the filter fields", but I'm not sure if VueX can do this. Is this possible? Or should I stick with the "move filter fields' model to the store" plan? Or do you know a nice alternative? I'm aware of the event bus concept, but that one uses Vue instance which shouldn't be used in store, I guess.
You can listen to vuex's actions by using subscribeAction.
// List.vue
mounted() {
this.$store.subscribeAction({
before: (action, state) => {
if (action.type === "applyFilters" && action.payload.id) {
this.emptyComponentFields();
}
},
after: (action, state) => {}
});
}
CodeSandbox.
I have a list of objects that can be updated from the database.
So, when I load the list, objects have only id and name.
When I click on an object I load other fields that can be of any length - that's why I don't load them with the objects in the list.
I found that when I update an object it can be difficult to keep reactivity https://v2.vuejs.org/v2/guide/reactivity.html so I need to find some workaround.
this code works almost okay:
axios.get('/api/news', item.id).then(function(response){
if (response){
Object.assign(item, response.data.item);
}
});
But the problem is the fields that have not been presented from the beginning is not 100% reactive anymore. What is going on is a new field has been updated reactively only when I change another, previous one. So, if I show 2 text field with old and new properties, if I change the second property, the field will not be updated until I change the first one.
I got item object from the component:
data () {
return {
items: [],
}
},
and
<div v-for="item in items" #click="selectItem(item)" >
<span>{{item.name}}</span>
</div>
Then item's been passed to the function selectItem.
What is the proper pattern to load new fields and keep them reactive? (NB: it's not the case when I can assign field by field - I want to reuse the same code no matter which object it is, so I need so the solution for updating an object at a time without listing all new fields.)
Note. This code works inside the component.
Completely revised post: Ok, the example you give uses an array, which has its own caveats, in particular that you can't directly set values like vm.items[indexOfItem] = newValue and have it react.
So you have to use Vue.set with the array as the first argument and the index as the second. Here's an example that adds a name property to object items and then uses Vue.set to set the item to a new object created by Object.assign.
new Vue({
el: '#app',
data: {
items: [{
id: 1,
other: 'data'
}, {
id: 2,
other: 'thingy'
}]
},
methods: {
selectItem(parent, key) {
const newObj = Object.assign({}, parent[key], {
name: 'some new name'
});
Vue.set(parent, key, newObj);
setTimeout(() => {parent[key].name = 'Look, reactive!';}, 1500);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="item, index in items" #click="selectItem(items, index)">
<span>{{item.name || 'empty'}}</span>
</div>
<pre>
{{JSON.stringify(items, null, 2)}}
</pre>
</div>
Have a look at Change-Detection-Caveats Vue cannot detect property addition or deletion if you use "normal assign" methods.
You must use Vue.set(object, key, value)
Try something like the following:
axios.get('/api/news', item.id).then(function(response){
if (response){
let item = {}
Vue.set(item, 'data', response.data.item)
}
});
Than item.data would than be reactiv.
Can simply use Vue.set to update this.item reactively
axios.get('/api/news', item.id).then(function(response){
if (response){
this.$set(this, "item", response.data.item);
}
});
I have a VueJS address lookup component.
Vue.component('address-lookup',
{
template: '#address-lookup-template',
data: function()
{
return {
address: {'name': '', 'town:': '', 'postcode': ''},
errors: {'name': false, 'town': false, 'postcode': false},
states: {'busy': false, 'found': false},
result: {}
}
},
methods:
{
findAddress: function(event)
{
if( typeof event === 'object' && typeof event.target === 'object' )
{
event.target.blur();
}
$.ajax(
{
context: this,
url: '/lookup',
data:
{
'name': this.address.name,
'town': this.address.town,
'postcode': this.address.postcode
},
success: function(data)
{
this.states.busy = false;
this.states.found = true;
this.address.name = data.name;
this.result = data;
}
});
},
reset: function()
{
this.states.found = false;
this.result = {};
}
}
});
Inside my template I've then bound the result like so:
<p>{{ result.formatted_address }}</p>
There is some extra data returned within the result (like a twitter handle) that isn't part of the address lookup template, and occurs on a separate part of the form. For reasons relating to how my form is structured I can't include these inputs within the same template.
I found a way to bind those inputs, although it felt somewhat 'hacky'.
<input type="text" name="twitter" v-model="$refs.lookupResult._data.result.twitter">
That all works fine.
My problem is that the form is included as part of a larger template sometimes in the context of creating a new record, sometimes in the context of editing. When editing a record, the lookup component is removed (using an if server-side, so the template is no longer loaded at all) and when that happens I get this error.
$refs.lookupResult._data.result.twitter": TypeError: Cannot read property '_data' of undefined
This makes sense. lookupResult is defined when I include the template, and when editing I am removing this line:
<address-lookup v-ref:lookup-result></address-lookup>
I've worked around it by including a version of each extra input without the v-model attribute, again using a server-side if. But there are quite a few of these and it's getting a bit messy.
Is there a cleaner approach I could be using to better achieve this?
So I don't know the hierarchy of your layout, it isn't indicated above, but assuming that address-lookup component is a child of your parent, and you in fact need the results of address lookup in that parent, eg:
<parent-component> <!-- where you need the data -->
<address-lookup></address-lookup> <!-- where you lookup the data -->
</parent-component>
then you can simply pass the data props, either top-down only (default) or bidirectionally by defining 'address' for example on your parent's vue data hook:
// parent's data() function
data = function () {
return {
address: {}
}
}
// parent template, passed address with .sync modifier (to make it bi-directional)
<parent-component>
<address-lookup :address.sync='address'></address-lookup>
</parent-component>
// have the props accepted in the address look up component
var addressComponent = Vue.extend({
props: ['address']
})
Now in your $.ajax success function, simply set the props you need on this.address. Of course you can do this with all the props you need: errors, results, state etc. Even better, if you can nest them into a single key on the parent, you can pass the single key for the object containing all four elements instead of all four separately.