Half_Duplex
Half_Duplex

Reputation: 5234

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>

Upvotes: 1

Views: 8388

Answers (2)

Anatoly
Anatoly

Reputation: 22783

  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')
    }
  }
}

Upvotes: 1

Arish Khan
Arish Khan

Reputation: 750

I would not recommend using the state for this task. since it not a very complex scenarios. you should use props and events for handling this kind of scenario

just modify the code a bit.

DataTable.vue

<script>
    methods: {
      onButtonClick: function(email) {
        console.log("clicked: " + email)
        this.$emit('openDialog') // or use any name here
      }
    }
</script>

UserAdmin.vue

<template>
  <v-container fluid >
      <!-- Listen to the event that you are emitting from DataTable.vue  -->
      <DataTable :rows="allUsers" :headers="headers" @showDialog="editUser = true" />

      <!-- Pass that variable as a Prop -->
      <EditUser :showDialog="editUser"  />
  </v-container>
</template>

<script>
....
    data: () => ({
      headers: [ 
        { text: 'Name', value: 'name' },
        { text: 'Username', value: 'email' },
        { text: 'Administrator', value: 'admin' },
        { text: "", value: "controls", sortable: false}
      ],

      editUser: false, // a flag to keep the status of modal.
    })

....
</script>

EditUser.vue

<script>
  export default {
    props: {
       showDialog: {
           type: Boolean,
           default: false
       }
    },
    data: () => ({
        dialog: false,
    }),
    mounted() {
        this.dialog = this.showDialog
    },
    watch: {
        showDialog() {
            if (this.showDialog)
                this.dialog = true
        }
    }
  }
</script>

I hope it should work, it worked for me in my scenario. I won't recommend using the Vuex store in this simple single level structure. Vuex should be used in case of some complex data structures where there are deep layers of components.

The code might have some syntax mistakes (do let me know). but i hope i just conveyed the concept

Upvotes: 2

Related Questions