kholdstayr
kholdstayr

Reputation: 197

vue.js not updating attributes in nested v-for loops

I'm playing around with vue.js and react, and I'm currently trying to adapt a simple editable HTML table example from a book on React that I have in order to learn Vue.

Here is what happens in the code:

  1. User clicks on td element
  2. coordinates of td element are stored in rowSel, colSel variables
  3. coordinates change causes Vue to re-render the view
  4. the td cell that the user clicked has the "contenteditable=true" attribute added, as well as focusout event listener added
  5. user can then change the data in the td cell, and click off the cell to stop editing (focusout triggered)
  6. when user clicks off the cell, the rowSel and colSel variables are reset which causes re-render
  7. vue re-renders the view, and because the coordinates are reset, Vue should remove the contenteditable and focusout from the td element in question
  8. The data array containing the table data will then be updated with the text that the user entered when the cell was editable (not implemented yet)

The problem I am having is with step 7. I can click multiple cells in my table and all of the cells will still have the contenteditable="true" set in them. If you look in Chrome dev tools you will see that the contenteditable tag sticks to all of the cells that are clicked on.

I believe the problem is happening because of the nested for loops I have, but I am not sure how to properly add keys to them to get it to work. I've tried to add keys but I think that I am not doing it correctly.

JSFiddle: https://jsfiddle.net/o69yaq7L/

Here is the code:

"use strict";

var headers = [
    "Book", "Author", "Language", "Published", "Sales"
];

var data = [
    ["The Lord of the Rings", "J. R. R. Tolkien", "English", "1954-1955", "150 million"],
    ["Le Petit Prince (The Little Prince)", "Antoine de Saint-Exupéry", "French", "1943", "140 million"],
    ["Harry Potter and the Philosopher's Stone", "J. K. Rowling", "English", "1997", "107 million"],
    ["And Then There Were None", "Agatha Christie", "English", "1939", "100 million"],
    ["Dream of the Red Chamber", "Cao Xueqin", "Chinese", "1754-1791", "100 million"],
    ["The Hobbit", "J. R. R. Tolkien", "English", "1937", "100 million"],
    ["She: A History of Adventure", "H. Rider Haggard", "English", "1887", "100 million"],
];

var Excel = new Vue({
	el: '#app',

	data: {
		colData: data,
		headers: headers,		
		colSel: -1,
		rowSel: -1
	},
	methods: {
		dataClick: function(ev){	
			this.colSel = ev.target.cellIndex;
			this.rowSel = ev.target.parentElement.rowIndex - 1;
			Vue.nextTick( function(){
				ev.target.focus();
			});
		},

		lostFocus: function(ev){						
			console.log(ev.target.textContent);
			//var changedRow = this.colData.slice(this.rowSel,this.rowSel+1);
			this.colSel = -1;
			this.rowSel = -1;
			console.log(this.colSel + " " + this.rowSel);
			
		}
	}
});
<html>
<head>
    <title>
        VUEJS Testing
    </title>
</head>
<body>
    <div id="app">        
        <table>
            <thead>
                <tr>
                    <th v-for="item in headers" :key="item.id">{{item}}</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="(row,rowInd) in colData"  :key="row.id">
                	<template v-for="(item,colInd) in row"  >
                		<template v-if="rowInd == rowSel && colInd == colSel">
                    		<td contenteditable="true" v-on:focusout="lostFocus" :key="item.id">{{item}}</td>
                    	</template>
                    	<template v-else>
                    		<td v-on:dblclick="dataClick">{{item}}</td>
                    	</template>
                    </template>
                </tr>
            </tbody>
        </table>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.4/vue.min.js"></script>   
</body>
</html>

EDIT***

The problem is with this section of the code:

<tr v-for="(row,rowInd) in colData" :key="row.id">
  <template v-for="(item,colInd) in row">
    <template v-if="rowInd == rowSel && colInd == colSel">
      <td contenteditable="true" v-on:focusout="lostFocus" :key="item.id">{{item}}</td>
    </template>
    <template v-else>
      <td v-on:dblclick="dataClick">{{item}}</td>
    </template>
  </template>
</tr>

There is two v-for loops, and a if-else statement. The problem is where the if statement makes the td element have a contenteditable=true attribute. Once the td element gets that attribute, the attribute never leaves, even after further re-renders the td will still have the contenteditable=true set, even if that part of the if statement does not effect the element again. I think there is a problem with the way I am using id tags but I am not sure how to fix it.

Upvotes: 3

Views: 4702

Answers (1)

Saurabh
Saurabh

Reputation: 73659

You have to give different keys to both if/else statements, like following:

<tr v-for="(row,rowInd) in colData"  :key="row.id">
    <template v-for="(item,colInd) in row"  >
        <template v-if="rowInd == rowSel && colInd == colSel">
            <td contenteditable="true" v-on:focusout="lostFocus" :key="item.id"  key="key1">{{item}}</td>
        </template>
        <template v-else>
            <td v-on:dblclick="dataClick" key="key2">{{item}}</td>
        </template>
    </template>
</tr>

See updated fiddle.

This is happening because Vue tries to render elements as efficiently as possible, often re-using them instead of rendering from scratch. Since in your case both if and else templace uses same element, is not replaced, just it's click event. As this isn’t always desirable though, so Vue offers a way for you to say, These two elements are completely separate - don’t re-use them, which can be done by adding key attribute with unique values.

Upvotes: 5

Related Questions