66

I'm using the VueJS Vuetify framework and I need to open a dialog - that gets imported as a component template - from another template. Once the Menu button in App.vue got clicked, the Modal should open. Here is my setup:

  • App.vue = navigation template with Menu button
  • Modal.vue = Modal template, imported as global in main.js

main.js

import Modal from './components/Modal.vue'
Vue.component('modal', Modal)

Modal.vue Template:

<template>
  <v-layout row justify-center>
    <v-btn color="primary" dark @click.native.stop="dialog = true">Open Dialog</v-btn>
    <v-dialog v-model="dialog" max-width="290">
      <v-card>
        <v-card-title class="headline">Use Google's location service?</v-card-title>
        <v-card-text>Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.</v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="green darken-1" flat="flat" @click.native="dialog = false">Disagree</v-btn>
          <v-btn color="green darken-1" flat="flat" @click.native="dialog = false">Agree</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-layout>
</template>
<script>
  export default {
    data () {
      return {
        dialog: false
      }
    }
  }
</script>

How to open the dialog?

Soleno
  • 1,949
  • 1
  • 11
  • 8
Tom
  • 5,588
  • 20
  • 77
  • 129

8 Answers8

190

No event bus needed and v-model

Update:

When I first answered this, I posted my answer as a "workaround", since it didn't felt completely "right" at the time and I was new to Vue.js. I wanted to open or close the dialog by using a v-model directive, but I couldn't get there. After some time I found how to do this in the docs, using the input event and the value property, and here's how I think it should be done without an event bus.

Parent component:

<template>
   <v-btn color="accent" large @click.stop="showScheduleForm=true">    
   <ScheduleForm v-model="showScheduleForm" />
</template>

<script>
import ScheduleForm from '~/components/ScheduleForm'

export default {
  data () {
    return {
      showScheduleForm: false
    }
  },
  components: {
    ScheduleForm
  }
}
</script>

Child component (ScheduleForm):

<template>
<v-dialog v-model="show" max-width="500px">
  <v-card>
    <v-card-actions>
      <v-btn color="primary" flat @click.stop="show=false">Close</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>
</template>

<script>
export default {
  props: {
     value: Boolean
  },
  computed: {
    show: {
      get () {
        return this.value
      },
      set (value) {
         this.$emit('input', value)
      }
    }
  }
}
</script>

Original answer:

I was able to work around this without the need for a global event bus.

I used a computed property with a getter AND a setter. Since Vue warns you about mutating the parent property directly, in the setter I simply emitted an event to the parent.

Here's the code:

Parent component:

<template>
   <v-btn color="accent" large @click.stop="showScheduleForm=true"></v-btn>   
   <ScheduleForm :visible="showScheduleForm" @close="showScheduleForm=false" />
</template>

<script>
import ScheduleForm from '~/components/ScheduleForm'

export default {
  data () {
    return {
      showScheduleForm: false
    }
  },
  components: {
    ScheduleForm
  }
}
</script>

Child component (ScheduleForm):

<template>
<v-dialog v-model="show" max-width="500px">
  <v-card>
    <v-card-actions>
      <v-btn color="primary" flat @click.stop="show=false">Close</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>
</template>

<script>
export default {
  props: ['visible'],
  computed: {
    show: {
      get () {
        return this.visible
      },
      set (value) {
        if (!value) {
          this.$emit('close')
        }
      }
    }
  }
}
</script>
tony19
  • 125,647
  • 18
  • 229
  • 307
Matheus Dal'Pizzol
  • 5,735
  • 3
  • 19
  • 29
  • 14
    This is an ABSOLUTELY perfect answer I think. Thank you. This works perfect! – Canet Robern Jan 02 '19 at 07:45
  • 2
    Tried that, it works as necessary. Better get this to the top! – Iulian Pinzaru Feb 01 '19 at 23:13
  • 2
    If the above doesn't work, change your child's computed property to use $attrs like this: `show: { get () { return this.$attrs.value }, set (value) { this.$emit('input', value) } }` – Mark Sonn Sep 02 '19 at 06:22
  • 3
    This is the best solution IMO. No event bus needed! – user616 Sep 12 '19 at 01:34
  • This is brilliant. – udog Apr 30 '21 at 21:25
  • For some reason I had hard time getting this solution working, it turned out I was missing `required: true` in the `props.value` object, my props just appeared as an $attrs and no computed props showed in the dev tool unless I did that. I still wonder why... – Alan Kersaudy Apr 28 '22 at 15:37
  • 1
    For me it only worked after changing the event emitting in the child to `this.$emit('update:modelValue', value)`. See [this answer](https://stackoverflow.com/a/69681998/6608142) for details. – KorbenDose Apr 29 '23 at 10:02
23

There are many ways to do it such is Vuex,Event Bus,Props with which you can manage whether the modal have to open or to close.I will show you my favourite way using the .sync modifier:

First i will simplify you question(the code part)

Parent component

<template>
   <div>
     <button @click="dialog=true">Open Dialog</button>
     <Child :dialog.sync="dialog" />
   </div>
</template>

<script>
import Child from './Child.vue'
export default {
    components: {
      Child
    },
    data: {
      return {
        dialog: false
      }
   }
}
</script>

Child(Dialog) Component

<template>
  <v-layout row justify-center>
    <v-dialog v-model="dialog" persistent max-width="290">
      <v-card>
        <v-card-title class="headline">Use Google's location service?</v-card-title>
        <v-card-text>Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.</v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="green darken-1" flat @click.native="close">Close</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-layout>
</template>

<script>

  export default {
    props: {
        dialog: {
        default: false
      }
    },
    methods: {
        close() {
        this.$emit('update:dialog', false)
      }
    }
  }

</script>
Roland
  • 24,554
  • 4
  • 99
  • 97
  • Are you sure? https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier – Roland Apr 21 '19 at 15:16
  • I found a documentation page stating that, but just found https://medium.com/front-end-weekly/vues-v-model-directive-vs-sync-modifier-d1f83957c57c which states that `.sync` has been re-added in 2.3 - but it didn't work for me. – Sebastian Apr 21 '19 at 19:54
  • This solution works for me, thanks! For some reason I'm having trouble grasping how to work with the sync modifier. – Petercopter Jul 02 '20 at 16:07
  • If you don't want to use 'persistent' on the dialog, you can also call the close method on the click outside event of the dialog '@click:outside="close"' – Robert Jul 24 '21 at 11:59
23

Simple minimal working example

codepen

Pass value prop as value to v-dialog component, and from child dialog emit input event whenever you want to close it:

//CustomDialog.vue
<v-dialog :value="value" @input="$emit('input', $event)">
  <v-btn color="red" @click.native="$emit('input', false)">Close</v-btn>
</v-dialog>
...
props:['value']

and add v-model to your parent

//Parent.vue
<custom-dialog v-model="dialog">

So no custom event bus, no data, no watch, no computed.

Traxo
  • 18,464
  • 4
  • 75
  • 87
  • this seems a very easy solution but I'm having an hard time understanding why we should listen to the input event and re-trigger the input event on the declaration. I cannot find any documentation where and when this input event is triggered...thanks – Leonardo Bernardini Jan 02 '20 at 20:41
  • @LeonardoBernardini because "input" event gets emitted when `v-dialog` "value" changes. For example, if we don't use "persistent" prop, then clicking outside of the v-dialog will also trigger that event. So we use it to also cover that case. And also `v-model` should not be used on a prop, in this case `value`. – Traxo Jan 02 '20 at 20:46
  • @LeonardoBernardini Notice I explained it also in another question in the past, so check out that whole thread maybe: https://stackoverflow.com/questions/49310417/vuejs-change-v-model-variable-from-child/49315704#comment85670372_49311319 (I'm not sure if Vue changed some practices since then tho) – Traxo Jan 03 '20 at 09:41
  • thanks, this seems to be not documented and it's confusing... now it's clear! – Leonardo Bernardini Jan 04 '20 at 08:47
  • It works only if you don't check for props type. Try adding props:{value: { type: Boolean, required: true }}. It'll show Invalid prop: type check failed for prop 'value'. Expected Boolean, got Undefined when you press ESC key to close dialog. https://codepen.io/duongthienlee/pen/abNjrbv?editors=1011 – Winchester Sep 16 '20 at 14:56
  • @ThienLy You are right, I edited the answer to resolve it. `@input="$emit('input', $event)"` – Traxo Sep 16 '20 at 15:37
  • In Vue 3, replace `'input'` event name with the [equivalent](https://v3-migration.vuejs.org/breaking-changes/v-model.html#migration-strategy): `'update:modelValue'` – Dunc Apr 04 '23 at 15:37
14

You can open the dialog using custom events and using an event bus for non parent-child communication.

If your application gets a bit more complex I recommend you use Vuex for state management.


Event bus solution:

In your main.js or in a new file create and export a new Vue instance :

export const bus = new Vue()

In app.vue import the busand emit the event:

<template>
  <div>
    <button @click.prevent="openMyDialog()">my button</button>
  </div>
</template>

<script>
  import {bus} from '../main' // import the bus from main.js or new file
  export default {
    methods: {
      openMyDialog () {
        bus.$emit('dialog', true) // emit the event to the bus
      }
    }
  }
</script>

In modal.vue also import the bus and listen for the event in the created hook:

<script>
  import {bus} from '../main'    
  export default {
    created () {
      var vm = this
      bus.$on('dialog', function (value) {
        vm.dialog = value
      })
    }
  }
</script>
tony19
  • 125,647
  • 18
  • 229
  • 307
Soleno
  • 1,949
  • 1
  • 11
  • 8
  • I tried it and got this _'bus' is not defined _ error after running *npm run dev*: `error in ./src/components/Modal.vue ✘ http://eslint.org/docs/rules/no-undef 'bus' is not defined src/components/Modal.vue:23:5 bus.$on('dialog', function (value) { ^ ✘ 1 problem (1 error, 0 warnings) Errors: 1 http://eslint.org/docs/rules/no-undef @ ./src/main.js 37:0-43 @ multi ./build/dev-client babel-polyfill ./src/main.js` What did I miss? – Tom Jan 01 '18 at 21:23
  • 2
    Oh, sorry! I corrected my answer! Of course we have to export and import the bus in the "moduled" version. For not messing it up again I have also tested the code :) Sorry again for that quick shot. – Soleno Jan 02 '18 at 09:42
  • Thanks for your help! :) There was an error because the data part was missing. Is this the correct/best-practice fix? `import {bus} from '../main' export default { data () { return { dialog: false } }, created () { var vm = this bus.$on('dialog', function (value) { vm.dialog = value }) } }` Thanks again – Tom Jan 02 '18 at 14:30
  • 1
    You're welcome :) Glad to help! You find the event bus solution in the official docs: https://vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication. Here it's just adapted for the module system. So I would say it's a best practice. But it's only suited for small projects. I could imagine since you're using vuetify that your app could get a bit bigger. And for bigger apps it's recommended to use Vuex: https://vuex.vuejs.org/en/ . It's pretty easy and straight forward. That's what I usually use. – Soleno Jan 02 '18 at 16:42
  • How does it work the other way around btw? I need to call a function in the _App.vue_ from a component called _Card.vue_. This code is in _Card.vue_: `` and the _openAdd()_ function is in _App.vue_ How to call it?` – Tom Jan 07 '18 at 00:58
  • 1
    Same principle. You would emit an event in your card.vue and listen to it in the app.vue. – Soleno Jan 07 '18 at 05:35
4

The most simpler way I found to do it is:

in data() of component, return a attribute, let's say, dialog.

When you include a component, you can set a reference to your component tag. E.g.:

import Edit from '../payment/edit.vue';

<edit ref="edit_reference"></edit>

Then, inside my component, I have set a method:

        open: function () {
            var vm = this;

            vm.dialog = true;
        }

Finally, I can call it from parent, using:

  editar(item)
  {
      var vm = this;

      vm.$refs.edit_reference.open();
  }
Marco
  • 2,757
  • 1
  • 19
  • 24
  • 1
    Thank you. Why do you write "var vm = this" instead of the shorter "this.$refs.edit_reference.open()"? – Tom Jun 12 '18 at 15:10
  • Because of the scope problem, although I not really sure it would be a problem, just to be safe as I was learning the concept of components. – Marco Jun 12 '18 at 20:58
  • 1
    this is brilliant! – Ron Al Jun 01 '20 at 14:38
1

I prefer use this:

DialogConfirm.vue

<template>
  <v-dialog :value="visible" max-width="450">
    <v-card>
      <v-card-title v-text="title" />
      <v-card-text v-text="message" />
      <v-card-actions v-if="visible">
        <template v-for="action in value">
          <v-spacer :key="action.label" v-if="typeof action == 'string'" />
          <v-btn
            v-else
            text
            :key="action.label"
            v-text="action.label"
            @click="doAction(action.action)"
            :color="action.color"
          />
        </template>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component';
import { Prop, Watch } from 'vue-property-decorator';

@Component
export default class DialogConfirm extends Vue {

  @Prop({ type: String, default: "Confirm" })
  title: string

  @Prop({ type: String, default: "Are you sure?" })
  message: string

  @Prop({ type: Array, default: undefined })
  value: { label: string, action: () => boolean, color: string }[]

  get visible() {
    return Array.isArray(this.value) && this.value.length > 0
  }

  doAction(action: () => boolean) {
    if ('undefined' == typeof action || action() !== false) {
      this.$emit('input', null)
    }
  }
}
</script>

Usage Example

/** Disable AP Mode */
  setApMode(enable: boolean) {
    const action = () => {
      Api.get('wifi', {
        params: {
          ap: enable
        }
      }).then(response => this.$store.dispatch('status'))
    }
    if (enable == true) {
      // No confirmation
      return action();
    }
    this.dialogTitle = 'Are you sure?'
    this.dialogMessage = "you may lost connection to this device.";
    this.dialogActions = [
      {
        label: 'Cancel',
        color: 'success'
      },
      'spacer',
      {
        label: "OK, Disable it",
        color: "error",
        action
      }
    ]
  }

Mochamad Arifin
  • 418
  • 5
  • 9
0

within your App.vue template add this

<modal></model>

it will render your current Modal.vue template with v-btn and v-dialog

now inside it there will be one button - Open Dialog when you click on that modal will open.

Hardik Satasiya
  • 9,547
  • 3
  • 22
  • 40
  • I need to open that modal also from within the navigation menu. How to do that? – Tom Dec 30 '17 at 18:18
  • When you create `v-model` you can bind `v-model` and when you make that v-model `true` your model will appear and when you make it `false` it will hide. – Hardik Satasiya Dec 31 '17 at 05:02
0
methods: {
  openDialog(e) {
    this.dialog = true;
  }
},

This one works for me

Piatos
  • 1
  • 1