Using v-for for fixed numbers and not objects - vue.js

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>

Related

Vue2: Can I pass an optional (global) filter into a reusable component?

I am quite new to Vue.
I am working on a table as component, which is supposed to be a lot of times. So far, so good, but now I want to use a filter, which can be optional passed into it.
That is how I "call" the table:
<table
:headers="headers"
:items="some.data"
></table>
data () {
return {
headers: [
{ title: 'date', value: ['date'], filter: 'truncate(0, 10, '...')' },
]
}
}
Here is my table component
<template>
<div>
<table class="table">
<thead>
<tr>
<!-- <th v-for="header in headers" :key="header.id" scope="col">{{ header.title }}</th> -->
<th v-for="header in headers" :key="header.id" scope="col">
{{ header.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id" scope="col">
<td v-for="header in headers" :key="header.id" scope="row">
<!-- {{item}} -->
<span v-for="val in header.value" :key="val.id">
{{item[val] | truncate(0, 10, '...') }}
</span>
<!-- {{header.filter}} -->
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script >
export default {
name: 'Table',
props: {
headers: Array,
items: Array
}
}
</script>
My global filter:
Vue.filter('truncate', function (text, start, truncLength, clamp = '') {
return text.slice(start, truncLength) + clamp
// return text.slice(0, stop) + (stop < text.length ? clamp || ' ...' : '')
})
I was hopping, to add by that an optional filter (via v-if I would chech for it). So far I can render the filter as string ... but not execute it.
Even if I put the filter in the span, it does not work (it says then, "text.slice is not a function" in the console.
I was not successful with googling it, because with filters/filter it is mostly about how to use .filter(...) on data as JS method, but not as Vue filter.
Any advise?
A filter is a function that runs inside JSX template in html. Example of how to create custom Vue.js filter
Vue.filter('ssn', function (ssn) { // ssn filter
return ssn.replace(/(\d{3})(\d{2})(\d{4})/, '$1-$2-$3');
});
Using it
{{stringToTrans | ssn}}
To use a filter outside this you can use a computed property or standard function like so
convertedDateString() { // computed
return this.$moment(this.dateString).format('MM/DD/YYYY')
}
convertedDateString(dateString) { // method
return this.$moment(dateString).format('MM/DD/YYYY')
}

Should I be using a computed property here?

My apologies if this is a simple question.
I have a table that's being built in vue.js
Column A is for numerical input, column B has a preset value and Column C calculates the difference between them.
Currently I'm using a computed property that loops through the rows, calculates the difference and stores that in my data array, then I'm calling the value {{row.difference}} in the table cells.
I've called my computed property difference, however it only works if I include {{difference}} within the element div.
Is that bad usage? Should I be calling a method on each row instead and returning the calculated value?
Computed values never should mutate data.
I would suggest that your computed value return a new array with the difference in it.
Vue.config.devtools = false;
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data: {
items: [{
a: 10,
b: 4
},
{
a: 443,
b: 23
}
]
},
computed: {
items_with_difference() {
return this.items.map((i) => ({
...i,
difference: i.a - i.b
}));
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table>
<thead>
<th>
A
</th>
<th>
B
</th>
<th>
Difference
</th>
</thead>
<tbody>
<tr v-for="(item, idx) in items_with_difference" :key="idx">
<td>
{{item.a}}
</td>
<td>
{{item.b}}
</td>
<td>
{{item.difference}}
</td>
</tr>
</tbody>
</table>
</div>

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

Vue: How to conditionally render tr in tbody

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>

Nested for loops and unexpected results

I'm developing a custom calendar component in vue. The early template is quite simple:
<template>
<table class="calendar">
<tr class="calendar-header">
<th class="calendar-header-cell" v-for="headerIndex in 7">
{{headerIndex}}
</th>
</tr>
<tr class="calendar-body-row" v-for="weekIndex in 5">
<td class="calendar-body-cell" v-for="dayIndex in 7">
{{day++}}
</td>
</tr>
</table>
</template>
<script>
export default {
name: 'Calendar',
data () {
return {
weekdays : 7,
day: 0,
}
},
}
</script>
the problem is that I expect to get a 5x7 table. In each cell I expect to see the "day" variable from 0 to 40. But It breaks because of an infinite loop.
What I am doing wrong here?
With {{day++}}, it's basically trying to render day as well as updating day at the same time, which re-trigger the render, hence it's complaining about an infinite loop. If you just want to display number from 1 to 35, you can do something like:
{{(weekIndex - 1) * weekdays + dayIndex}}