VueJs 2.0 emituje zdarzenie z wnuczka do komponentu pradziadka

89

Wygląda na to, że Vue.js 2.0 nie emituje zdarzeń od wnuczka do komponentu pradziadka.

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

To JsFiddle rozwiązuje problem https://jsfiddle.net/y5dvkqbd/4/ , ale przez wyzwolenie dwóch zdarzeń:

  • Jedna od wnuczka do środkowej części
  • Następnie ponownie emituje ze środkowego komponentu do dziadka

Dodanie tego środkowego zdarzenia wydaje się powtarzalne i niepotrzebne. Czy istnieje sposób emisji bezpośrednio do dziadka, którego nie jestem świadomy?

BassMHL
źródło

Odpowiedzi:

66

Vue 2.4 wprowadził sposób na łatwe przekazywanie zdarzeń w górę hierarchii przy użyciu vm.$listeners

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

Zawiera v-ondetektory zdarzeń o zasięgu nadrzędnym (bez .nativemodyfikatorów). Można to przekazać do komponentu wewnętrznego za pośrednictwem v-on="$listeners"- przydatne przy tworzeniu przezroczystych komponentów opakowania.

Zobacz poniższy fragment dotyczący używania v-on="$listeners"w grand-childkomponencie w childszablonie:

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
źródło
Okazało się, że jest to najczystszy sposób bez wprowadzania vuex / bus / root. Spowoduje to przejście wszystkich funkcji emitujących w górę. Nadal możesz korzystać z emisji dziecięcych, ustawiając @ someEmit = "someEmitHandler" przed v-on = "$ listeners"
Mathias Haugsbø
39

Społeczność Vue generalnie preferuje używanie Vuex do rozwiązywania tego rodzaju problemów. Zmiany są wprowadzane w stanie Vuex, a reprezentacja DOM po prostu wypływa z tego, co w wielu przypadkach eliminuje potrzebę zdarzeń.

Poza tym, ponowna emisja byłaby prawdopodobnie kolejnym najlepszym wyborem, a na koniec możesz zdecydować się na użycie magistrali zdarzeń, jak opisano szczegółowo w innej wysoko ocenionej odpowiedzi na to pytanie.

Poniższa odpowiedź jest moją oryginalną odpowiedzią na to pytanie i nie jest podejściem, które podjąłbym teraz, mając większe doświadczenie z Vue.


To przypadek, w którym mógłbym nie zgodzić się z wyborem projektu Vue i uciec się do DOM.

W 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);
        }
    }
}

a parent,

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

W przeciwnym razie tak, musisz ponownie wyemitować lub użyć autobusu.

Uwaga: dodałem kod w metodzie doEvent do obsługi IE; ten kod można wyodrębnić w sposób umożliwiający ponowne użycie.

Bert
źródło
To zachowuje się inaczej w IE? nie wiedziałem, że są rozbieżności przeglądarki z vue ...
BassMHL
@BassemLhm Vue jest w porządku z IE. Problem z IE to nie Vue, to jest rozwiązanie DOM i nie możesz zrobić nowego Event () w IE. Musisz document.createEvent (). W razie potrzeby mogę dodać obsługę IE.
Bert,
Nie ma sensu instalować vuex tylko w jednym prostym przypadku.
Adam Orlov
@AdamOrlov Zgadzam się z tobą.
Bert,
Prostsze rozwiązanie: stackoverflow.com/a/55650245/841591
digout
28

NOWA ODPOWIEDŹ (aktualizacja z listopada 2018 r.)

Odkryłem, że możemy to zrobić, wykorzystując $parentwłaściwość składnika grand child:

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

Dużo czystsze i prostsze.

BassMHL
źródło
16
Zauważ, że działa to tylko wtedy, gdy relacja to dziecko -> dziadek. To nie działa, jeśli dziecko może być zagnieżdżone na dowolnych poziomach głęboko.
Qtax
Zobacz moją odpowiedź stackoverflow.com/a/55650245/841591 w odpowiedzi na komentarz z @Qtax
digout Kwietnia
7
Nie chcesz, żeby takie rzeczy działy się w twoim dużym projekcie. Umieszczasz „dziecko” w jakimś transitioninnym elemencie opakowania, a ono się zepsuje, pozostawiając w głowie wielki znak zapytania.
Adam Orlov
@AdamOrlov Zgadzam się, to zła praktyka. Obsługuj takie zdarzenia w sklepie Vuex.
Fabian von Ellerts
To jest prostsze: stackoverflow.com/a/55650245/841591
digout
26

Tak, twoje prawidłowe wydarzenia przechodzą tylko od dziecka do rodzica. Nie idą dalej, np. Od dziecka do dziadka.

Dokumentacja Vue (pokrótce) omawia tę sytuację w sekcji Komunikacja między rodzicami a dzieckiem .

Ogólna idea polega na tym, że w komponencie dziadków tworzysz pusty Vuekomponent, który jest przekazywany od dziadków do dzieci i wnuków za pośrednictwem rekwizytów. Następnie dziadek nasłuchuje zdarzeń, a wnuki emitują zdarzenia na tej „szynie zdarzeń”.

Niektóre aplikacje używają globalnej magistrali zdarzeń zamiast magistrali zdarzeń dla poszczególnych komponentów. Korzystanie z globalnej magistrali zdarzeń oznacza, że ​​będziesz potrzebować unikalnych nazw zdarzeń lub przestrzeni nazw, aby zdarzenia nie kolidowały między różnymi komponentami.

Oto przykład implementacji prostej globalnej magistrali zdarzeń .

Sly_cardinal
źródło
16

Inne rozwiązanie będzie włączone / emitowane w węźle głównym :

Używa vm.$root.$emitw wnukach , a następnie używa vm.$root.$onu przodka (lub gdziekolwiek chcesz).

Zaktualizowano : czasami chciałbyś wyłączyć nasłuchiwanie w określonych sytuacjach, użyj vm. $ Off (na przykład: 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>

Sfinks
źródło
Najbardziej eleganckie rozwiązanie dla mnie.
Ovilia,
Niezłe rozwiązanie! Działało to dobrze dla mnie w aplikacji głównie po stronie serwera (Laravel) z kilkoma komponentami Vue (nie chciałem wprowadzać warstwy Vuex). Moim konkretnym problemem był niestandardowy składnik przycisku, który wyzwalał modal Buefy ze składnikiem formularza. Po przesłaniu formularza chciałem wyłączyć przycisk, ale zdarzenia niestandardowe emitowane z formularza nie docierały do ​​przycisku, ponieważ moduł Modal był bezpośrednim rodzicem formularza.
Richard
To nie niszczy słuchacza po zniszczeniu. Czy to w porządku? Byłoby wspaniale, gdybyś mógł dodać tę logikę, więc ja też wiem, jak to zrobić.
mesqueeb
14

Jeśli chcesz być elastyczny i po prostu nadawać wydarzenie wszystkim rodzicom i ich rodzicom rekurencyjnie aż do katalogu głównego, możesz zrobić coś takiego:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
wykopać
źródło
1
To jest właściwa odpowiedź. Utworzyłem funkcję narzędzia, aby zrobić to w wielu miejscach w moim kodzie: propagateEvent (komponent, nazwa_zdarzenia, wartość) {while (komponent) {komponent. $ Emit (nazwa_zdarzenia, wartość); komponent = komponent. $ rodzic; }}
ccleve
2
To elegancka odpowiedź, bardzo prosta w użyciu i łatwa do odczytania. I nie jest zepsuty przez dodanie dodatkowych poziomów zawijania.
Dean
4

To jedyny przypadek, kiedy używam magistrali zdarzeń !! Do przekazywania danych od głęboko zagnieżdżonego dziecka, a nie bezpośrednio do komunikacji z rodzicem.

Po pierwsze : Utwórz plik js (nazywam go eventbus.js) z następującą zawartością:

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

Po drugie : w komponencie podrzędnym emituj zdarzenie:

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

Po trzecie : u rodzica posłuchaj tego zdarzenia:

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

Uwaga: jeśli nie chcesz już tego wydarzenia, wyrejestruj je:

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

INFO: Nie musisz czytać poniższej osobistej opinii

Nie lubię używać vuex do komunikacji między wnukiem a dziadkiem (Lub podobny poziom komunikacji).

W vue.js do przekazywania danych z dziadka do wnuczka możesz użyć funkcji provider / inject . Ale nie ma czegoś podobnego w przypadku przeciwnej rzeczy. (od wnuczka do dziadka) Więc korzystam z magistrali zdarzeń, gdy mam taką komunikację.

roli roli
źródło
2

Zrobiłem krótki mix na podstawie odpowiedzi @digout. Chcesz go umieścić przed inicjalizacją instancji Vue (nowy Vue ...), aby używać go globalnie w projekcie. Możesz go używać podobnie do normalnego wydarzenia.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
kubaklamca
źródło
To rozwiązanie jest tym, czego użyłem do mojej implementacji, ale dodałem dodatkowy parametr, targetRefktóry zatrzymuje propagację na docelowym komponencie. whileStan zawierałby wówczas && vm.$refs[targetRef]- że trzeba także uwzględnić ten refatrybut elementu docelowego w moim przypadku użycia nie trzeba tunelu aż do korzenia, oszczędzając kilka zdarzeń z wypalania i może kilka cennych nanosekund czas
mcgraw
2

Komponenty VueJS 2 mają $parentwłaściwość, która zawiera ich komponent macierzysty.

Ten komponent nadrzędny zawiera również własną $parentwłaściwość.

Następnie, aby uzyskać dostęp do komponentu „dziadek”, wystarczy uzyskać dostęp do komponentu „rodzic rodzica”:

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

W każdym razie jest to trochę trudne i zalecam użycie globalnego menedżera stanu, takiego jak Vuex lub podobnych narzędzi, jak powiedzieli inni respondenci.

rogervila
źródło
1

Odrzucając odpowiedzi @kubaklam i @ digout, używam tego, aby uniknąć emisji na każdym komponencie nadrzędnym między wnukiem a (prawdopodobnie odległym) dziadkiem:

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

Podczas budowania komponentu z odległymi wnukami, w których nie chcesz, aby wiele / żadnych komponentów było powiązanych ze sklepem, ale chcesz, aby komponent główny działał jako sklep / źródło prawdy, działa to całkiem dobrze. Jest to podobne do działania w dół w dół filozofii Ember. Wadą jest to, że jeśli chcesz słuchać tego zdarzenia u każdego rodzica w międzyczasie, to nie zadziała. Ale wtedy możesz użyć $ propogateEmit, jak w powyższej odpowiedzi @kubaklam.

Edycja: początkowa maszyna wirtualna powinna być ustawiona na komponent, a nie na element nadrzędny komponentu. To znaczy let vm = thisi nielet vm = this.$parent

Nacięcie
źródło
0

Naprawdę szukam sposobu, w jaki jest to obsługiwane, tworząc klasę powiązaną z oknem i upraszczając konfigurację transmisji / słuchania, aby działała gdziekolwiek jesteś w aplikacji 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);  
    }

}

Teraz możesz po prostu odpalić / nadać / cokolwiek z dowolnego miejsca, dzwoniąc:

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

... i możesz słuchać u rodzica, dziadka, co tylko zechcesz dzwoniąc:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
fylzero
źródło
1
Gorąco polecam nie dołączając losowe właściwości obiektu window, ponieważ jest to bardzo proste, aby zastąpić istniejące właściwości lub konflikt z istniejącymi bibliotekami 3rd party. Zamiast tego każdy, kto używa Vue do rozwiązania tego problemu, powinien zamiast tego użyć odpowiedzi @roli roli
HMilbradt.
1
Nie jestem pewien, czy całkowicie rozumiem lub zgadzam się z tym problemem. Powiązanie z prototypem jest dobrym podejściem, ale wiązanie z oknem jest tak samo, jeśli nie bardziej powszechne, i prawdopodobnie bardziej standardowym sposobem obsługi tego. Nazwij właściwość, aby uniknąć konfliktów nazw. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Jest to również proponowane rozwiązanie, którego Jeff Way używa na Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero