如何从其父组件下的组件打开和关闭 v-dialog?使用 Vuex?

Posted

技术标签:

【中文标题】如何从其父组件下的组件打开和关闭 v-dialog?使用 Vuex?【英文标题】:How do I open and close v-dialog from a component under its parent? Use Vuex? 【发布时间】:2020-09-03 10:51:23 【问题描述】:

我需要从数据表组件打开一个 CRUD 对话框。对话框和数据表共享同一个父级。数据表是可重用的,但 CRUD 对话框不是。

用例似乎很常见。管理页面包含一个数据表,每行包含一个打开编辑对话框的编辑按钮。

我尝试在下面使用 Vuex - 但是出现了这个错误:

[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>

为什么导入的 mutator 不可用,这是实现通用功能的好方法吗?

我使用这两种方法得出了我当前的解决方案 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://***.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- 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

基于@Anatoly 建议的新代码 - 除了从对话框发出的事件外,一切正常,例如:onEditUserConfirmed 不会在父组件中拾取。

模态组件

<template>
  <v-row justify="center">
    <v-dialog v-model="visible" persistent max->
      <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>

父组件

<template>
  <v-container fluid>
    <v-data-table :headers="headers" :items="allUsers" :items-per-page="5" class="elevation-1">
      <!-- https://***.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>

【问题讨论】:

【参考方案1】:

我不建议在此任务中使用状态。因为它不是一个非常复杂的场景。你应该使用道具和事件来处理这种情况

稍微修改一下代码。

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>

我希望它应该起作用,它在我的场景中对我有用。我不建议在这种简单的单级结构中使用 Vuex 商店。 Vuex应该用于一些复杂的数据结构,其中有很深的组件。

代码可能有一些语法错误(请告诉我)。但我希望我只是传达了这个概念

【讨论】:

【参考方案2】:
    使用表格组件中的事件通知父组件您希望编辑用户(在此事件中发送选定的用户)。 在父组件中捕获事件,将事件中的用户写入 data 部分中的道具,并将此道具传递给对话框组件。 使用道具显示/隐藏父组件的对话框 在对话确认后使用事件接收编辑的用户。

类似这样的:

父组件

<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 
  

表格组件:

// 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)
      
    

对话框组件:

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


【讨论】:

这行得通,但我也将数据表组件向下折叠到父级中,并将上述内容应用于 v-data-table。似乎这会减少开销。 我发现发出的动作没有在父级中拾取。但除此之外,一切正常。 我固定在我的回答中发出呼叫。检查它们。 那行得通。父级中@v:on 之间是否有任何偏好? @ 只是v-on 的简写,:v-bind 的简写。我总是使用@:,这样模板中的noise 就更少了,而真正的内容更多了。

以上是关于如何从其父组件下的组件打开和关闭 v-dialog?使用 Vuex?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Vuejs 中从其父组件设置嵌套组件的样式?

使用“动态宽度”时如何将过渡应用于 v-dialog,这意味着宽度 =“未设置”?

如何从 angular-material2 对话框与其父级进行通信

如何在 ReactJS 中将更改的状态从子组件传递给其父组件

如何呈现包含打开和未关闭标签的组件?反应.js

删除元素后,子组件无法从其父组件获取数组。