Vue: How to conditionally render tr in tbody - vue.js

I have a table body with multiple rows, such as this:
<table>
<tbody>
<tr>...</tr>
<tr>...</tr>
</tbody>
</table>
I want to conditionally combine v-if an v-for, to conditionally render one or more additional rows. The Vue manual says to wrap the v-for in a v-if, such as follows:
<div v-if="team.positions != null">
<my-row v-for="position in team.positions"
:position="position"
:key="position.id">
</my-row>
</div>
The problem is that I can't put a div in a tbody, or any other element for that matter. What's the solution?

In those situations where no element would fit, you can use <template>, like:
<template v-if="team.positions != null">
<my-row v-for="position in team.positions"
:position="position"
:key="position.id">
</my-row>
</template>
Demo:
new Vue({
el: '#app',
data: {
showTwoRows: true
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<table>
<tr>
<td>A</td><td>B</td>
</tr>
<template v-if="showTwoRows">
<tr>
<td>1</td><td>2</td>
</tr>
<tr>
<td>3</td><td>4</td>
</tr>
</template>
<tr>
<td>C</td><td>D</td>
</tr>
</table>
<button #click="showTwoRows = !showTwoRows">Toggle two middle rows</button>
</div>
Though in that specific example of yours, it doesn't seem needed. Have you tried simply not using the v-if:
<my-row v-for="position in team.positions"
:position="position"
:key="position.id">
</my-row>
Because the v-for just won't iterate (without throwing errors) if its value is undefined/null/0/[]/'':
new Vue({
el: '#app',
data: {
message: "If I'm being displayed, Vue works!",
team: {
positionsU: undefined,
positionsN: null,
positionsZ: 0,
positionsE: [],
positionsS: ''
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<p>{{ message }}</p>
<table>
<tr v-for="position in team.positionsU"><td>u: {{ position }}</td></tr>
<tr v-for="position in team.positionsN"><td>n: {{ position }}</td></tr>
<tr v-for="position in team.positionsZ"><td>z: {{ position }}</td></tr>
<tr v-for="position in team.positionsE"><td>e: {{ position }}</td></tr>
<tr v-for="position in team.positionsS"><td>s: {{ position }}</td></tr>
<tr v-for="position in team.positionsF"><td>f: {{ position }}</td></tr>
</table>
</div>

You can use v-for and v-if on the same tag, however, it works differently to how you'd expect it to.
within the v-if you can reference the iterated item since v-for is performed before v-if
<div v-if="team.positions != null">
<my-row v-for="position in team.positions" v-if="position"
:position="position"
:key="position.id">
</my-row>
</div>
this would still iterate through all positions in team.positions, and not halt the for loop if the condition in the v-if was not met, but rather skip it.
think of it like this:
for (var i = 0; i < array.length-1; i++) {
if (array[i]) {
doTheThing();
}
}

I am not sure if this is exactly what the original question is looking for, but I just had a similar issue where I wanted to ignore rendering rows where the price of a item was 0.
I ran into the problem using v-if in the <tr> containing the v-for. I solved it by simply using a v-show instead.
So this worked perfectly in my case.
<tr v-show="item.price !== 0" :key="item._id" v-for="item in items"> ... </tr>

Related

Using v-for for fixed numbers and not objects

I'm trying to render a simple table with 20 rows and 5 columns with v-for but I'm having problems. My code:
<tr v-for="row in totalRows" :key="row">
<td v-for="col in totalColumns" :key="col">
{{ getTableNum() }}
</td>
</tr>
In data:
totalColumns: 5,
totalRows: 20,
numberCount: 0,
Method:
getTableNum() {
return ++this.numberCount;
},
It is throwing a warning...
[Vue warn]: You may have an infinite update loop in a component render function.
... and rendering like 20k rows.
I can't find an example on how use v-for for fixed numbers(only using objects) anywhere.
I imagine that those loops above would reproduce a result like:
for (let row = 0; row < totalRows; row++) {
for (let col = 0; col < totalCols; col++) {
getTableNum();
}
}
But I'm wrong for some reason.
UPDATE: Ended up using no variables at all:
<tr v-for="row in 20" :key="row">
<td v-for="col in 5" :key="col">
{{ col + (row - 1) * 5 }}
</td>
</tr>
I wish official docs could have examples like that. If I knew that fixed numbers could be used in the for loop it would spare me some time.
Running methods inside the template leads to some infinite rendering loops since the variable is also used in template, to avoid this create a two-dimensional array as a computed property and then render it :
computed:{
arr(){
let n=0;
return [...Array(20)].map((_,i)=>[...Array(5)].map((_,j)=>{
++n;
return n;
}))
}
}
in template :
<tr v-for="(row,i) in arr" :key="i">
<td v-for="col in row" :key="col">
{{ col }}
</td>
</tr>
The getTableNum function execution changes the numberCount in data and it triggers Vue to re-render the template, which causes the function execution, and so on.
In this case, you should try to avoid altering the numberCount value.
If I didn't get you wrong, you wish to have 1-2-3-4-5 in the first row, 6-7-8-9-10 in the second, and so on.
If so, you can try to rewrite your function as such:
getTableNum(row, col) {
// row and col are 1-based
return 1 + ((row - 1) * this.totalColumns) + (col - 1);
},
You are changing numberCount (which is a reactive data property) directly in the template. That triggers a re-render (and thus an infinite loop). You can simply do this :
var app = new Vue({
el: '#app',
data: {
totalColumns: 5,
totalRows: 20
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border=1>
<tr v-for="(row, rowIndex) in totalRows" :key="row">
<td v-for="(col, columnIndex) in totalColumns" :key="col">
{{ rowIndex * totalColumns + col}}
</td>
</tr>
</table>
</div>
Another alternative ONLY for static tables (where the table content is not modified after initial rendering) is the use of v-once directive. Each table row will be rendered only once, and every subsequent call to getTableNum function will not trigger the rerendering of previous rows:
var app = new Vue({
el: '#app',
data: {
totalColumns: 5,
totalRows: 20,
numberCount: 0
},
methods: {
getTableNum() {
return ++this.numberCount
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border=1>
<tr v-once v-for="(row, rowIndex) in totalRows" :key="row">
<td v-for="(col, columnIndex) in totalColumns" :key="col">
{{ getTableNum() }}
</td>
</tr>
</table>
</div>

Vue - change/set a variable value in template

Working in Vue, I am trying to set a variable based on another variable within the template. This is within a loop, and I need to set a value that can be used in 'next' iteration of the loop (to change the way a table is rendered, based on the variable).
I have the following:
<template>
...
<tbody v-for="(lo,index) in learn" :key="lo.id">
<tr>
<td colspan="2">LO{{index+1}} {{lo.attributes.field_lo}}</td>
<td v-if="!nextDist"> </td>
</tr>
<tr>
<td>
<div
v-for="(pass,index) in lo.attributes.field_pass"
:key="index"
>P{{pStep()}} {{pass}}</div>
</td>
<td>
<div
v-for="(merit,index) in lo.attributes.field_merit"
:key="index"
>M{{mStep()}} {{merit}}</div>
</td>
<td v-if="lo.attributes.field_dshared && next" ***SET VALUE OF this.next*** rowspan="3">
<span class="has-text-weight-bold">D{{dStep()}} </span>{{lo.attributes.field_dist}}
</td>
<td v-else-if="!lo.attributes.field_dshared" ***SET VALUE of this.next*** ><span class="has-text-weight-bold">D{{dStep()}} </span>{{lo.attributes.field_dist}}
</td>
***else render nothing***
</tr>
</tbody>
</template>
export default {
name: "SpecUnit",
components: {
EssentialContent
},
data() {
return {
unit: "",
learn: "",
nextDist: "",
next: ""
};
},
...
}
What I'd like to be able to do is set the value of 'next' (this.next) so that when the loop iterates, I can check to see if I should which of the I should render or render nothing (because we are 'rowspanning').
I've tried computed and methods, but can't seem to get this working. I've looked to use Vue.set, but I'm struggling with that.
I'm still new to Vue, so any help would be greatly appreciated.
Thanks
It looks like Florian Reuschel had a similar problem and already solved it (although with some caveats)
Let's say we have something like that:
<!-- List.vue -->
<ul>
<li v-for="id in users" :key="id">
<img :src="getUserData(id).avatar"><br>
🏷️ {{ getUserData(id).name }}<br>
🔗 {{ getUserData(id).homepage }}
</li>
</ul>
His approach is to use a helper renderless component with a scoped slot
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
and then
<!-- List.vue -->
<ul>
<Pass v-for="id in users" :key="id" :metadata="getUserData(id)">
<li slot-scope="{ metadata }">
<img :src="metadata.avatar"><br>
🏷️ {{ metadata.name }}<br>
🔗 {{ metadata.homepage }}
</li>
</Pass>
</ul>
If you take a look at the comments section on his blog article, you will see other approaches, too. For example, you can use an expression inside v-bind
<li v-for="id in users" :key="id" :demo="item = getUserData(id)">
<img :src="item.avatar" /><br />
🏷️ {{ item.name }}<br />
🔗 {{ item.homepage }}
</li>

vuejs undefined value when switching template

I'm porting a list to a table to further expand out the columns in the future, but I'm running into an undefined variable error, which I don't understand.
Here's the existing list:
<ul class="collection with-header">
<li class="collection-item" v-for="day in days">
<drop #drop="function(data, event) { handleDrop(data, day, event); }">
<div>{{ day.getLabel() }}</div>
<drag class="chip" v-for="meal in day.meals">{{ meal.title }}<i class="close material-icons" v-on:click="deleteMeal(meal)">close</i></drag>
</drop>
</li>
</ul>
And here's the new table:
<table>
<tr class="collection-item" v-for="day in days">
<td>{{ day.getLabel() }}</td>
<drop #drop="function(data, event) { handleDrop(data, day, event); }" tag="td">
<drag class="chip" v-for="meal in day.meals">{{ meal.title }}<i class="close material-icons" v-on:click="deleteMeal(meal)">close</i></drag>
</drop>
</tr>
</table>
When running the code, I get a "day is not defined" error, which seems to occur on the drag line as commenting it out fixes the error. What I don't understand is why that variable isn't defined. In both an element has access to the day object to get its label, but in the drop tag, which is identical in both examples, the day object is now out of scope.
Is there something about tables specifically that is causing a problem here?
The problem is that the replacement of the component tag with the tag passed to it in the props occurs after the template is transformed into DOM. And in the case of a table, tags that are not allowed to be inside the table are transferred to the DOM before the table:
new Vue({
el: "#app"
})
table {
border: 4px solid green;
}
td {
border: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table>
<tr>
<td>First column</td>
<component :is="'td'" tag="td">Second column</component>
<td>Third column</td>
</tr>
</table>
</div>
And, accordingly, there will be no variable day in tr scope. So do not use in this case the attribute tag:
<table>
<tr class="collection-item" v-for="day in days">
<td>{{ day.label }}</td>
<td>
<drop #drop="function(data, event) { handleDrop(data, day, event); }">
<drag class="chip" v-for="meal in day.meals">{{ meal.title }}
<i class="close material-icons" v-on:click="deleteMeal(meal)">x</i>
</drag>
</drop>
</td>
</tr>
</table>

Getting data back into the root with nested components in vue

I am building a multiple page app with latest Laravel and latest Vue.js. At the end of this post you will see what I am trying to achieve - which I have done visually. However the user needs to be able to edit the text, assigned user and the date of each item. I have started with the date and as you can see I have the date picker working as well.
Where I am struggling is updating the main model of data in the root so that I can save the changes that the user has made via a HTTP request. Initially the tree's data is loaded in via HTTP as well (example below).
I have built the below using nested components and I have read that two binding has been depreciated for props on components. I know that I need to emit and user events but I'm sure how this would work if the components are nested?
Here is an example of the data that get's loaded via HTTP. Below is a very small example, however this could be much larger
{
"objective":"Test",
"user_id":null,
"by":"08\/09\/2018",
"colour":"#1997c6",
"children":[
{
"objective":"Test",
"user_id":11,
"by":"08\/09\/2018",
"colour":"#d7e3bc",
"children":[]
}, {
"objective":"Test",
"user_id":11,
"by":null,
"colour":"#1997c6",
"children":[]
}
]
}
Here are the components that I have put together so far.
Vue.component('tree-date', {
props: ['date'],
data () {
return {
id: 0
}
},
mounted() {
this.id = uniqueId();
$('#picker-' + this.id).datetimepicker({
format: 'DD/MM/YYYY',
ignoreReadonly: true
});
},
template: `
<div class="date-container" :id="'picker-' + id" data-target-input="nearest" data-toggle="datetimepicker" :data-target="'#picker-' + id">
<div class="row">
<div class="col-2">
<div class="icon">
<i class="fa fa-calendar-alt"></i>
</div>
</div>
<div class="col-10">
<input type="text" class="form-control datetimepicker-input" readonly="readonly" :data-target="'#picker-' + id" v-model="date">
</div>
</div>
</div>`
});
Vue.component('tree-section', {
props: ['data', 'teamUsers', 'first'],
methods: {
test () {
this.$emit('test');
}
},
template: `
<table v-if="data.length != 0">
<tr>
<td :colspan="data.children !== undefined && (data.children.length * 2) > 0 ? data.children.length * 2 : 2">
<div class="node" :class="{'first': first == true}">
<div class="inner">
<tree-date :date="data.by"></tree-date>
<div class="objective">
{{ data.objective }}
</div>
<div class="author" v-if="data.user_id !== null">
{{ teamUsers[data.user_id].first_name }} {{ teamUsers[data.user_id].last_name }}
</div>
<div class="author" v-if="data.user_id === null">
Unassigned
</div>
</div>
</div>
</td>
</tr>
<tr class="lines" v-if="data.children.length > 0">
<td :colspan="data.children.length * 2"><div class="downLine"></div></td>
</tr>
<tr class="lines" v-if="data.children.length > 0">
<td class="rightLine"></td>
<td class="topLine" v-for="index in ((data.children.length * 2) - 2)" :key="index" :class="{'rightLine': index % 2 == 0, 'leftLine': Math.abs(index % 2) == 1}"></td>
<td class="leftLine"></td>
</tr>
<tr v-if="data.children.length > 0">
<td colspan="2" v-for="child in data.children">
<tree-section :data="child" :team-users="teamUsers" :first="false"></tree-section>
</td>
</tr>
</table>
`
});
This all get's called in the view by:
<tree-section :data="data" :team-users="teamUsers" :first="true"></tree-section>
Any help getting data update in the components back into the root will be most helpful.
by default, vue props (if objects or arrays) are being passed by reference- that means that if you change your object on the child component, the original object on the parent component will get changed too.
from vue api:
Note that objects and arrays in JavaScript are passed by reference, so
if the prop is an array or object, mutating the object or array itself
inside the child component will affect parent state.
https://v2.vuejs.org/v2/guide/components-props.html

v-for look using curly brackets

I ran into a situation that I need to loop true items without producing any HTML. I expect the code to look something like this.
<table id="detailTable">
<tr>
<th class='editRow'></th>
<th class='editRow'></th>
<!-- <th class='editRow'></th> -->
<th v-for='(columns, index) in $parent.columns' :key='index'>{{ firstLetterCaps(columns) }}</th>
</tr>
{{ for (row, index) in $parent.results }}
<resultsRows v-for='(row, index) in $parent.results' :key='index' :row='row' :index='index' :deleteQueryObjects='$parent.deleteQueryObjects'></resultsRows>
<resultsCommentRow v-for='(row, index) in $parent.results' :index='index'></resultsCommentRow>
{{ end-for}}
</table>
To make it clear I expect instead of using <div v-for=item in items></div> to this { for item in items } { end for } does this exist for vue ?
There is no such a syntax in vuejs, if you want to loop on something without touching it you can use <template> like :
<template v-for="element in elements" >
...
</template>
template tag description from : https://www.w3schools.com/TagS/tag_template.asp
you can go deeper : https://v2.vuejs.org/v2/guide/syntax.html
In your case :
<template v-for='(row, index) in $parent.results' >
<resultsRows :row='row' :index='index' :deleteQueryObjects = '$parent.deleteQueryObjects' />
<resultsCommentRow :index='index' />
</template>