How to v-for iterate in a specific order? - javascript

I have and Array of Objects whose elements are randomly ordered. I would like to list the values in a specific order (of the keys).
As an example, the iteration below just lists them:
var vm = new Vue({
el: "#root",
data: {
all: [{
second: 2,
third: 3,
first: 1
},
{
third: 30,
first: 10,
second: 20
}
],
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.js"></script>
<div id="root">
<div v-for="a in all">
<div v-for="(v, k) in a">{{v}}</div>
</div>
</div>
Is this possible to drive the iteration so that it is ordered according to a list of keys (["first", "second", third"]) which would yield
1
2
3
10
20
30

I don't know vue but you can do it like this in javascript.
<div v-for="k in Object.keys(a).sort()">{{k}}:{{a[k]}}</div>
Also note that alphabetic sorting accidentally fits into your need, but you might need a custom sort function like sort((a,b)=>order.indexOf(a)-order.indexOf(b)) with your custom order order: ["first","second","third","fourth"] which may not be alphabetic.
var vm = new Vue({
el: "#root",
data: {
all: [{
second: 2,
third: 3,
first: 1,
fourth: 4
},
{
third: 30,
first: 10,
second: 20,
fourth: 40
}
],
order: ["first","second","third","fourth"]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.js"></script>
<div id="root">
<div v-for="a in all">
<div v-for="k in Object.keys(a).sort((a,b)=>order.indexOf(a)-order.indexOf(b))">{{k}}:{{a[k]}}</div>
<hr/>
</div>
</div>

You can put your list of sorted keys in an array and v-for over that instead.
<div v-for="a in all">
<div v-for="key in presortedListOfKeys">
{{a[key]}}
</div>
</div>

You can use computed properties and get the all sorted first before iteration through them.

In Module 5 of my Vue.js Training course, I actually discuss this. At first glance, the Object.keys approach may seem like the correct approach. However, different JavaScript engines return the properties in different orders. For that reason, you can't rely on it.
In Vue.js, you could either a) sort your Array when the Array gets populated b) Create a "computed property" or c) Create a sort method that you can pass your property into.
In my opinion, you should use option b. The reason why is because computed properties get cached. By doing this, you'll only run your sort code once. However, if you were to use option c the sort would execute during each iteration. Option a is a possibility, but I don't know enough about your scenario to know if this is a real option or not.

Related

Grouping over an {#each} loop in Svelte

I have an array of non-homogeneous objects that are each rendered in a loop using a <svelte:component this={type}> component, and I would like to group adjacent things that are of the same type within a div.
For example, I have some code similar to this:
<script>
let things = [
{type: A, content: "One"},
{type: B, content: "Two"},
{type: B, content: "Three"},
{type: A, content: "Four"}
];
</script>
{#each things as thing, i}
{()=>someMagicHere1(things, thing, i)}
<svelte:component this={thing.type}>
{()=>someMagicHere2(things, thing, i)}
{/each}
And I want the output to group the things like so:
<div class="group type-A">
<div class="thing type-A">One</div>
</div>
<div class="group type-B">
<div class="thing type-B">Two</div>
<div class="thing type-B">Three</div>
</div>
<div class="group type-A">
<div class="thing type-A">Four</div>
</div>
In the things array, the things are not sorted (actually, they are, but by date, unrelated to their type), but the idea is to visually group together the ones that are the same type. Ideally, I'd be able to group only certain types (like group all of type A, but type B's would remain separate), but I feel like I would be able to derive a separate solution if I could group at all. There are also more than two types; this is just a minimal sample.
In Svelte, the individual A and B components can't have partial HTML elements like this inside, because Svelte won't allow conditionals around unclosed elements:
{#if groupStart}
<div class="group">
{/if}
From within each someMagicHereX() I could output some HTML with {#html customTags} to get the DOM output that I want, but then I lose the style encapsulation and other Svelte component benefits.
What I'd really like is a more "sveltian" solution. Perhaps I need to create something new with use? Anyone have any good ideas?
Update: A key feature I seem to have left out is that any controls must ultimately bind to the original dataset. So even if the data is transformed somehow, the original data must be updated at runtime and vice-versa on any bound controls.
Not absolutely sure if the following satisfies what you are looking for, as I can't tell from your question if you want to keep the order of values untouched or if you want to reorder them by group first.
The idea is to re-arrange your data in order to loop through it in the way you want to. It's always easier to manipulate data in order to fit a layout than the other way around.
The following would turn your initial array into an array with the following structure:
[
{
cssClass: 'type-A',
values: [ /* all objects of type 'A' */ ],
},
{
cssClass: 'type-B',
values: [ /* all objects of type 'B' */ ],
},
// etc.
]
The data transformation is pretty straightforward:
let groups = things.reduce((curr, val) => {
let group = curr.find(g => g.cssClass === `type-${val.type}`)
if (group)
group.values.push(val)
} else {
curr.push({ cssClass: `type-${val.type}`, values: [ val ] })
}
return curr
}, [])
With this new data structure available, it's fairly easy to achieve the layout you had in mind:
{#each groups as group}
<div class="group {group.cssClass}">
{#each group.values as value}
<div class="thing {group.cssClass}">
{value.content}
</div>
{/each}
</div>
{/each}
Demo REPL
Edit: If you prioritize the order of objects as it stands in your initial array, the data transformation would be slightly different. Basically, you'd want to create a new 'group' every time the type changes.
Something like the following would do the trick (note that the svelte #each structure would remain the same, only the data transformation changes):
let groups = things.reduce((curr, val) => {
let group = curr.length ? curr[curr.length - 1] : undefined
if (group && group.cssClass === `type-${val.type}`) {
group.values.push(val)
} else {
curr.push({ cssClass: `type-${val.type}`, values: [ val ] })
}
return curr
}, [])
Option 2 Demo REPL

Vue filter function to return always 5 more elements

actually no biggie but how would a computed property filter function look like that always returns the current array + 5 more elements?
more in detail:
Template:
<span class="box-content" v-for="item in activeItems" :key="item.id">
<img class="item" :src="item.filename" />
</span>
Script
data: function() {
return {
items: [],
limit: 1,
};
},
computed: {
activeItems: function() {
return this.items.filter( function(s) {
if(s.length > this.limit) {
return s;
}
});
// return this.limit ? this.items : this.items;
}
},
on page load , an axios post request gets an object of items, whose response is pushed into the items array which is empty upon component declaration.
so axios -> get object with items -> push into empty array.
now i want to display ,like, 5 items and make a show more button.
The problem now is, my activeItems function is invalid, it does not know "this.limit" and i doubt anyway that it returns the correct result as i just made it return itself and not a set of objects / arrays.
What I would do next is trying around with splice and slice, array copies and pushing elements into it until a certain condition is met but.. is there a better way ?
Thanks in advance
The filter function should be used to filter based on the internal values of an array. Say you have an array of objects with persons, and each Person as an age, then you could use the Array.prototype.filter function to filter based on that age of each entry.
The filter function therefore goes through every entry in your array and determines whether an item should be included or excluded.
If you, on the other hand, want to limit the amount of entries based on a maximum number of entries, I would suggest you use Array.prototype.slice, as you mentioned already.
Your computed function could be rewritten to:
activeItems: function() {
return this.items.slice(0, this.limit)
}
First, in your code, this.limit is undefined because this is referencing the anonymous function. If you want to access the component, you will better use arrow functions syntax.
Also, s references an element of your array, so s.length will be undefined too I guess...
Now, filter does not seem to be the best choice for your need. I'll go with slice instead. Somthing like:
computed: {
activeItems() {
return this.items.splice(0, this.limit)
}
}
Where limit is increased by 5 when you click the show more button.
Of course you could do it. You just missed some code on it. Here how you fix it
activeItems: function() {
let limit = this.limit
return this.items.filter( function(item, s) {
return s <= limit
});
}
If you don't mind using filter, here are some way to do it.
First : put condition in your for loop, this one
<span class="box-content" v-for="(item, index) in items" :key="item.id" v-if="index <= limit">
<img class="item" :src="item.filename" />
</span>
Second is to slice your array on you desired length, this one
<span class="box-content" v-for="(item, index) in items.slice(0, limit)" :key="item.id">
<img class="item" :src="item.filename" />
</span>

Vuejs and Vue.set(), update array

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

Meteor: how to set array value, within spacebars, dynamically

I don't really know how to explain this... I have a collection that has an array in it and when I go through it I've been setting colors.[0].imageLink and not changing the [0], but now I'd like that to be dynamic depending on the value of a function (in this case the function is viewIndex).
Works, but isn't dynamic:
<h3 class='display-price'>$ {{colors.[0].price}}</h3>
What I'd think would work but doesn't:
<h3 class='display-price'>$ {{colors.[(viewIndex)].price}}</h3>
In the corresponding js file (does return 0):
'viewIndex': function() {
console.log(Template.instance().variation.get());
return Template.instance().variation.get();
}
One way to do what you are trying to do is to define a colorPrice helper that takes colors and viewIndex as parameters as follows:
Template.hello.helpers({
colors() {
return [
{ price: 1},
{ price: 2},
{ price: 3}
];
},
viewIndex(){
return 1;
},
colorPrice(colors, viewIndex){
return colors[viewIndex].price;
}
});
Then, in your template you can use it as follows:
<template name="hello">
${{ colorPrice colors viewIndex }}
</template>
So thank you to Kalman for getting me thinking of a dynamic way to do this using more of the javascript! My solution was essentially to use helpers and the "this" keyword by calling helper functions within {{#with item}}, which passed "this" all of the values of the current item it was on, so essentially this became possible:
variationImage() {
return this.colors[Template.instance().variation.get()].imageLink[0];
},
variationLink() {
return this.colors[Template.instance().variation.get()].referralLink;
},
variationPrice() {
return this.colors[Template.instance().variation.get()].price;
}
And at that point getting those values was as simple as using {{variationPrice}} where I needed the price, etc.
{{#with item}}
<h3 class='display-price'>$ {{variationPrice}}</h3>
{{/with}}
And just to add more about how the variations worked each item has a pretty much random number of variations (since we scrape them) so we have to do something along these lines to render how many there are and set which variation you're looking at:
(where colors is an array containing the different color variations of an item)
{{#each colors}}
<option value='{{#index}}'>Variation {{variationIndex #index}}</option>
{{/each}}
(helper function)
variationIndex(index) {
return index+1;
}
(event function to set variation value)
'change .color-variation-select': function(event, template) {
template.variation.set($(event.target).val());
},

AngularJS Multiple ng-repeat

I want to use ng-repeat for two different item at the same time.
I mean, ng-repeat="mylist1 in list1 && mylist2 in list2"
I need to use repeat for list1 and list2.
How can i use ng-repeat for more than one item?
I think you'd be better off creating a combined list. For example (in your controller)
$scope.lists = list1.map(function(item, i) {
return {
item1: item,
item2: list2[i] || null
};
});
Then you can just iterate over this
<div ng-repeat="item in lists">
<p>list1 item: {{ item.item1 }}</p>
<p>list2 item: {{ item.item2 }}</p>
</div>
Alternatively, you could just use $index to reference the corresponding item in list2.
<div ng-repeat="item1 in list1" ng-init="item2 = list2[$index]">
Use a function in the controller to concat them:
$scope.mergeLists = function (arr1, arr2) {
return arr1.concat(arr2);
}
Then you have:
<div ng-repeat="mylist1 in mergeLists(list1, list2)">
Fiddle
You can take advantage of ng-init
<div ng-repeat="mylist1 in list1" ng-init="i=0;lst2 = list2[i++]">
Now you can use lst2
I am assuming you have two lists as follows:
mylist1=[1,2,3,4,5]
mylist2=[6,7,8,9,0]
and you want to iterate over them such that the resulting elements are
[[1,6,], [2,7], [3,8], [4,9], [5,0]]
I'd suggest you take a look at zip in lodash.js (or underscore.js) - Amazing library of functions that HELP you comb and keep all your hair.
Example:
_.zip(['fred', 'barney'], [30, 40], [true, false]);
// → [['fred', 30, true], ['barney', 40, false]]
and in this case:
mergedLists = _.zip(mylist1, mylist2)
ng-repeat="item in mergedLists"
In your application js file, you can assign the result of zipping the two lists, then use ng-repeat to iterate over the values in the list. Note that each item in the list is also a list.

Categories