0
votes

I have a set of nested Vue JS Components that I am implementing two-way prop binding on, that I am unable to get working.

There is a DataTable child component nested inside a FoodCat parent component. I have an attribute called selected inside the FoodCat parent component that I passed down to DataTable via props. I want to implement two-way binding on the prop using the .sync method.

FoodCats.vue - Parent Component:

<template>
    <admin-data-table
        :dataTable="dataTable"
        :modelName="modelName"
        :collection="collection"
        :tblShowFields="tblShowFields"
        :selectedList.sync="selected"
    ></admin-data-table>
</template>

<script>
    import { MessageBox } from '../../MessageBox.js';
    import { crudFunctionsMixin } from './mixins/crud-functions.js';

    Vue.component('admin-data-table', require('../components/Admin/DataTable').default);

    export default {
        mixins: [ crudFunctionsMixin ],
        data() {
            return {
                model: "food-cat",
                modelName: "Food Category",
                modelNamePlural: "Food Categories",
                form: {
                    inputs: {
                        id: {
                            val: '', save: true,
                            hide: true
                        },
                        title: {
                            val: '', save: true, add: true,
                            gridSize: 12,
                            icon: 'business',
                            placeholder: 'Title'
                        },
                        slug: {
                            val: '', save: true, add: true
                        }
                    },
                    titleText: "Add Food Category",
                    errors: false
                },
                formSearch: {
                    inputs: {
                        keywords: {
                            show: true,
                            val: "",
                            icon: "keyboard",
                            placeholder: "Keywords"
                        }
                    }
                },
                toolbar: {
                    btns: [],
                    menuItems: []
                },
                dataTable: {
                    headers: [
                        { text: 'ID', value: 'id', sortable: true },
                        { text: 'Title', value: 'title', sortable: true },
                        { text: 'Slug', value: 'slug', sortable: true },
                        { sortable: false }
                    ],
                    pagination: {
                        sortBy: 'title'
                    },
                    rowButtons: []
                }
            }
        },
        watch: {
            selected: function(newSelectedList) {
                this.$root.selected = newSelectedList;
            }
        }
    }
</script>

DataTable.vue - Child Component:

<template>
    <v-flex xs12>
        <v-progress-linear :indeterminate="true" :height="3" color="#c79121" :active="dataTable.loadingVal" class="mb-0 mt-5"></v-progress-linear>
        <v-data-table :ref="modelName + 'Table'" v-model="selectedList" :headers="dataTable.headers" :items="collection" :pagination.sync="dataTable.pagination" select-all item-key="id" class="elevation-1" >
            <template v-slot:headers="props">
                <tr>
                    <th><v-checkbox :input-value="props.all" color="#c79121" :indeterminate="props.indeterminate" primary hide-details @click.stop="toggleAllSelected"></v-checkbox></th>
                    <th v-for="header in props.headers" :key="header.text"
                        :class="['column sortable', dataTable.pagination.descending ? 'desc' : 'asc', header.value === dataTable.pagination.sortBy ? 'active' : '']"
                        @click="changeSort(header.value)">
                        <v-icon small>arrow_upward</v-icon>
                        {{ header.text }}
                    </th>
                </tr>
            </template>
            <template v-slot:items="props">
                <tr :active="props.selected">
                    <td class="text-center align-middle" @click="props.selected = !props.selected">
                        <v-checkbox :input-value="props.selected" primary hide-details color="#c79121"></v-checkbox>
                    </td>
                    <td v-for="(field, key) in props.item" v-if="tblShowFields.includes(key)">{{ field }}</td>
                    <td class="text-right align-middle">
                        <v-btn title="Edit" color="primary" fab small @click="edit(props.item.id)"><v-icon>edit</v-icon></v-btn>
                        <v-btn title="Delete" color="error" fab small class="text-white" @click="remove(props.item.id)"><v-icon>delete_outline</v-icon></v-btn>
                    </td>
                </tr>
            </template>
            <template slot="no-data">
                <p class="text-xs-center">No Data</p>
            </template>
        </v-data-table>
    </v-flex>
</template>

<script>
    export default {
        name: "admin-data-table",
        props: [
            'dataTable',
            'collection',
            'modelName',
            'collection',
            'selectedList',
            'tblShowFields'
        ],
        watch: {
            selectedList: function(newList) {
                this.$emit('update:selectedList', newList);

            }
        }
    }
</script>

Currently the selectedList object inside the DataTable child component is working correctly with the table checkboxes, and changes are syncing correctly to the selected object in the FoodCat parent component, and to the $root instance.

However I am still receiving a vue warning that tells me not to modify the selectedList prop directly.

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "selectedList"


Why am I still receiving this warning? Did I implement this incorrectly?

1

1 Answers

3
votes

This is the culprit:

v-model="selectedList"

which is equivalent to

:value="selectedList" @input="selectedList = $event"

As you can see, you are assigning to selectedList in the handler for the input event.

When you are designing components in this way, be sure not to bind props with v-model, instead you have to handle the input event explicitly:

:value="selectedList" @input="$emit('update:selectedList', $event)"

Also I don't understand the purpose of the watcher? The prop should only change from the parent down to the child, so why would you need to emit an update back up to the parent in response?