I know that "v-if" must avoid with "v-for" but not sure about "v-show" because it is just to toggle the display attribute.
This is the code in case anyone wants to know. Basically, I try to switch 3 different types of filter list. The code runs fine but I just wanna know if it should be avoid like "v-if".
<template>
<button
v-for="(filter, index) in filterList" :key="index"
#click="chosenFilter = filter.name"
>
{{ filter.name }}
</button>
<div
v-for="(filter, index) in filterList" :key="index"
v-show="chosenFilter === filter.name"
>
<div v-for="(listItem, index) in filter.list" :key="index">
{{ listItem }}
</div>
</div>
</template>
<script>
data () {
return {
filterList: [
{ name: 'Type 1', list: [] },
{ name: 'Type 2', list: [] },
{ name: 'Type 3', list: [] }
],
chosenFilter: 'Type 1'
}
}
</script>
From the official style guide: https://v2.vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential
There are 2 points in which it's not a good practice to use that, the 2nd one is interesting: To avoid rendering a list if it should be hidden. This one is basically fine since you're not doing heavy JS rendering, just basic CSS toggling.
So yeah, I'd say it's correct to have a v-show (and ESlint is not complaining btw).
But IMO, you can solve this kind of behavior in pretty much all cases with a computed: your filter button could be selected with an ID and your list rendering could be filtered with a filter here.
Replace #click="chosenFilter = filter.name" with #click="chooseFilter and get the ID (thanks to $event) of the item you've clicked on, then filter your list with the selected filter.
Related
I have used vue.js for a couple of projects and I have been using the index as the key in the for loops
<div v-for="(item, index) in items" :key="index"></div>
...and have started to wonder if there are problems with that since examples usually use the ID of the item.
<div v-for="(item, index) in items" :key="item.ID"></div>
Because arrays are mutable. The index of any given item can and will change if items are added to or removed from the array.
You want your key to be a unique value identifying only your unique component. A primary key that you create is always better than using an index.
Here is an example.
console.clear()
Vue.component("item", {
props: ["value"],
data() {
return {
internalValue: this.value
}
},
template: `<li>Internal: {{internalValue}} Prop: {{value}}</li>`
})
new Vue({
el: "#app",
data: {
items: [1, 2, 3, 4, 5]
},
methods: {
addValue() {
this.items.splice(this.items.length / 2, 0, this.items.length + 1)
}
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
{{items}}
<ul>
<item v-for="i in items" :value="i" :key="i"></item>
</ul>
<button #click="addValue">AddValue</button>
<ul>
<item v-for="(i, index) in items" :value="i" :key="index"></item>
</ul>
</div>
Note that when addValue is clicked, the list on top represents the new numbers in the array where the truly are in the array; in the middle. In the second list below the button, the values do not represent the actual location in the array and the internal and property values do not agree.
From the Vue docs (emphasis mine): https://vuejs.org/guide/essentials/list.html#maintaining-state-with-key
To give Vue a hint so that it can track each node's identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item
If the index of any item in the array is changed (e.g. by adding/removing a new item anywhere other than the end of the array), then Vue will lose track of the item.
For example:
let data = [A, B, C, D]
<div v-for="(item, index) in data" :key="index">
Vue tracks each item like this:
A: 0
B: 1
C: 2
D: 3
If you remove B from the array, Vue will then track each item like this:
A: 0
C: 1
D: 2
The indices of C and D have changed, so Vue has lost track of them, and will remove D from the rendered output instead of B (because from its point of view, it is the item with index 3 that was removed).
This is why the key should uniquely identify an item, which an index does not do.
However, it also means that there are some cases where using the index as the key is acceptable:
The array will not change
Or the array will only have items added/removed at the end and will not be reordered
Or the items are all identical
If you cannot use the index as a the key, but cannot uniquely identify items (e.g. some of the items in the list are identical), a solution may be to use lodash's uniqueId():
<div v-for="item in data" :key="uniqueId()">
console.clear()
Vue.component("item", {
props: ["value"],
data() {
return {
internalValue: this.value
}
},
template: `<li>Internal: {{internalValue}} Prop: {{value}}</li>`
})
new Vue({
el: "#app",
data: {
items: [{name:'a'},{name:'b'},{name:'c'}]
},
methods: {
addValue() {
this.items = [{name:'a'},{name:6},{name:'b'},{name:'c'}];
}
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
{{items}}
<ul>
<item v-for="i in items" :value="i.name" :key="i"></item>
</ul>
<button #click="addValue">AddValue</button>
<ul>
<item v-for="(i, index) in items" :value="i.name" :key="index"></item>
</ul>
</div>
To be more clear
I have a list of objects that are being shown in a v-for loop. They all have a certain key value pair, and based upon that value I'd like for the user to be able to toggle a button outside the loop structure to either show or hide those elements. Basically I want all of the items to be shown at first, and then once the button is toggled, only the items with the true value to be shown, then back to all items, etc.
Something like
const items = [
{
exampleKey: false
},
{
exampleKey: true
},
{
exampleKey: false
}
]
<button #click="updateList">click to update list</div>
<div v-for="items in itemList">item example</div>
methods: {
updateList: function(){
// make the magic happen
}
}
Of course this is just some pseudo code but it illustrates what I'm trying to get at. I am looking for some type of method or computed property that will let the user toggle the items visibility.
So you shouldn't combine v-for and v-if on the same element. What you can do is either include a filter in your v-for:
<div v-for="item in items.filter(i => i.exampleKey)">{{item.foo}}</div>
Or (my preference) you can iterate items as normal to create container divs, and add child content only where the desired condition is satisfied:
<div v-for="item in items">
<div v-if="item.exampleKey">{{item.foo}}</div>
</div>
If you want to add a control to hide/show items with an exampleKey of false, you can change your loop to:
<div v-for="item in items">
<div v-if="item.exampleKey || showItemsWithFalseExampleKey">{{item.foo}}</div>
</div>
And you can create a data property "showItemsWithFalseExampleKey" that is toggled by a button:
<button #click="showItemsWithFalseExampleKey = !showItemsWithFalseExampleKey">Toggle hidden items</button>
Of course, render cost for v-if is a lot higher than using v-show, so choose which is better based on your situation: https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
I would make a data property to toggle as true/false, when the button is clicked and have a computed property return the items based on that property.
Something like this:
<button #click="showElements = !showElements">click me</button>
<div v-for="item in filteredItems">{{ item }}</div>
data() {
return {
items: [
{
exampleKey: false
},
{
exampleKey: true
},
{
exampleKey: false
}
],
showElements: true
};
},
computed: {
filteredItems() {
return showElements ? this.items : this.items.filter(item => item.exampleKey);
}
}
This a simple to-do list and I am trying to add an id in each <li> that is rendered. Each id should be named different (obviously) but similar name: "id0","id1","id2"...etc
The number comes from the directive v-for="i in items"
I do it like this:
<li :id="`id${i.id}`" v-for="i in items" :key="`id${i.id}`">
Complete code:
<ul class="item-list-ul">
<li :id="`id${i.id}`" v-for="i in items" :key="`id${i.id}`">{{ i }}
<div class="item-butons">
<b-button class="done-btn" #click="strikeItem(i)" size="sm" variant="outline-dark">DONE!</b-button>
<b-button class="delete-btn" #click="deleteItem(i)" size="sm" variant="warning">Delete</b-button>
</div>
</li>
</ul>
My items array:
data () {
return {
items: ["five", "<li>", "should","be","rendered"]
}
}
But when I check in the console the names of the new dynamically created id´s of the <li> they just appear idundefined when in the case of having for example 3 <li> they should appear like this:
id0
id1
id2
However in the console there aren't any errors. It seems that the vue-html simply does not read a number in ${i.id} but just undefined. Why?
If items is just an array of strings, then there is no id property available. The standard idiom in this case is to use the index like so:
<li v-for="(i, index) in items" :id="`id${index}`" :key="index">
Alternatively, you could reshape your data to have ids like:
[{ id: 0, text: 'foo' }, { id: 1, text: 'bar' }, { id: 2, text: 'baz' }]
That could be done as part of a computed property, for example. Note this would also require a few changes to your template.
I'm using Vue2.js and Element UI as a framework. I would like to be able to filter a table which is sliced. To do so, I use the table and filter components whose documentation could be found here.
Situation OK
The table is not sliced. When you picked a filter, a loop goes trough each row and check if the value of the column is equal to the filter.
Situation NOT OK
The table is sliced. When you picked a filter, a loop goes trough each row that results of the slice and check if the value of the column is equal to the filter. By doing that we don't filter the "hidden" values.
I've made a little https://jsfiddle.net/acm3q6q8/3/ so it is easier to understand.
All of this make sense since I'm not working on the whole data, but on a sliced version.
One solution could be to hide rows instead of excluding them by slicing the data, but I'm wondering if there is a better solution ?
What I want to achieve
In the jsfiddle, display only 2 items.
Filter the tag to display only rows whose tag is Office
Actual result
There is no row displayed since the row whose tag was office was not part of the sliced table.
Expected result
When filtering, I would like to take into account rows that are not necessarily displayed.
Important
This should work fine with a multiple filter (ie I select several tags)
EDIT
In the same extent if you want to sort the name by alphabetical order, Albert won't be displayed if you displayed only 2 items.
You can handle the filter-change event on the table component (documented here), and filter/slice yourself.
var Main = {
data() {
return {
numberItemToShow : 4,
tableData: [...],
filter: []
}
},
computed : {
filterData() {
if (!this.filter.length)
return this.tableData.slice(0, this.numberItemToShow)
else
return this.tableData
.filter(row => this.filter.includes(row.tag))
.slice(0, this.numberItemToShow);
}
},
methods: {
onFilterChange(filters){
if (filters.tag)
this.filter = filters.tag;
}
}
}
And the template
<template>
<input v-model="numberItemToShow" placeholder="edit me">
<p>Number of item to display: {{ numberItemToShow }}</p>
<el-table ref="tab" :data="filterData" border style="width: 100%" #filter-change="onFilterChange">
<el-table-column prop="name" label="Name" sortable>
</el-table-column>
<el-table-column prop="tag" label="Tag" column-key="tag" :filters="[{ text: 'Home', value: 'Home' }, { text: 'Office', value: 'Office' }]">
<template scope="scope">
<el-tag :type="scope.row.tag === 'Home' ? 'primary' : 'success'" close-transition>{{scope.row.tag}}</el-tag>
</template>
</el-table-column>
</el-table>
</template>
Example.
The problem is that the slicing is done before the filtering. The filter has to see the original data, and the row-counting must be part of the filtering.
Since the filter looks at one row at a time, keeping track of the matched rows is a little tricky. What I did here is keep a counter of matched rows that resets to zero when the row being looked at is the first row of data. This is hacky, but it works. There may be a better way; I am not familiar with the table widget.
var Main = {
data() {
return {
numberItemToShow : 4,
tableData: [{
name: 'One',
tag: 'Home'
}, {
name: 'Two',
tag: 'Home'
}, {
name: 'Three',
tag: 'Home'
}, {
name: 'Four',
tag: 'Office'
}],
scratchCounter: 0
}
},
methods: {
filterTag(value, row) {
const matched = row.tag === value;
if (row === this.tableData[0]) {
this.scratchCounter = 0;
}
if (matched) {
++this.scratchCounter;
}
return this.scratchCounter <= this.numberItemToShow && matched;
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
#import url("//unpkg.com/element-ui/lib/theme-default/index.css");
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui/lib/index.js"></script>
<div id="app">
<template>
<input v-model="numberItemToShow" placeholder="edit me">
<p>Number of item to display: {{ numberItemToShow }}</p>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="name" label="Name">
</el-table-column>
<el-table-column prop="tag" label="Tag" :filters="[{ text: 'Home', value: 'Home' }, { text: 'Office', value: 'Office' }]" :filter-method="filterTag">
<template scope="scope">
<el-tag :type="scope.row.tag === 'Home' ? 'primary' : 'success'" close-transition>{{scope.row.tag}}</el-tag>
</template>
</el-table-column>
</el-table>
</template>
</div>
I have this object structure:
lines: [{
order: '1',
text: ' blue'
},{
order: '2',
text: 'green'
},{
order: '3',
text: 'yellow'
}]
And this is rendered on the page like this:
Blue
Green
Yellow
I want reorder the elements (and the object) without drag-drop, but with button up and down. Like this:
Blue - [down]
Green [up, down]
Yellow [up]
Each bullet is a component. How can I achieve that?
Based on assumptions from the little information you provided, I gave it a go.
Read: Vuejs list caveats
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
So when you modify an array in Vue you should use one of the following:
// Vue.set
Vue.set(example1.items, indexOfItem, newValue)
// Array.prototype.splice
example1.items.splice(indexOfItem, 1, newValue)
Your view can look something like:
<div id="app">
<div v-for="(line, index) in lines">
<p style="display: inline-block">{{line.text}}</p>
<button #click="up(index)" v-if="index !== 0">UP</button>
<button #click="down(index)" v-if="index !== lines.length-1">DOWN</button>
</div>
</div>