Reputation: 4436
I've got a child component that receives a prop from the parent component. I've added an event to update the prop in the parent component when a button is clicked. But the child component does not detect the prop change.
Approximate code:
Parent component:
<template>
<table>
<Student v-for="(student, index) in students"
:key="index"
:student="student"
:attendance="attendance[student.id]"
@attended-clicked="changeAttendance"
></student>
</table>
</template>
<script>
export default {
data() {
return {
students: [
{id: 1, name: 'Pepito'},
{id: 2, name: 'Sue'},
],
attendance: {
1: {attended: true},
2: {attended: false},
}
}
},
methods: {
changeAttendance(studentId) {
this.attendance[studentId].attended = !this.attendance[studentId].attended;
}
}
}
</script>
Child component:
<template>
<tr>
<td>{{ student.name }}</td>
<td><v-btn :color="attendance.attended ? 'green' : 'red'"
@click="$emit('attended-clicked', student.id)"
>Attended</v-btn>
</tr>
</template>
<script>
export default {
props: ['student', 'attendance']
}
</script>
I've tried to keep the code as minimal as possible to give you an idea of what I'm trying to achieve. The expectation is that when the button is clicked, attendance.attended
will change.
I can verify that the value is changing when clicked using Vue developer tools (although I have to press the "force refresh" button to see the change). But apparently the child view is not picking up on the change.
Is there something I'm doing wrong here that's breaking reactivity?
I've also tried using Vue.set
with some code like this:
methods: {
changeAttendance(studentId) {
let attendance = this.attendance[studentId];
attendance.attended = !attendance.attended;
Vue.set(this.attendance, studentId, attendance);
}
}
No difference.
I appreciate any insight into what I'm doing wrong, thanks!
Upvotes: 2
Views: 5840
Reputation: 3101
According to the comments, the underlying problem is that attendance
gets set to {}
, meaning the attendance[student.id]
objects are not really there or not being tracked anymore according to the change detection part in the VueJS docs. This is what should be done by the Vue.set
example code provided in the question.
Basically I see two options how to solve this:
attendance
.Since I don't know about the API and how it's being called, I'll provide an answer for option 2. The provided code almost worked, it only needs two changes to the Parent
component to make sure the students attendance object is always filled and it will get tracked in the future:
<template>
<table>
<!-- :attendance needs an object to work -->
<Student
v-for="(student, index) in students"
:key="index"
:student="student"
:attendance="attendance[student.id] || {}"
@attended-clicked="changeAttendance"
/>
</table>
</template>
<script>
import Vue from "vue";
import StudentVue from "./Student.vue";
export default {
data() {
return {
students: [{ id: 1, name: "Pepito" }, { id: 2, name: "Sue" }],
attendance: {}
};
},
methods: {
changeAttendance(studentId) {
// make sure to have an object when accessing a student!
let attendance = this.attendance[studentId] || {};
attendance.attended = !attendance.attended;
// start tracking - this will always "reset" the tracking though!
Vue.set(this.attendance, studentId, attendance);
}
},
components: { Student: StudentVue }
};
</script>
Since this.attendance[student.id] || {}
is being used multiple times, I'd capsulate this into a getAttendance(studentId) {...}
function though. In there, Vue.set
could be used to start tracking if the object did not exist before, making changeAttendance
a bit simpler and the intention of this || {}
more clear.
<template>
<table>
<Student
v-for="(student, index) in students"
:key="index"
:student="student"
:attendance="getAttendance(student.id)"
@attended-clicked="changeAttendance"
/>
</table>
</template>
<script>
import Vue from "vue";
import StudentVue from "./Student.vue";
export default {
data() {
return {
students: [{ id: 1, name: "Pepito" }, { id: 2, name: "Sue" }],
attendance: {}
};
},
methods: {
changeAttendance(studentId) {
let attendance = this.getAttendance(studentId);
attendance.attended = !attendance.attended;
},
getAttendance(studentId) {
const studentAttendance = this.attendance[studentId];
if (!studentAttendance) {
Vue.set(this.attendance, studentId, { attended: false });
}
return this.attendance[studentId];
}
},
components: { Student: StudentVue }
};
</script>
If the code base is using this.something[something]
in other components, the this.getAttendance
might look a bit inconsistent. In that case, it's probably better to change the result of the API call to all students with a default for them before feeding it to the Parent
component then. Personally, I'd try to use that option. And I'd use this.$set
instead of Vue.set
to get rid of the import Vue
line... ;)
Upvotes: 1