I'm using vue, and I found a bizarre behaviour while working on one of my projects. When I update an array in javascript the items are put inside the old html elements (I suppose) so if these old elements have some particular attributes the new items are going to get them as well.
I'll put this example code (visually it sucks but that's not the point).
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue@3" defer></script>
<script src="script.js" defer></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div >
<div v-for="day in daysList">
<input type="radio" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" @click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div >
<div v-for="hour in hoursList" :id="returnTheInput(hour)" @click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>
Here's the script:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
selectedDay = day;
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i ){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
To see my point:
- select a day
- select an hour (click on it)
- select the other day
By doing this the hour will still be selected, even though it will be from the new ones. That's not what I had expected nor what I'd want. I thought the new items would be assigned to completely new html elements.
How do I avoid this? I could change the internal logic of my script, but I was wondering if there was another way. Ideally I'd want Vue to create new html elements for the new items (since I guess it's recycling the old ones).
CodePudding user response:
There are at least 2 solutions for this.
The first is to assign an unique key to each child with the :key attribute:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
selectedDay = day;
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i ){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div >
<div v-for="day in daysList">
<input type="radio" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" @click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div >
<div v-for="hour in hoursList" :key="hour" :id="returnTheInput(hour)" @click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>
The second is to reset child elements then re-render them asynchronously with the nextTick utility:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
vm.hoursList = [];
selectedDay = day;
Vue.nextTick(() => {
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i ){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
});
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div >
<div v-for="day in daysList">
<input type="radio" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" @click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div >
<div v-for="hour in hoursList" :id="returnTheInput(hour)" @click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>