Vue.js v-for increment some variable in looped component - javascript

I'm trying to display a list of items on an event's agenda.
The event has a start_date end each item on the agenda has a duration in minutes, for example:
event:{
start_date: '2017-03-01 14:00:00',
agendas:[
{id:1,duration:10},
{id:2,duration:15},
{id:3,duration:25},
{id:4,duration:10}
]
}
Now, in my event component, I load agendas with a v-for:
<agenda v-for="(agenda,index) in event.agendas"
:key="agenda.id"
:index="index"
:agenda="agenda">
In agenda component, I want to increment the time at which each item starts:
<div class="agenda">
//adding minutes to start_date with momentJs library
{{ moment(event.start_date).add(agenda.duration,'m') }} //this should increment, not add to the fixed event start date
</div>
Currently it only adds to the fixed event start_date... I would like to show the times 14:00 for event 1, 14:10 for event 2, 14:25 for event 3 and 14:50 for event 4.
How can I increment the value in a v-for directive in Vue.js 2.0?

It looks like you already got an answer that works for you but I'll post this here in case anyone is looking for an alternate solution. The accepted answer might be good for an initial render of the agendas but will start to break if the agendas array is mutated or anything causes the list to re-render because the start time calculation is based on a stored value that gets incremented every iteration.
The code below adds a computed property (based on event.agendas) that returns a new array of of agenda objects, each with an added start property.
Vue.component('agenda', {
template: '<div>Agenda {{ agenda.id }} — {{ agenda.duration }} min — {{ agenda.start }}</div>',
props: {
agenda: Object
}
});
new Vue({
el: "#root",
data: {...},
computed: {
agendas_with_start() {
const result = [];
let start = moment(this.event.start_date);
for(let agenda of this.event.agendas) {
result.push({
id: agenda.id,
duration: agenda.duration,
start: start.format('HH:mm')
});
start = start.add(agenda.duration, 'm');
}
return result;
}
}
});
Then, in the template, the agendas_with_start computed property is used in the v-for:
<div id="root">
<h3>Event Start: {{ event.start_date }}</h3>
<agenda v-for="agenda in agendas_with_start"
:key="agenda.id"
:agenda="agenda"></agenda>
</div>
Here's a working codepen. The benefit of this approach is that if the underlying agendas array is mutated or re-ordered or the event start time changes or anything causes Vue to re-render the DOM, this computed property will be re-evaluated and the start times will be re-calculated correctly.

Vue.js will actually let you bind prop values to methods on the parent's scope, so the easiest way will be to do something like this:
<agenda class="agenda" v-for="(agenda,index) in event.agendas"
:key="agenda.id"
:index="index"
:agenda="agenda"
:start-time="get_start_time(agenda.duration)">
</agenda>
And then the get_start_time() is a method within the parent's scope:
new Vue({
el: '#app',
data: {
time_of_day: '',
event:{
// ...
},
},
methods: {
get_start_time(duration) {
if (this.next_event_start === '') {
this.next_event_start = moment(this.event.start_date);
}
this.event_start = this.next_event_start.format('h:mm a');
this.next_event_start.add(duration, 'minutes');
return this.event_start;
},
},
});
I've made a quick CodePen as a basic example to show it in action, but you'll need to update the actual code to compensate for multiple days.

Related

Angular: Template rerenders Array while its length does not change

Here is a parent component's template:
<ng-container *ngFor="let set of timeSet; index as i">
<time-shift-input *ngIf="enabled"
[ngClass]="{
'mini-times' : miniTimes,
'field field-last': !miniTimes,
'field-central': !canAddSet,
'field-central--long': (canAddSet || canDeleteSet) && !miniTimes }"
[startHour]="set.startHour"
[endHour]="set.endHour"
[endsNextDay]="set.endsNextDay"
[canAddSet]="canAddSet()"
[canDeleteSet]="canDeleteSet(i)"
[required]="true"
(onAddSet)="onAddSet(i)"
(onDeleteSet)="onDeleteSet(i)"
(onChange)="onShiftTimes($event, i)"></time-shift-input>
</ng-container>
Here is the code which will update the timeSet array after onChange event has been triggered:
public onShiftTimes( set: TimeSchedule | Array<TimeSchedule>, ind?: number ): void {
if ( ind !== undefined ) {
this.timeSet[ind] = <TimeSchedule>set;
} else {
this.timeSet = <Array<TimeSchedule>>set;
}
this.timeChanged.emit({
data: this.timeSet,
di: this.dayIndex
});
}
The child component, <time-shift-input> is getting re-rendered every time the onShiftTimes method has been called, EVEN when the the length of the array stays the same.
Which is a bummer, because it breaks user experience in an annoying way (removes focus, etc). I thought that pushing OR updating an index of an existing array won't change the object reference for the array, so the ngFor loop will not be triggered. However ngOnInit in <time-shift-input> is getting called every time after onShiftTimes...
Any ideas how to prevent re-rendering?
RTFM, as they say.
trackByFn to the rescue - that was the simple and correct solution to my problem. More on this gem:
https://angular.io/api/common/NgForOf#ngForTrackBy
https://www.concretepage.com/angular-2/angular-4-ngfor-example#trackBy

v-model not update after settimeout finish Vue [duplicate]

I'm new to Vuejs. Made something, but I don't know it's the simple / right way.
what I want
I want some dates in an array and update them on a event. First I tried Vue.set, but it dind't work out. Now after changing my array item:
this.items[index] = val;
this.items.push();
I push() nothing to the array and it will update.. But sometimes the last item will be hidden, somehow... I think this solution is a bit hacky, how can I make it stable?
Simple code is here:
new Vue({
el: '#app',
data: {
f: 'DD-MM-YYYY',
items: [
"10-03-2017",
"12-03-2017"
]
},
methods: {
cha: function(index, item, what, count) {
console.log(item + " index > " + index);
val = moment(this.items[index], this.f).add(count, what).format(this.f);
this.items[index] = val;
this.items.push();
console.log("arr length: " + this.items.length);
}
}
})
ul {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="app">
<ul>
<li v-for="(index, item) in items">
<br><br>
<button v-on:click="cha(index, item, 'day', -1)">
- day</button>
{{ item }}
<button v-on:click="cha(index, item, 'day', 1)">
+ day</button>
<br><br>
</li>
</ul>
</div>
EDIT 2
For all object changes that need reactivity use Vue.set(object, prop, value)
For array mutations, you can look at the currently supported list here
EDIT 1
For vuex you will want to do Vue.set(state.object, key, value)
Original
So just for others who come to this question. It appears at some point in Vue 2.* they removed this.items.$set(index, val) in favor of this.$set(this.items, index, val).
Splice is still available and here is a link to array mutation methods available in vue link.
VueJS can't pickup your changes to the state if you manipulate arrays like this.
As explained in Common Beginner Gotchas, you should use array methods like push, splice or whatever and never modify the indexes like this a[2] = 2 nor the .length property of an array.
new Vue({
el: '#app',
data: {
f: 'DD-MM-YYYY',
items: [
"10-03-2017",
"12-03-2017"
]
},
methods: {
cha: function(index, item, what, count) {
console.log(item + " index > " + index);
val = moment(this.items[index], this.f).add(count, what).format(this.f);
this.items.$set(index, val)
console.log("arr length: " + this.items.length);
}
}
})
ul {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="app">
<ul>
<li v-for="(index, item) in items">
<br><br>
<button v-on:click="cha(index, item, 'day', -1)">
- day</button> {{ item }}
<button v-on:click="cha(index, item, 'day', 1)">
+ day</button>
<br><br>
</li>
</ul>
</div>
As stated before - VueJS simply can't track those operations(array elements assignment).
All operations that are tracked by VueJS with array are here.
But I'll copy them once again:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
During development, you face a problem - how to live with that :).
push(), pop(), shift(), unshift(), sort() and reverse() are pretty plain and help you in some cases but the main focus lies within the splice(), which allows you effectively modify the array that would be tracked by VueJs.
So I can share some of the approaches, that are used the most working with arrays.
You need to replace Item in Array:
// note - findIndex might be replaced with some(), filter(), forEach()
// or any other function/approach if you need
// additional browser support, or you might use a polyfill
const index = this.values.findIndex(item => {
return (replacementItem.id === item.id)
})
this.values.splice(index, 1, replacementItem)
Note: if you just need to modify an item field - you can do it just by:
this.values[index].itemField = newItemFieldValue
And this would be tracked by VueJS as the item(Object) fields would be tracked.
You need to empty the array:
this.values.splice(0, this.values.length)
Actually you can do much more with this function splice() - w3schools link
You can add multiple records, delete multiple records, etc.
Vue.set() and Vue.delete()
Vue.set() and Vue.delete() might be used for adding field to your UI version of data. For example, you need some additional calculated data or flags within your objects. You can do this for your objects, or list of objects(in the loop):
Vue.set(plan, 'editEnabled', true) //(or this.$set)
And send edited data back to the back-end in the same format doing this before the Axios call:
Vue.delete(plan, 'editEnabled') //(or this.$delete)
One alternative - and more lightweight approach to your problem - might be, just editing the array temporarily and then assigning the whole array back to your variable. Because as Vue does not watch individual items it will watch the whole variable being updated.
So you this should work as well:
var tempArray[];
tempArray = this.items;
tempArray[targetPosition] = value;
this.items = tempArray;
This then should also update your DOM.
Observe object and array reactivity here:
https://v2.vuejs.org/v2/guide/reactivity.html

VueJs - DOM not updating on array mutation

I have a model in which I'm initializing an array on ajax success after the model is mounted
var self = this;
$.getJSON("somejson.json",
function (data) {
var list = [];
list = data.list.map(function (item) {
return { id: item.id, text: item.text };
});
self.selectableItems = list;
});
I have a click method on each of these items which removes the item from selectableItems
select: function (item) {
this.selectableItems.pop(item);
},
selectableItems renders correctly initially, but when I mutate the array, the dom isn't updating. Although the actual array is being modified correctly.
I verified this by having a computed property that returns the count of selectableItems. This count is updated correctly when the item is removed, but the dom still shows the item.
I also noticed that when I hard code the value of selectableItems in the ajax, everything works as expected!
self.selectableItems = [{ id: 1, text: "adsad"}];
I'm aware of the caveats of array mutation in vue. But I feel I'm missing something basic here, as I have just started exploring Vue.
Can someone point out on what I'm missing?
Array.pop() removes the last item from the array, it does not take any argument. It only removes the last item any argument you pass it.
That the reason your computed property showing the array count works as last item is being removed but not the item you want.
Use Array.splice()instead.
pass the index to your click method like this:
<ul>
<li v-for="(item, index) in selectableItems" #click="select(index)>{{item}}</li>
</ul>
script
select: function (index) {
this.selectableItems.splice(index, 1);
},

How do I reverse the order of an array using v-for and orderBy filter in Vue JS?

I am using Vue JS to do viewmodel bindings. In my data object I have an array of items that are sorted in ascending order (oldest to newest) and I'd like to keep it that way for code-based reasons.
var v = new Vue({
el: '#app',
data: {
items: [
{id: 51, message: 'first'},
{id: 265, message: 'second'},
{id: 32, message: 'third'}
],
}
}
However, when I display the array in the template I'd like to reverse the order so that it's descending (newest to oldest). I tried the following:
<ol>
<li v-for="item in items | orderBy -1" track-by="id">
This didn't work since the orderBy filter seems to require a field name as its first argument.
Is there any way to accomplish this in the template using the v-for syntax using the orderBy filter? Or am I going to have to create a custom reverse filter?
Simple and concise solution:
<li v-for="item in items.slice().reverse()">
//do something with item ...
</li>
Instead of reversing the order of the elements for creation, I only change the order of the display.
<ol class="reverseorder">
<li v-for="item in items" track-by="id">
And my CSS
<style>
.reverseorder {
display: flex;
flex-direction: column-reverse;
}
</style>
No need to clone the array and reverse it.
Note: The below works in Vue 1, but in Vue 2 filters are deprecated and you
will see: ' Property or method "reverse" is not defined on the
instance but referenced during render.' See tdom_93's answer for
vue2.
You could create a custom filter to return the items in reversed order:
Vue.filter('reverse', function(value) {
// slice to make a copy of array, then reverse the copy
return value.slice().reverse();
});
Then use it in the v-for expression:
<ol>
<li v-for="item in items | reverse" track-by="id">
https://jsfiddle.net/pespantelis/sgsdm6qc/
Update for Vue2
I want to show some ways that you can work with data and not using filters as they are deprecated in Vue2:
inside computed property
Use computed properties in place of filters, which is much better because you can use that data everywhere in component, not only just in template:
jsFiddle
computed: {
reverseItems() {
return this.items.slice().reverse();
}
}
inside Vuex getter property
If you're using Vuex, and you store your data in store.state object. The best way do some transformation with data stored in state is to do that in getters object (for example filtering through a list of items and counting them, reverse order and so on...)
getters: {
reverseItems: state => {
return state.items.slice().reverse();
}
}
and retrieve state from getters in component computed property:
computed: {
showDialogCancelMatch() {
return this.$store.state.reverseItems;
}
}
Possibly I'm missing some downsides here, but how about iterating over the array from end to start using an index?
<ol>
<li v-for="i in items.length" :set="item = items[items.length - i]">
Like, if your array consists of thousands of elements, copying it with .slice().reverse() every time is probably not the most efficient approach.
Upd.: note, :set is not an official way for defining variables in template, it just works. As an alternative, the item variable could be replaced by a call to some getItem(i) method that would encapsulate the items[items.length - i] expression.
Based on the fact that the directive v-for can accept not only an array but also any other valid JavaScript iterable object (at least in Vue 2.6+ and Vue 3 releases), we can create our own iterable object to loop through a needed array in the opposite direction. I created a very simplified runnable example (for more details - check information about the JavaScript iterator protocol).
class Iterable {
constructor(arr) {
this.arr = arr;
}
*[Symbol.iterator]() {
const arr = this.arr;
for (let i = arr.length - 1; i >= 0; i--) yield arr[i];
}
getIterable(isReversedOrder) {
return isReversedOrder ? this : this.arr;
}
}
Vue.component('list', {
props: ['iterable'],
template: '<ul><li v-for="(el, i) in iterable" :key="`${i}-${el}`">{{ el }}</li></ul>'
});
const app = new Vue({
data() {
return {
todos: new Iterable(['Learn JavaScript', 'Learn Vue', 'Learn Vuex']),
isReversed: true,
inputValue: ''
};
},
computed: {
computedTodos() {
return this.todos.getIterable(this.isReversed);
}
},
methods: {
toggleReverse() {
this.isReversed = !this.isReversed;
},
addTodo() {
this.inputValue && this.todos.arr.push(this.inputValue);
this.inputValue = '';
}
}
});
app.$mount('#app');
<!DOCTYPE html>
<html>
<body style="display: flex; justify-content: center;">
<div id="app">
<button #click="toggleReverse">Toggle reverse to {{ !isReversed }}</button>
<br />
<input v-model="inputValue" style="margin-top:5px;" />
<button #click="addTodo" :disabled="!inputValue">Add todo</button>
<!-- <ul><li v-for="(todo, i) in computedTodos" :key="`${i}-${todo}`">{{ todo }}</li></ul> -->
<list :iterable="computedTodos" />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<script src="script.js"></script>
</body>
</html>
P.S.Try to avoid using such Array.prototype functions as shift/ unshift , reverse etc. to add / remove items from the beginning of the array or reverse the order, especially in the case when such operations are performed frequently and / or an array includes a big quantity of items, because they are quite costly as for performance (have O(n) complexity).
Another good solution is to use CSS to display elements in the reversed order (see an answer above).
The v-for directive doesn't support iterating backwards, so if you want to order by newest you're going to need to add another field to indicate when the item was added, or change id to increment every time an item is added.
Then, with field being the field indicting the order added:
<li v-for="item in items | orderBy 'field' -1" track-by="id">
For my use case (which is admittedly, apparently different than the OP...) I wanted to have the indices of the Array in reverse order in the v-for "loop."
My solution was to create a Vue app method reverseRange(length) that returns an Array of integers from length-1 to 0. I then used that in my v-for directive and simply referred to my Array elements as myArray[index] every time I needed it.
That way, the indices were in reverse order and I was able to then use them to access elements of the Array.
I hope this helps someone who landed on this page with this subtle nuance in their requirements like me.
You can use lodash reverse:
<li v-for="item in _.reverse(items)">

Vue doesn't update data local to component during method call

I've got a vue component that will show a calendar week. The component is meant to be modular so it will not know what days are populated with what dates until it's parent component (the month) passes in the data.
My template looks like this:
<div class="cs-week">
<div class="day" v-for="n in 7">
<!-- I'm still building it out, so for now I jsut want to show the date -->
{{ dayLabels[n] }}
</div>
</div>
The Vue Component looks like this:
module.exports = {
props:[
'events',
'weekdata',
'weeknumber'
],
data: function (){
return {
// initializing each of the day label slots so the view doesn't blow up for not having indexed data when rendered
dayLabels: [
null,
null,
null,
null,
null,
null,
null
]
}
},
methods:{
loadWeek: function (){
for(
var i = this.weekdata.days[0],
j = this.weekdata.dates[0];
i <= this.weekdata.days[1];
i++, j++
){
this.dayLabels[i] = j;
}
},
test: function (){
this.loadWeek();
}
}
}
The data being passed to the component from the parent tells it the range of the days to fill and the dates to use:
weekdata: {
days: [3,6], // start on wednesday, end on saturday
dates: [1,3] // use the 1st through the 3rd for the labels
}
When I fire this method, the data updates, but the bound elements never update:
The thing is, if I hard code an update to the labels array before I iterate through the loop...
loadWeek: function (){
debugger;
this.dayLabels = [1,2,3,3,2,1]; // arbitrary array data assigned
for(
var i = this.weekdata.days[0],
j = this.weekdata.dates[0];
i <= this.weekdata.days[1];
i++, j++
){
this.dayLabels[i] = j;
}
},
... the bound elements will update:
Is there a reason why it won't work without the arbitrary assignment before the loop??
When you change an array by setting a value in it, Vuejs cannot detect the change and wont fire any on-change methods. See here: http://vuejs.org/guide/list.html#Caveats
You can use the $set() method to change an object in an array, and that will force Vue to see the change. So in your for-loop
this.dayLabels.$set(i, j);

Categories

Resources