VueJs 2.0 memancarkan event dari grand child ke komponen grand parent-nya

97

Tampaknya Vue.js 2.0 tidak memancarkan peristiwa dari anak cucu ke komponen induknya.

Vue.component('parent', {
  template: '<div>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 'No action'
    }
  },
  methods: {
    performAction() { this.action = 'actionDone' }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child></grand-child></div>'
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Do Event</button></div>',
  methods: {
    doEvent() { this.$emit('eventtriggered') }
  }
})

new Vue({
  el: '#app'
})

JsFiddle ini memecahkan masalah https://jsfiddle.net/y5dvkqbd/4/ , tetapi dengan mengemisikan dua peristiwa:

  • Satu dari cucu sampai komponen tengah
  • Kemudian dipancarkan lagi dari komponen tengah ke grand parent

Menambahkan acara tengah ini tampaknya berulang dan tidak perlu. Apakah ada cara untuk memancarkan langsung ke kakek nenek yang tidak saya ketahui?

BassMHL
sumber

Jawaban:

73

Vue 2.4 memperkenalkan cara untuk dengan mudah melewatkan acara ke hierarki menggunakan vm.$listeners

Dari https://vuejs.org/v2/api/#vm-listeners :

Berisi v-onevent listener parent-scope (tanpa .nativepengubah). Ini dapat diteruskan ke komponen dalam melalui v-on="$listeners"- berguna saat membuat komponen pembungkus transparan.

Lihat potongan di bawah menggunakan v-on="$listeners"dalam grand-childkomponen dalam childtemplate yang:

Vue.component('parent', {
  template:
    '<div>' +
      '<p>I am the parent. The value is {{displayValue}}.</p>' +
      '<child @toggle-value="toggleValue"></child>' +
    '</div>',
  data() {
    return {
      value: false
    }
  },
  methods: {
    toggleValue() { this.value = !this.value }
  },
  computed: {
    displayValue() {
      return (this.value ? "ON" : "OFF")
    }
  }
})

Vue.component('child', {
  template:
    '<div class="child">' +
      '<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
      '<grand-child v-on="$listeners"></grand-child>' +
    '</div>'
})

Vue.component('grand-child', {
  template:
    '<div class="child">' +
      '<p>I am the grand-child: ' +
        '<button @click="emitToggleEvent">Toggle the value</button>' +
      '</p>' +
    '</div>',
  methods: {
    emitToggleEvent() { this.$emit('toggle-value') }
  }
})

new Vue({
  el: '#app'
})
.child {
  padding: 10px;
  border: 1px solid #ddd;
  background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <parent></parent>
</div>

Michael Rush
sumber
42

Komunitas Vue umumnya lebih suka menggunakan Vuex untuk menyelesaikan masalah semacam ini. Perubahan dibuat pada status Vuex dan representasi DOM hanya mengalir dari situ, menghilangkan kebutuhan akan kejadian dalam banyak kasus.

Kecuali itu, pemancaran ulang mungkin akan menjadi pilihan terbaik berikutnya, dan terakhir Anda mungkin memilih untuk menggunakan bus acara seperti yang dijelaskan dalam jawaban lain yang sangat dipilih untuk pertanyaan ini.

Jawaban di bawah ini adalah jawaban asli saya untuk pertanyaan ini dan bukan pendekatan yang akan saya ambil sekarang, memiliki lebih banyak pengalaman dengan Vue.


Ini adalah kasus di mana saya mungkin tidak setuju dengan pilihan desain Vue dan menggunakan DOM.

Dalam grand-child,

methods: {
    doEvent() { 
        try {
            this.$el.dispatchEvent(new Event("eventtriggered"));
        } catch (e) {
            // handle IE not supporting Event constructor
            var evt = document.createEvent("Event");
            evt.initEvent("eventtriggered", true, false);
            this.$el.dispatchEvent(evt);
        }
    }
}

dan di parent,

mounted(){
    this.$el.addEventListener("eventtriggered", () => this.performAction())
}

Jika tidak, ya, Anda harus mengeluarkan ulang, atau menggunakan bus.

Catatan: Saya menambahkan kode dalam metode doEvent untuk menangani IE; kode itu dapat diekstraksi dengan cara yang dapat digunakan kembali.

Bert
sumber
Ini berperilaku berbeda untuk IE? tidak menyadari ada ketidaksesuaian browser dengan vue ...
BassMHL
@BassemLhm Vue baik-baik saja dengan IE. Masalah dengan IE bukanlah Vue, ini adalah solusi DOM dan Anda tidak dapat melakukan Event () baru di IE. Anda harus document.createEvent (). Saya dapat menambahkan dukungan IE jika diperlukan.
Bert
Tidak masuk akal menginstal vuex hanya untuk satu kasus sederhana.
Adam Orlov
@AdamOrlov Saya setuju dengan Anda.
Bert
Solusi yang lebih sederhana: stackoverflow.com/a/55650245/841591
digout
34

JAWABAN BARU (Pembaruan Nov-2018)

Saya menemukan bahwa kami sebenarnya dapat melakukan ini dengan memanfaatkan $parentproperti di komponen grand child:

this.$parent.$emit("submit", {somekey: somevalue})

Jauh lebih bersih dan sederhana.

BassMHL
sumber
21
Perhatikan bahwa ini hanya berfungsi jika hubungannya adalah anak -> kakek-nenek. Ini tidak berfungsi jika anak dapat bersarang di tingkat arbitrer yang dalam.
Kuantitas
Lihat jawaban saya stackoverflow.com/a/55650245/841591 sebagai tanggapan atas komentar dari @Qtax
digout
8
Anda tidak ingin hal semacam itu terjadi dalam proyek besar Anda. Anda meletakkan 'anak' di dalam transitionatau komponen pembungkus lainnya dan itu akan pecah, meninggalkan Anda dengan tanda tanya besar di kepala Anda.
Adam Orlov
1
@AdamOrlov Saya setuju, ini adalah praktik yang buruk. Harap tangani acara seperti ini menggunakan toko Vuex.
Fabian von Ellerts
Ini lebih sederhana: stackoverflow.com/a/55650245/841591
menggali
26

Ya, Anda benar kejadian hanya pergi dari anak ke orang tua. Mereka tidak melangkah lebih jauh, misalnya dari anak ke kakek-nenek.

Dokumentasi Vue (secara singkat) membahas situasi ini di bagian Komunikasi Non Parent-Child .

Ide umumnya adalah bahwa dalam komponen grandparent Anda membuat kosong Vue komponen yang diturunkan dari kakek-nenek ke anak-anak dan cucu melalui alat peraga. Kakek kemudian mendengarkan acara dan cucu memancarkan acara di "bus acara" itu.

Beberapa aplikasi menggunakan bus peristiwa global, bukan bus peristiwa per komponen. Menggunakan bus peristiwa global berarti Anda harus memiliki nama peristiwa atau namespacing unik sehingga peristiwa tidak bentrok di antara komponen yang berbeda.

Berikut adalah contoh cara mengimplementasikan bus acara global sederhana .

Sly_cardinal
sumber
18

Solusi lain akan on / emit di root node:

Menggunakan vm.$root.$emitdi grand-child , lalu menggunakanvm.$root.$on di leluhur (atau di mana pun Anda suka).

Diperbarui : terkadang Anda ingin menonaktifkan pendengar di beberapa situasi tertentu, gunakan vm. $ Off (misalnya: vm.$root.off('event-name')inside lifecycle hook = beforeDestroy ).

Vue.component('parent', {
  template: '<div><button @click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 1,
      eventEnable: false
    }
  },
  created: function () {
    this.addEventListener()
  },
  beforeDestroy: function () {
    this.removeEventListener()
  },
  methods: {
    performAction() { this.action += 1 },
    toggleEventListener: function () {
      if (this.eventEnable) {
        this.removeEventListener()
      } else {
        this.addEventListener()
      }
    },
    addEventListener: function () {
      this.$root.$on('eventtriggered1', () => {
        this.performAction()
      })
      this.eventEnable = true
    },
    removeEventListener: function () {
      this.$root.$off('eventtriggered1')
      this.eventEnable = false
    }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child @eventtriggered="doEvent"></grand-child></div>',
  methods: {
    doEvent() { 
    	//this.$emit('eventtriggered') 
    }
  }
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Emit Event</button></div>',
  methods: {
    doEvent() { this.$root.$emit('eventtriggered1') }
  }
})

new Vue({
  el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<div id="app">
  <parent></parent>
</div>

Sphinx
sumber
17

Jika Anda ingin fleksibel dan hanya menyiarkan acara kepada semua orang tua dan orang tua mereka secara rekursif hingga ke root, Anda dapat melakukan sesuatu seperti:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
menggali
sumber
4

Ini adalah satu-satunya kasus ketika saya menggunakan bus acara !! Untuk meneruskan data dari anak bertingkat dalam, ke tidak langsung ke induk, komunikasi.

Pertama : Buat file js (saya beri nama eventbus.js) dengan konten ini:

import Vue from 'vue'    
Vue.prototype.$event = new Vue()

Kedua : Dalam komponen anak Anda, keluarkan acara:

this.$event.$emit('event_name', 'data to pass')

Ketiga : Di induk, dengarkan acara itu:

this.$event.$on('event_name', (data) => {
  console.log(data)
})

Catatan: Jika Anda tidak menginginkan acara itu lagi, harap batalkan pendaftarannya:

this.$event.$off('event_name')

INFO: Tidak perlu membaca pendapat pribadi di bawah ini

Saya tidak suka menggunakan vuex untuk komunikasi kakek-anak ke kakek-tua (Atau tingkat komunikasi serupa).

Di vue.js untuk meneruskan data dari grand-parent ke grand-child Anda bisa menggunakan sediakan / inject . Tetapi tidak ada yang serupa untuk hal sebaliknya. (cucu sampai kakek) Jadi saya menggunakan bus acara setiap kali saya harus melakukan komunikasi semacam itu.

roli roli
sumber
2

Saya telah membuat mixin pendek berdasarkan jawaban @digout. Anda ingin meletakkannya, sebelum inisialisasi instance Vue Anda (Vue baru ...) untuk menggunakannya secara global dalam proyek. Anda dapat menggunakannya seperti acara biasa.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
kubaklamca
sumber
solusi ini adalah apa yang saya gunakan untuk implementasi saya, tetapi saya menambahkan parameter tambahan targetRefyang menghentikan penyebaran pada komponen yang Anda targetkan. The whileKondisi itu akan mencakup && vm.$refs[targetRef]- Anda juga akan perlu untuk memasukkan bahwa refatribut pada komponen yang ditargetkan Dalam kasus penggunaan saya saya tidak perlu terowongan sepanjang jalan ke akar, menyimpan beberapa peristiwa dari menembak dan mungkin beberapa nanodetik berharga waktu
mcgraw
2

Komponen VueJS 2 memiliki a $parent properti yang berisi komponen induknya.

Komponen induk itu juga termasuk miliknya sendiri $parent propertinya .

Kemudian, mengakses komponen "grandparent" adalah masalah mengakses komponen "parent's parent":

this.$parent["$parent"].$emit("myevent", { data: 123 });

Bagaimanapun, ini agak rumit , dan saya merekomendasikan menggunakan manajer keadaan global seperti Vuex atau alat serupa, seperti yang dikatakan responden lain.

rogervila.dll
sumber
1

Mengasah jawaban @kubaklam dan @ digout, inilah yang saya gunakan untuk menghindari pancaran pada setiap komponen induk antara cucu dan kakek (mungkin jauh):

{
  methods: {
    tunnelEmit (event, ...payload) {
      let vm = this
      while (vm && !vm.$listeners[event]) {
        vm = vm.$parent
      }
      if (!vm) return console.error(`no target listener for event "${event}"`)
      vm.$emit(event, ...payload)
    }
  }
}

Saat membangun komponen dengan cucu jauh di mana Anda tidak ingin banyak / komponen terikat ke penyimpanan, namun ingin komponen root bertindak sebagai penyimpan / sumber kebenaran, ini bekerja dengan cukup baik. Ini mirip dengan filosofi data down actions up dari Ember. Kelemahannya adalah jika Anda ingin mendengarkan acara itu pada setiap orang tua di antaranya, maka ini tidak akan berhasil. Tapi kemudian Anda bisa menggunakan $ propogateEmit seperti pada jawaban di atas oleh @kubaklam.

Edit: vm awal harus disetel ke komponen, dan bukan induk komponen. Yaitu let vm = thisdan tidaklet vm = this.$parent

Nick
sumber
0

Saya benar-benar memahami cara ini ditangani dengan membuat kelas yang terikat ke jendela dan menyederhanakan penyiapan siaran / dengar untuk bekerja di mana pun Anda berada di aplikasi Vue.

window.Event = new class {

    constructor() {
        this.vue = new Vue();
    }

    fire(event, data = null) {
        this.vue.$emit(event, data);
    }

    listen() {
        this.vue.$on(event, callback);  
    }

}

Sekarang Anda bisa menyalakan / menyiarkan / apapun dari mana saja dengan menelepon:

Event.fire('do-the-thing');

... dan Anda dapat mendengarkan orang tua, kakek, apa pun yang Anda inginkan dengan menelepon:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
fylzero.dll
sumber
1
Saya sangat menyarankan untuk tidak melampirkan properti acak ke objek jendela, karena sangat mudah untuk menimpa properti yang ada atau konflik dengan pustaka pihak ketiga yang ada. Sebaliknya, siapa pun yang menggunakan Vue untuk memecahkan masalah ini sebaiknya menggunakan jawaban @roli roli
HMilbradt
1
Saya tidak yakin saya sepenuhnya memahami atau setuju dengan masalah ini. Mengikat ke prototipe adalah pendekatan yang bagus tetapi mengikat ke jendela sama, jika tidak lebih, umum dan mungkin cara yang lebih standar untuk menangani ini. Anda menamai properti tersebut, sehingga mudah untuk menghindari konflik penamaan. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Ini juga merupakan solusi yang diusulkan yang digunakan Jeff Way di Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero