VueJs 2.0 senden ein Ereignis vom Enkelkind an seine Großelternkomponente

89

Es scheint, dass Vue.js 2.0 keine Ereignisse von einem Enkelkind an seine Großelternkomponente ausgibt.

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

Diese JsFiddle löst das Problem https://jsfiddle.net/y5dvkqbd/4/ , indem jedoch zwei Ereignisse ausgelöst werden:

  • Eine vom Enkelkind bis zur mittleren Komponente
  • Dann wieder von der mittleren Komponente zum Großelternteil emittieren

Das Hinzufügen dieses mittleren Ereignisses scheint sich zu wiederholen und ist nicht erforderlich. Gibt es eine Möglichkeit, direkt an Großeltern zu senden, die mir nicht bekannt sind?

BassMHL
quelle

Antworten:

66

In Vue 2.4 wurde eine Möglichkeit eingeführt, Ereignisse mithilfe der Hierarchie einfach weiterzugeben vm.$listeners

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

Enthält v-onEreignis-Listener für übergeordnete Bereiche (ohne .nativeModifikatoren). Dies kann über v-on="$listeners"- nützlich beim Erstellen transparenter Wrapper-Komponenten an eine innere Komponente weitergegeben werden.

Siehe den folgenden Ausschnitt v-on="$listeners"in der grand-childKomponente in der childVorlage:

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
quelle
Ich fand, dass dies der sauberste Weg ist, ohne vuex / bus / root einzuführen. Dadurch werden alle Emissionsfunktionen nach oben übergeben. Sie können weiterhin auf Kinderemissionen zugreifen, indem Sie @ someEmit = "someEmitHandler" vor v-on = "$ listeners" haben
Mathias Haugsbø
39

Die Vue-Community bevorzugt im Allgemeinen die Verwendung von Vuex, um diese Art von Problem zu lösen. Der Vuex-Status wird geändert, und die DOM-Darstellung ergibt sich daraus. In vielen Fällen sind keine Ereignisse erforderlich.

Wenn dies nicht der Fall ist , ist eine erneute Ausgabe wahrscheinlich die nächstbeste Wahl, und schließlich können Sie einen Ereignisbus verwenden, wie in der anderen hoch bewerteten Antwort auf diese Frage beschrieben.

Die Antwort unten ist meine ursprüngliche Antwort auf diese Frage und kein Ansatz, den ich jetzt verfolgen würde, da ich mehr Erfahrung mit Vue habe.


Dies ist ein Fall, in dem ich mit Vues Designentscheidung nicht einverstanden sein und auf DOM zurückgreifen könnte.

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

und in parent,

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

Andernfalls müssen Sie erneut senden oder einen Bus verwenden.

Hinweis: Ich habe der doEvent-Methode Code hinzugefügt, um mit dem IE umzugehen. Dieser Code könnte auf wiederverwendbare Weise extrahiert werden.

Bert
quelle
Dies verhält sich für IE anders?
Ich
@BassemLhm Vue ist gut mit IE. Das Problem mit dem IE ist nicht Vue, sondern eine DOM-Lösung, und Sie können kein neues Event () im IE ausführen. Sie müssen document.createEvent (). Ich kann die IE-Unterstützung bei Bedarf hinzufügen.
Bert
Es macht keinen Sinn, vuex nur für einen einfachen Fall zu installieren.
Adam Orlov
@AdamOrlov Ich stimme dir zu.
Bert
Eine einfachere Lösung: stackoverflow.com/a/55650245/841591
Digout
28

NEUE ANTWORT (Update November 2018)

Ich entdeckte, dass wir dies tatsächlich tun können, indem wir die $parentEigenschaft in der Enkelkindkomponente nutzen:

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

Viel sauberer und einfacher.

BassMHL
quelle
16
Beachten Sie, dass dies nur funktioniert, wenn die Beziehung Kind -> Großeltern ist. Dies funktioniert nicht , wenn das Kind beliebig tief verschachtelt werden kann.
Qtax
Siehe meine Antwort stackoverflow.com/a/55650245/841591 als Antwort auf den Kommentar von @Qtax
digout
7
Sie möchten nicht, dass solche Dinge in Ihrem großen Projekt passieren. Sie legen das 'Kind' in eine transitionoder eine andere Wrapper-Komponente und es wird zerbrechen und Sie haben ein großes Fragezeichen im Kopf.
Adam Orlov
@AdamOrlov Ich stimme zu, das ist eine schlechte Praxis. Bitte behandeln Sie solche Ereignisse in einem Vuex-Store.
Fabian von Ellerts
Dies ist einfacher: stackoverflow.com/a/55650245/841591
Digout
26

Ja, Sie sind richtig, Ereignisse gehen nur vom Kind zum Elternteil. Sie gehen nicht weiter, zB vom Kind zum Großelternteil.

Die Vue-Dokumentation (kurz) behandelt diese Situation im Abschnitt Nicht-Eltern-Kind-Kommunikation .

Die allgemeine Idee ist, dass Sie in der Großelternkomponente eine leere VueKomponente erstellen , die vom Großelternteil über Requisiten an die Kinder und Enkelkinder weitergegeben wird. Der Großelternteil hört dann auf Ereignisse und Enkelkinder senden Ereignisse auf diesem "Ereignisbus" aus.

Einige Anwendungen verwenden einen globalen Ereignisbus anstelle eines Ereignisbusses pro Komponente. Die Verwendung eines globalen Ereignisbusses bedeutet, dass Sie eindeutige Ereignisnamen oder Namespaces benötigen, damit Ereignisse nicht zwischen verschiedenen Komponenten zusammenstoßen.

Hier ist ein Beispiel für die Implementierung eines einfachen globalen Ereignisbusses .

Sly_cardinal
quelle
16

Eine andere Lösung wird in ein / Emit sein root node:

Verwendet vm.$root.$emitim Enkelkind , dann vm.$root.$onbeim Vorfahren (oder wo immer Sie möchten).

Aktualisiert : Manchmal möchten Sie den Listener in bestimmten Situationen deaktivieren. Verwenden Sie vm. $ Off (zum Beispiel: 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
quelle
Die eleganteste Lösung für mich.
Ovilia
Schöne Lösung! Dies funktionierte gut für mich in einer hauptsächlich serverseitigen Anwendung (Laravel) mit einigen Vue-Komponenten (ich wollte keine Vuex-Ebene einführen). Mein spezielles Problem war eine benutzerdefinierte Schaltflächenkomponente, die ein Buefy-Modal mit einer Formularkomponente auslöste. Sobald das Formular gesendet wurde, wollte ich die Schaltfläche deaktivieren, aber benutzerdefinierte Ereignisse, die vom Formular ausgegeben wurden, erreichten die Schaltfläche nicht, da das Modal das direkte übergeordnete Element des Formulars war.
Richard
Dies zerstört den Hörer beim Zerstören nicht. Ist das in Ordnung? Wäre toll, wenn Sie diese Logik hinzufügen könnten, also weiß ich auch, wie das geht.
Mesqueeb
14

Wenn Sie flexibel sein und einfach ein Ereignis an alle Eltern und deren Eltern rekursiv bis zur Wurzel senden möchten, können Sie Folgendes tun:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
ausgraben
quelle
1
Das ist die richtige Antwort. Ich habe eine Dienstprogrammfunktion erstellt, um dies an mehreren Stellen in meinem Code zu tun: propagateEvent (Komponente, Ereignisname, Wert) {while (Komponente) {Komponente. $ Emit (Ereignisname, Wert); Komponente = Komponente. $ Eltern; }}
ccleve
2
Dies ist eine elegante Antwort, sehr einfach zu bedienen und leicht zu lesen. Und es wird nicht durch Hinzufügen zusätzlicher Wickelebenen beschädigt.
Dean
4

Dies ist der einzige Fall , wenn ich Eventbus !! Für die Weitergabe von Daten von einem tief verschachtelten Kind an eine nicht direkt übergeordnete Kommunikation.

Erstens : Erstellen Sie eine js-Datei (ich nenne sie eventbus.js) mit folgendem Inhalt:

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

Zweitens : In Ihrer untergeordneten Komponente wird ein Ereignis ausgegeben:

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

Drittens : Hören Sie im übergeordneten Element auf dieses Ereignis:

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

Hinweis: Wenn Sie dieses Ereignis nicht mehr möchten, heben Sie bitte die Registrierung auf:

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

INFO: Die unten stehende persönliche Meinung muss nicht gelesen werden

Ich verwende Vuex nicht gerne für die Kommunikation zwischen Enkelkindern und Großeltern (oder einer ähnlichen Kommunikationsebene).

In vue.js können Sie für die Übergabe von Daten vom Großelternteil an das Enkelkind bereitstellen / injizieren verwenden . Aber für das Gegenteil gibt es nichts Ähnliches. (Enkel zu Großelternteil) Also benutze ich den Eventbus, wenn ich diese Art von Kommunikation machen muss.

roli roli
quelle
2

Ich habe ein kurzes Mixin basierend auf @digout Antwort gemacht. Sie möchten es vor der Initialisierung Ihrer Vue-Instanz (neues Vue ...) einfügen, um es global im Projekt zu verwenden. Sie können es ähnlich wie ein normales Ereignis verwenden.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
kubaklamca
quelle
Diese Lösung habe ich für meine Implementierung verwendet, aber ich habe einen zusätzlichen Parameter hinzugefügt targetRef, der die Weitergabe der Komponente, auf die Sie abzielen, stoppt. Die whileBedingung würde dann beinhalten && vm.$refs[targetRef]- Sie müssten dieses refAttribut auch in die Zielkomponente aufnehmen. In meinem Anwendungsfall musste ich nicht bis zur Wurzel tunneln, um ein paar Ereignisse vor dem Auslösen und vielleicht ein paar wertvolle Nanosekunden zu sparen time
mcgraw
2

VueJS 2-Komponenten haben eine $parentEigenschaft, die ihre übergeordnete Komponente enthält.

Diese übergeordnete Komponente enthält auch eine eigene $parentEigenschaft.

Wenn Sie dann auf die Komponente "Großeltern" zugreifen, müssen Sie auf die Komponente "Eltern des Elternteils" zugreifen:

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

Wie auch immer, das ist ein bisschen schwierig , und ich empfehle, einen globalen Statusmanager wie Vuex oder ähnliche Tools zu verwenden, wie andere Antwortende gesagt haben.

Rogervila
quelle
1

Ich riffle die Antworten von @kubaklam und @ digout ab und vermeide dies, um zu vermeiden, dass auf jede Elternkomponente zwischen dem Enkelkind und dem (möglicherweise entfernten) Großelternteil gesendet wird:

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

Wenn Sie eine Komponente mit entfernten Enkelkindern erstellen, bei der nicht viele / keine Komponenten an das Geschäft gebunden werden sollen, die Stammkomponente jedoch als Geschäft / Quelle der Wahrheit fungieren soll, funktioniert dies recht gut. Dies ähnelt der Philosophie von Ember. Der Nachteil ist, dass dies nicht funktioniert, wenn Sie jedes Ereignis dazwischen auf jedes Elternteil abhören möchten. Aber dann können Sie $ propogateEmit wie in der obigen Antwort von @kubaklam verwenden.

Bearbeiten: Die anfängliche VM sollte auf die Komponente und nicht auf die übergeordnete Komponente der Komponente festgelegt werden. Dh let vm = thisund nichtlet vm = this.$parent

Nick
quelle
0

Ich finde es wirklich toll, wie dies gehandhabt wird, indem ich eine Klasse erstelle, die an das Fenster gebunden ist, und das Broadcast / Listen-Setup so vereinfache, dass es überall in der Vue-App funktioniert.

window.Event = new class {

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

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

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

}

Jetzt können Sie einfach von überall aus feuern / senden / was auch immer, indem Sie anrufen:

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

... und Sie können einem Elternteil, einem Großelternteil, zuhören, was immer Sie wollen, indem Sie anrufen:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
Fylzero
quelle
1
Ich würde dringend empfehlen, keine zufälligen Eigenschaften an das Fensterobjekt anzuhängen, da es sehr einfach ist, vorhandene Eigenschaften zu überschreiben oder Konflikte mit vorhandenen Bibliotheken von Drittanbietern zu verursachen. Stattdessen sollte jeder, der Vue verwendet, um dieses Problem zu lösen, stattdessen die Antwort von @roli roli verwenden
HMilbradt
1
Ich bin mir nicht sicher, ob ich dieses Anliegen vollständig verstehe oder damit einverstanden bin. Das Binden an den Prototyp ist ein guter Ansatz, aber das Binden an das Fenster ist genauso, wenn nicht sogar noch häufiger, und wahrscheinlich eine Standardmethode, um damit umzugehen. Sie benennen die Eigenschaft, sodass es einfach ist, Namenskonflikte zu vermeiden. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Dies ist auch die vorgeschlagene Lösung, die Jeff Way für Laracasts verwendet. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero