I’m working with a Vue.js application where I need to watch changes on a nested property (isChecked) in a deeply nested component structure. My goal is to trigger a method (sumOfChecked) whenever the isChecked property changes in any row or its sub-rows.
Here’s a simplified version of my component structure:
<template>
<div class="tw-max-w-max">
<div v-for="(row, index) in rows" :key="index">
<fieldset class="tw-grid tw-grid-cols-[2fr_100px_120px_100px_100px_30px] tw-gap-4">
<v-list-item-title class="tw-text-sm tw-flex tw-justify-between tw-items-center">
<span>{{ row.kostenstelleName }}</span>
<v-checkbox
v-model="row.isChecked"
@change="onCheckboxClicked"
/>
</v-list-item-title>
<!-- Other fields omitted for brevity -->
</fieldset>
<KreuztabelleRow
v-if="row.subRows.length"
:is-subitem="true"
:kreuztabelle="{rows: row.subRows}"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
import KreuztabelleRow from './KreuztabelleRow.vue';
@Component({
components: { KreuztabelleRow },
})
export default class KreuztabelleRow extends Vue {
@Prop({ required: true, type: Object }) kreuztabelle;
@Prop({ required: false, default: false }) isSubitem;
get rows() {
return this.kreuztabelle?.rows || [];
}
@Emit('sum-of-checked')
onCheckboxClicked() {
return this.sumOfChecked();
}
sumOfChecked() {
const calculateSum = (rows) => {
return rows.reduce((sum, row) => {
if (row.isChecked) {
sum += row.summe || 0;
}
if (row.subRows && row.subRows.length > 0) {
sum += calculateSum(row.subRows);
}
return sum;
}, 0);
};
return calculateSum(this.rows);
}
@Watch('rows', { immediate: true, deep: true })
onRowsChange(newRows) {
newRows.forEach((row) => {
this.$watch(() => row.isChecked, this.onCheckboxClicked, { deep: true, immediate: true });
row.subRows?.forEach((subRow) => {
this.$watch(() => subRow.isChecked, this.onCheckboxClicked, { deep: true, immediate: true });
});
});
}
}
</script>
The Problem:
The sumOfChecked method is not being called when the isChecked property changes. I need this method to be triggered whenever a checkbox is toggled, even for nested sub-rows.
What I Tried:
I added a deep watcher on rows to listen for changes in isChecked properties.
Implemented a recursive function within sumOfChecked to calculate the sum of checked rows, including sub-rows.
Ensured that the checkbox change triggers the onCheckboxClicked method.
Despite these efforts, the sumOfChecked method is not getting triggered as expected.
Question:
How can I properly watch for changes in the isChecked property of each row and its sub-rows and trigger the sumOfChecked method accordingly? Is there a more effective way to achieve this in Vue.js?
The key to the solution is in the definition of the problem. A change is easy to track in the place when it occurs, v-checkbox
in this case. As long as there are problems with tracking it afterwards in a watcher, this becomes the first choice.
v-model
isn’t supposed to be used together with events, as this results in undetermined behaviour that isn’t documented, and it’s a mistake to use change
event on a component that doesn’t document it either. If there’s a need to add logic to v-model
, it should be desugared:
<v-checkbox
:modelValue="row.isChecked"
@update:modelValue="onCheckboxClicked(row, $event)"
/>
Where event listener receives a value and can do anything with it:
onCheckboxClicked(row, value) {
row.isChecked = value;
this.sumOfChecked();
}