Search code examples
vue.jsvuexvuetify.js

How do I open and close v-dialog from a component under its parent? Use Vuex?


I need to open a CRUD dialog from a data table component. Both the dialog and data table share the same parent. The data table is reusable but the CRUD dialog is not.

The use case seems very common. An admin page contains a table of data, each row containing an edit button that opens edit dialog.

I've attempted using Vuex below - however this error occurs:

[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'showUserModal' of undefined"

found in

---> <VBtn>
       <VSimpleTable>
         <VData>
           <VDataTable>
             <DataTable> at src/components/DataTable.vue
               <Settings> at src/views/Settings.vue
                 <VContent>
                   <VApp>
                     <App> at src/App.vue
                       <Root>

Why is the imported mutator not available and is this a good approach to achieving the common functionality?

I arrived at my current solution using these 2 approaches https://markus.oberlehner.net/blog/building-a-modal-dialog-with-vue-and-vuex/ https://forum.vuejs.org/t/how-to-trigger-a-modal-component-from-vuex-store/27243/9

UserAdmin.vue

    <template>
      <v-container fluid >
          <DataTable v-bind:rows="allUsers" v-bind:headers="headers" />
          <EditUser />
      </v-container>
    </template>
    
    <script>
    import { mapGetters, mapActions } from "vuex";
    import DataTable from '../components/DataTable';
    import EditUser from '../modals/EditUser';
    
    export default {
      name: 'UserAdmin',
      
      methods: {
        ...mapActions(["getUsers"])
      },
    
      computed: mapGetters(["allUsers"]),
      
      components: {
        DataTable, EditUser
      },
    
      data(){
        return {
          headers: [ 
            { text: 'Name', value: 'name' },
            { text: 'Username', value: 'email' },
            { text: 'Administrator', value: 'admin' },
            { text: "", value: "controls", sortable: false}
          ]
        }
      },
    
      created(){
        this.getUsers();
      }
    }
    </script>

DataTable.vue

    <template>
        <v-data-table
            :headers="headers"
            :items="rows"
            :items-per-page="5"
            class="elevation-1"
        >
        <!-- https://stackoverflow.com/questions/59081299/vuetify-insert-action-button-in-data-table-and-get-row-data --> 
         <template v-slot:item.controls="props">
            <v-btn class="my-2" fab dark x-small color="blue" @click="onButtonClick(props.item.email)">
              <v-icon dark>mdi-pencil</v-icon>
            </v-btn>
          </template> 
        </v-data-table>
    </template>
    
    <script>
    
      import { mapMutations } from "vuex";
    
      export default {
        name: "DataTable",
        props:["headers", "rows"],
        methods: {
          ...mapMutations(["toggleUserModal"]),
          onButtonClick: function(email) {
            console.log("clicked: " + email)
            this.toggleUserModal();
          }
        }
      }
    </script>

EditUser.vue

    <template>
      <v-row justify="center">
        <v-dialog v-model="dialog" persistent max-width="600px" v-show='showUserModal'>
          <v-card>
            <v-card-title>
              <span class="headline">User Profile</span>
            </v-card-title>
            <v-card-text>
              <v-container>
                <v-row>
                  <v-col cols="12" sm="6" md="4">
                    <v-text-field label="Legal first name*" required></v-text-field>
                  </v-col>
                  <v-col cols="12" sm="6" md="4">
                    <v-text-field label="Legal middle name" hint="example of helper text only on focus"></v-text-field>
                  </v-col>
                  <v-col cols="12" sm="6" md="4">
                    <v-text-field
                      label="Legal last name*"
                      hint="example of persistent helper text"
                      persistent-hint
                      required
                    ></v-text-field>
                  </v-col>
                  <v-col cols="12">
                    <v-text-field label="Email*" required></v-text-field>
                  </v-col>
                  <v-col cols="12">
                    <v-text-field label="Password*" type="password" required></v-text-field>
                  </v-col>
                  <v-col cols="12" sm="6">
                    <v-select
                      :items="['0-17', '18-29', '30-54', '54+']"
                      label="Age*"
                      required
                    ></v-select>
                  </v-col>
                  <v-col cols="12" sm="6">
                    <v-autocomplete
                      :items="['Skiing', 'Ice hockey', 'Soccer', 'Basketball', 'Hockey', 'Reading', 'Writing', 'Coding', 'Basejump']"
                      label="Interests"
                      multiple
                    ></v-autocomplete>
                  </v-col>
                </v-row>
              </v-container>
              <small>*indicates required field</small>
            </v-card-text>
            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn color="blue darken-1" text @click="dialog = false">Close</v-btn>
              <v-btn color="blue darken-1" text @click="dialog = false">Save</v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>
      </v-row>
    </template>
    
    <script>
      export default {
        data: () => ({
            dialog: false,
        }),
        computed: {
            showUserModal(){
                return this.$store.state.showUserModal
            }
        }
      }
    </script>

modals.js

    const state = {
        showUserModal: false
    }
    
    const mutations = {
        toggleUserModal: () => (this.showUserModal = !this.showUserModal)
    }
    
    const getters = {
        showUserModal: state => {
            return state.showUserModal
        }
    }
    
    export default {
        state,
        getters,
        mutations
    }

New code based on @Anatoly suggestions - everything works except the events emitted from the dialog, ex: onEditUserConfirmed are not picked up in the parent component.

ModalComponent

    <template>
      <v-row justify="center">
        <v-dialog v-model="visible" persistent max-width="600px">
          <v-card v-if="user">
            <v-card-title>
              <span class="headline">User Profile</span>
            </v-card-title>
            <v-card-text>
              <v-container>
                <v-row>
                  <v-col cols="12" sm="6" md="4">
                    <v-text-field v-model="user.name" label="Legal first name*" required></v-text-field>
                  </v-col>
                  <v-col cols="12" sm="6" md="4">
                    <v-text-field label="Legal middle name" hint="example of helper text only on focus"></v-text-field>
                  </v-col>
                  <v-col cols="12" sm="6" md="4">
                    <v-text-field
                      label="Legal last name*"
                      hint="example of persistent helper text"
                      persistent-hint
                      required
                    ></v-text-field>
                  </v-col>
                  <v-col cols="12">
                    <v-text-field label="Email*" required></v-text-field>
                  </v-col>
                  <v-col cols="12">
                    <v-text-field label="Password*" type="password" required></v-text-field>
                  </v-col>
                  <v-col cols="12" sm="6">
                    <v-select :items="['0-17', '18-29', '30-54', '54+']" label="Age*" required></v-select>
                  </v-col>
                  <v-col cols="12" sm="6">
                    <v-autocomplete
                      :items="['Skiing', 'Ice hockey', 'Soccer', 'Basketball', 'Hockey', 'Reading', 'Writing', 'Coding', 'Basejump']"
                      label="Interests"
                      multiple
                    ></v-autocomplete>
                  </v-col>
                </v-row>
              </v-container>
              <small>*indicates required field</small>
            </v-card-text>
            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn color="blue darken-1" text @click="onCancel">Close</v-btn>
              <v-btn color="blue darken-1" text @click="onSave">Save</v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>
      </v-row>
    </template>
    
    <script>
    export default {
      name: "EditUser",
      props: {
        user: Object,
        visible: {
          type: Boolean,
          default: false
        }
      },
      methods: {
        onSave() {
          console.log('save button gets here...')
          this.$emit("onEditUserConfirmed", this.user);
        },
        onCancel() {
          console.log('cancel button gets here...')
          this.$emit("onEditUserCancelled");
        }
      }
    };
    </script>

Parent Component

    <template>
      <v-container fluid>
        <v-data-table :headers="headers" :items="allUsers" :items-per-page="5" class="elevation-1">
          <!-- https://stackoverflow.com/questions/59081299/vuetify-insert-action-button-in-data-table-and-get-row-data -->
          <template v-slot:item.controls="props">
            <v-btn class="my-2" fab dark x-small color="blue" @click="onEditClick(props.item)">
              <v-icon dark>mdi-pencil</v-icon>
            </v-btn>
          </template>
        </v-data-table>
    
        <EditUser
          :user="user"
          :visible="isDialogVisible"
          @confirmed="onEditUserConfirmed"
          @cancelled="onEditUserCancelled"
        />
      </v-container>
    </template>
    
    <script>
    import { mapGetters, mapActions } from "vuex";
    import EditUser from "../modals/EditUser";
    
    export default {
      name: "Settings",
      data() {
        return {
          user: null,
          isDialogVisible: false,
          headers: [
            { text: "Name", value: "name" },
            { text: "Username", value: "email" },
            { text: "Administrator", value: "admin" },
            { text: "", value: "controls", sortable: false }
          ]
        };
      },
      methods: {
        ...mapActions(["getUsers"]),
        onEditClick: function(user) {
          console.log('Editing user: ' + user.email)
          this.user = user;
          this.isDialogVisible = true;
        },
        onEditUserConfirmed(user) {
          console.log('Saving user: ' + user.email)
          this.isDialogVisible = false;
        },
        onEditUserCancelled () {
          this.isDialogVisible = false;
        }
      },
    
      computed: mapGetters(["allUsers"]),
    
      components: {
        EditUser
      },
    
      created() {
        this.getUsers();
      }
    };
    </script>

Solution

    1. Use an event in a table component to inform a parent component you wish to edit a user (send a selected user in this event).
    2. Catch the event in a parent component, write a user from the event to a prop in data section and pass this prop to a dialog component.
    3. Use a prop to show/hide dialog from a parent component
    4. Use an event to receive edited user after dialog confirmation.

    Something like this:

    Parent component

    <DataTable v-bind:rows="allUsers" v-bind:headers="headers" @onEdit="onEditUser"/>
    <EditUser :user="user" :visible="isDialogVisible" @confirmed="onEditUserConfirmed" @cancelled="onEditUserCancelled"/>
    
    ...
    data: {
      return {
        // other data
        user: null,
        isDialogVisible : false
      }
    },
    methods: {
      onEditUser (user) {
        this.user = user
        this.isDialogVisible = true
      },
      onEditUserConfirmed (user) {
       // hide a dialog 
       this.isDialogVisible = false 
       // save a user and refresh a table
      },
      onEditUserCancelled () {
       // hide a dialog 
       this.isDialogVisible = false 
      }
    }
    

    Table component:

    // better send a whole user object insteaf of just e-mail prop? It's up to you
    @click="onButtonClick(props.item)"
    ...
    methods: {
          onButtonClick: function(user) {
            this.$emit('onEdit', user)
          }
        }
    

    Dialog component:

     <v-dialog v-model="visible" ...
       // render card only if user is passed
       <v-card v-if="user">
       <v-col cols="12" sm="6" md="4">
         <v-text-field v-model="user.firstName" label="Legal first name*" required></v-text-field>
        </v-col>
    ...
    <v-btn color="blue darken-1" text @click="onCancel">Close</v-btn>
    <v-btn color="blue darken-1" text @click="onSave">Save</v-btn>
    ...
    export default {
      props: {
       user: {
         type: Object
       },
       visible: {
         type: Boolean,
         default: false
       }
      },
    ...
      methods: {
        onSave() {
          this.$emit('confirmed', this.user)
        },
        onCancel () {
          this.$emit('cancelled')
        }
      }
    }