VueJs 2.0 emette l'evento dal nipote al suo componente nonno


98

Sembra che Vue.js 2.0 non trasmetta eventi da un nipote al suo componente nonno.

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

Questo JsFiddle risolve il problema https://jsfiddle.net/y5dvkqbd/4/ , ma emettendo due eventi:

  • Uno da nipote a componente centrale
  • Quindi emettere di nuovo dal componente centrale al nonno

Aggiungere questo evento intermedio sembra ripetitivo e inutile. C'è un modo per emettere direttamente al nonno di cui non sono a conoscenza?

Risposte:


73

Vue 2.4 ha introdotto un modo per passare facilmente gli eventi nella gerarchia utilizzando vm.$listeners

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

Contiene v-onlistener di eventi con ambito padre (senza .nativemodificatori). Questo può essere trasmesso a un componente interno tramite v-on="$listeners"- utile quando si creano componenti wrapper trasparenti.

Vedi lo snippet di seguito utilizzando v-on="$listeners"nel grand-childcomponente nel childmodello:

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>


42

La comunità di Vue generalmente preferisce utilizzare Vuex per risolvere questo tipo di problema. Vengono apportate modifiche allo stato di Vuex e la rappresentazione DOM fluisce da quello, eliminando la necessità di eventi in molti casi.

A parte ciò, riemettere sarebbe probabilmente la scelta migliore successiva, e infine , potresti scegliere di utilizzare un bus di eventi come spiegato in dettaglio nell'altra risposta altamente votata a questa domanda.

La risposta di seguito è la mia risposta originale a questa domanda e non è un approccio che adotterei ora, avendo più esperienza con Vue.


Questo è un caso in cui potrei non essere d'accordo con la scelta del design di Vue e ricorrere a DOM.

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

e in parent,

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

Altrimenti sì, devi riemettere o utilizzare un bus.

Nota: ho aggiunto del codice nel metodo doEvent per gestire IE; quel codice potrebbe essere estratto in modo riutilizzabile.


Questo si comporta diversamente per IE? non sapeva che ci fossero discrepanze del browser con vue ...
BassMHL

@BassemLhm Vue sta bene con IE. Il problema con IE non è Vue, è una soluzione DOM e non puoi fare new Event () in IE. Devi document.createEvent (). Posso aggiungere il supporto di IE se necessario.
Bert

Non ha senso installare vuex solo per un semplice caso.
Adam Orlov

@AdamOrlov sono d'accordo con te.
Bert

Una soluzione più semplice: stackoverflow.com/a/55650245/841591
DIGOUT

34

NUOVA RISPOSTA (aggiornamento novembre 2018)

Ho scoperto che potremmo effettivamente farlo sfruttando la $parentproprietà nel componente nipote:

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

Molto più pulito e semplice.


21
Nota che questo funziona solo se la relazione è bambino -> nonno. Questo non funziona se il figlio può essere annidato a livelli arbitrari in profondità.
Qtax

Vedi la mia risposta stackoverflow.com/a/55650245/841591 in risposta al commento di @Qtax
digout

9
Non vuoi che succedano cose del genere nel tuo grande progetto. Metti il ​​"bambino" in uno transitiono qualsiasi altro componente dell'involucro e si romperà, lasciandoti con un grande punto interrogativo nella tua testa.
Adam Orlov

1
@AdamOrlov Sono d'accordo, questa è una cattiva pratica. Gestisci eventi come questo utilizzando un negozio Vuex.
Fabian von Ellerts il

Questo è più semplice: stackoverflow.com/a/55650245/841591
DIGOUT

26

Sì, hai ragione, gli eventi vanno solo da bambino a genitore. Non vanno oltre, ad esempio da bambino a nonno.

La documentazione di Vue (brevemente) affronta questa situazione nella sezione Comunicazione non genitore-figlio .

L'idea generale è che nel componente nonni si crei un Vuecomponente vuoto che viene passato dai nonni ai figli e ai nipoti tramite oggetti di scena. Il nonno quindi ascolta gli eventi ei nipoti emettono eventi su quel "bus degli eventi".

Alcune applicazioni utilizzano un bus di eventi globale invece di un bus di eventi per componente. L'utilizzo di un bus di eventi globale significa che sarà necessario disporre di nomi di eventi o spazi dei nomi univoci in modo che gli eventi non entrino in conflitto tra i diversi componenti.

Ecco un esempio di come implementare un semplice bus di eventi globali .


18

Un'altra soluzione sarà on / emit al nodo radice :

Utilizza vm.$root.$emitin grand-child , quindi utilizzavm.$root.$on presso l'antenato (o ovunque desideri).

Aggiornato : a volte vorresti disabilitare il listener in alcune situazioni specifiche, usa vm. $ Off (ad esempio: 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>


17

Se vuoi essere flessibile e trasmettere semplicemente un evento a tutti i genitori e ai loro genitori in modo ricorsivo fino alla radice, potresti fare qualcosa del tipo:

let vm = this.$parent

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

4

Questo è l'unico caso in cui utilizzo il bus degli eventi !! Per il passaggio di dati da un figlio nidificato in profondità, a un padre non direttamente, comunicazione.

Primo : crea un file js (lo chiamo eventbus.js) con questo contenuto:

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

Secondo : nel tuo componente figlio emetti un evento:

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

Terzo : nel genitore ascolta quell'evento:

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

Nota: se non desideri più quell'evento, annulla la registrazione:

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

INFO: Non c'è bisogno di leggere la seguente opinione personale

Non mi piace usare vuex per la comunicazione da nipote a nonno (o un livello di comunicazione simile).

In vue.js per il passaggio dei dati da nonno a nipote è possibile utilizzare fornire / iniettare . Ma non c'è qualcosa di simile per la cosa opposta. (da nipote a nonno) Quindi uso il bus degli eventi ogni volta che devo fare quel tipo di comunicazione.


2

Ho fatto un breve mixin basato sulla risposta @digout. Vuoi metterlo, prima dell'inizializzazione della tua istanza Vue (nuovo Vue ...) per usarlo globalmente nel progetto. Puoi usarlo in modo simile all'evento normale.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})

questa soluzione è quella che ho usato per la mia implementazione ma ho aggiunto un ulteriore parametro targetRefche arresta la propagazione sul componente che stai mirando. La whilecondizione quindi includerebbe && vm.$refs[targetRef]: dovresti anche includere refquell'attributo sul componente mirato Nel mio caso d'uso non ho avuto bisogno di eseguire il tunneling fino al root, salvando alcuni eventi dall'attivazione e forse un paio di preziosi nanosecondi di tempo
mcgraw

2

I componenti VueJS 2 hanno una $parentproprietà che contiene il loro componente genitore.

Quel componente genitore include anche la sua $parentproprietà.

Quindi, accedendo al componente "nonno" si tratta di accedere al componente "genitore genitore":

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

Comunque, questo è un po ' po complicato e consiglio di utilizzare un gestore di stato globale come Vuex o strumenti simili, come hanno detto altri soccorritori.


1

Riffingendo le risposte di @kubaklam e @ digout, questo è ciò che uso per evitare di emettere su ogni componente genitore tra il nipote e il nonno (possibilmente distante):

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

Quando si costruisce un componente con nipoti lontani in cui non si desidera che molti / nessun componente siano legati al negozio, ma si desidera che il componente radice agisca come un negozio / fonte di verità, questo funziona abbastanza bene. Questo è simile alla filosofia data down actions up di Ember. Lo svantaggio è che se vuoi ascoltare quell'evento su ogni genitore nel mezzo, allora non funzionerà. Ma poi puoi usare $ propogateEmit come nella risposta sopra di @kubaklam.

Modifica: la vm iniziale dovrebbe essere impostata sul componente e non sul genitore del componente. Cioè let vm = thise nonlet vm = this.$parent


0

Apprezzo davvero il modo in cui viene gestito creando una classe che è legata alla finestra e semplificando la configurazione di trasmissione / ascolto per funzionare ovunque ti trovi nell'app 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);  
    }

}

Ora puoi semplicemente sparare / trasmettere / qualunque cosa da qualsiasi luogo chiamando:

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

... e puoi ascoltare un genitore, un nonno, quello che vuoi chiamando:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});

1
Consiglio vivamente di non allegare proprietà casuali all'oggetto finestra, poiché è molto facile sovrascrivere le proprietà esistenti o entrare in conflitto con le librerie di terze parti esistenti. Invece, chiunque utilizzi Vue per risolvere questo problema dovrebbe invece usare la risposta di @roli roli
HMilbradt,

1
Non sono sicuro di aver compreso completamente o di essere d'accordo con questa preoccupazione. Il collegamento al prototipo è un buon approccio, ma il collegamento alla finestra è altrettanto comune e probabilmente un modo più standard di gestirlo, se non di più. Assegnate un nome alla proprietà, quindi è semplice evitare conflitti di denominazione. medium.com/@amitavroy7/... stackoverflow.com/questions/15008464/... Questa è anche la soluzione proposta Jeff Way utilizza il Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.