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>
Related
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.
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);
}
}
I have a v-for like so:
<p> User Responses(s):</p>
<template v-for="item in UserResponses">
<ol v-if="item.some_condition==item._is_true">
<li :name="item.id + '__UR'"> [[ item.some_variable_to_render ] ]] </li>
</ol>
</template>
Works great. However, what I would like to do is say No user responses when the some_condition_is_true fails, but if I add an v-else the message (No user responses) will be printed in every loop, which is not desired ofcourse. How does one solve this problem?
To this end, I wondered if I could test if the element item.id + '__UR'" is present and if not add an element with text sayingNo user responses`
I am not sure however that this is the correct way to go about this.
EDIT
Just to reiterate: using v-if before v-for is not an option since the object being iterated on is a nesed JSON which then is filtered through some vuejs filters, so yea, this is not an option.
note that boolVar corresponds to your actual object key that contains the bools
let model.flag = true
function foo(val) => {
model.flag = val
return val
}
<p> User Responses(s):</p>
<template>
<div v-if="model.flag">
<div v-for="item in UserResponses" >
<ol v-if="foo(item.Boolvar)" >
<li :name="item.id + '__UR'"> [[ item.some_variable_to_render ] ]] </li>
</ol>
</div>
</div>
<h1 v-else>No user responses 😢</h1>
</template>
It is possible you may be able to implement this using CSS:
ol + .no-responses {
display: none;
}
There's a full example below but the idea is simply to hide the message if it comes after an <ol>. Tweak the selector to reflect your real use case.
I have tried to keep the template in my example exactly the same as the original code, with the addition of the relevant message and a couple of buttons for demo purposes.
new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data () {
return {
UserResponses: []
}
},
methods: {
add () {
this.UserResponses.push(
{ some_condition: 3, _is_true: 3, id: 3, some_variable_to_render: 'A' },
{ some_condition: 3, _is_true: 4, id: 4, some_variable_to_render: 'B' },
{ some_condition: 4, _is_true: 4, id: 5, some_variable_to_render: 'C' },
{ some_condition: 5, _is_true: 5, id: 6, some_variable_to_render: 'D' }
)
},
clear () {
this.UserResponses.splice(0)
}
}
})
ol + .no-responses {
display: none;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<p>User Responses(s):</p>
<template v-for="item in UserResponses">
<ol v-if="item.some_condition==item._is_true">
<li :name="item.id + '__UR'"> [[ item.some_variable_to_render ]] </li>
</ol>
</template>
<p class="no-responses">No user responses</p>
<button #click="add">Add values</button>
<button #click="clear">Clear values</button>
</div>
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>