VueJs 2.0 emite un evento de su nieto a su componente principal

87

Parece que Vue.js 2.0 no emite eventos de un nieto a su componente principal.

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

Este JsFiddle resuelve el problema https://jsfiddle.net/y5dvkqbd/4/ , pero al generar dos eventos:

  • Uno del nieto al componente medio
  • Luego emitiendo nuevamente desde el componente medio al abuelo

Agregar este evento intermedio parece repetitivo e innecesario. ¿Hay alguna forma de emitir directamente al abuelo que no conozco?

BajoMHL
fuente

Respuestas:

64

Vue 2.4 introdujo una forma de pasar fácilmente eventos en la jerarquía usando vm.$listeners

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

Contiene v-ondetectores de eventos de ámbito primario (sin .nativemodificadores). Esto se puede transmitir a un componente interno a través de v-on="$listeners": útil al crear componentes de envoltura transparentes.

Vea el fragmento a continuación que se usa v-on="$listeners"en el grand-childcomponente de la childplantilla:

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
fuente
Encontré que esta es la forma más limpia sin introducir vuex / bus / root. Esto pasará todas las funciones de emisión hacia arriba. Aún puede aprovechar las emisiones de los niños si tiene @ someEmit = "someEmitHandler" antes de v-on = "$ listeners"
Mathias Haugsbø
39

La comunidad de Vue generalmente favorece el uso de Vuex para resolver este tipo de problema. Se realizan cambios en el estado de Vuex y la representación DOM simplemente fluye de eso, eliminando la necesidad de eventos en muchos casos.

Salvo eso, la reemisión sería probablemente la siguiente mejor opción y, por último , podría optar por utilizar un bus de eventos como se detalla en la otra respuesta altamente votada a esta pregunta.

La respuesta a continuación es mi respuesta original a esta pregunta y no es un enfoque que tomaría ahora, teniendo más experiencia con Vue.


Este es un caso en el que podría no estar de acuerdo con la elección de diseño de Vue y recurrir a DOM.

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

y parent,

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

De lo contrario, sí, tienes que volver a emitir, o utilizar un autobús.

Nota: agregué código en el método doEvent para manejar IE; ese código podría extraerse de forma reutilizable.

Bert
fuente
¿Esto se comporta de manera diferente para IE? no sabía que había discrepancias en el navegador con vue ...
BassMHL
@BassemLhm Vue está bien con IE. El problema con IE no es Vue, es una solución DOM y no puede hacer un nuevo evento () en IE. Tienes que document.createEvent (). Puedo agregar el soporte de IE si es necesario.
Bert
No tiene sentido instalar vuex solo para un caso simple.
Adam Orlov
@AdamOrlov Estoy de acuerdo contigo.
Bert
Una solución más simple: stackoverflow.com/a/55650245/841591
digout
28

NUEVA RESPUESTA (actualización de noviembre de 2018)

Descubrí que en realidad podríamos hacer esto aprovechando la $parentpropiedad en el componente de nietos:

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

Mucho más limpio y sencillo.

BajoMHL
fuente
15
Tenga en cuenta que esto solo funciona si la relación es hijo -> abuelo. Esto no funciona si el niño se puede anidar en niveles arbitrarios de profundidad.
Qtax
Vea mi respuesta stackoverflow.com/a/55650245/841591 en respuesta al comentario de @Qtax
digout
7
No quieres que sucedan ese tipo de cosas en tu gran proyecto. Pones el 'niño' en uno transitiono cualquier otro componente de envoltura y se romperá, dejándote con un gran signo de interrogación en tu cabeza.
Adam Orlov
@AdamOrlov Estoy de acuerdo, esta es una mala práctica. Maneja eventos como este usando una tienda Vuex.
Fabian von Ellerts
Esto es más simple: stackoverflow.com/a/55650245/841591
digout
26

Sí, tienes razón, los eventos solo van de un niño a otro. No van más allá, por ejemplo, de niño a abuelo.

La documentación de Vue (brevemente) aborda esta situación en la Comunicación sin padres e hijos sección .

La idea general es que en el componente abuelo crea un vacío Vue componente que se pasa de los abuelos a los hijos y nietos a través de accesorios. El abuelo luego escucha los eventos y los nietos emiten eventos en ese "bus de eventos".

Algunas aplicaciones utilizan un bus de eventos global en lugar de un bus de eventos por componente. El uso de un bus de eventos global significa que deberá tener nombres de eventos únicos o espacios de nombres para que los eventos no entren en conflicto entre diferentes componentes.

A continuación, se muestra un ejemplo de cómo implementar un bus de eventos global simple .

Sly_cardinal
fuente
16

Otra solución estará encendida / emitida en el nodo raíz :

Se usa vm.$root.$emiten nieto , luego se usa vm.$root.$onen el antepasado (o en cualquier lugar que desee).

Actualizado : a veces le gustaría deshabilitar el oyente en algunas situaciones específicas, use vm. $ Off (por ejemplo: vm.$root.off('event-name')dentro del ciclo de vida 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>

Esfinge
fuente
La solución más elegante para mí.
Ovilia
¡Buena solución! Esto funcionó bien para mí en una aplicación principalmente del lado del servidor (Laravel) con algunos componentes de Vue (no quería introducir una capa de Vuex). Mi problema específico fue un componente de botón personalizado que desencadenó un modal Buefy con un componente de formulario. Una vez que se envió el formulario, quise deshabilitar el botón, pero los eventos personalizados emitidos desde el formulario no llegarían al botón ya que el Modal era el padre directo del formulario.
Richard
Esto no destruye al oyente al destruir. ¿Eso esta bien? Sería genial si pudieras agregar esa lógica, así que también sé cómo hacerlo.
mesqueeb
14

Si desea ser flexible y simplemente transmitir un evento a todos los padres y a sus padres de forma recursiva hasta la raíz, puede hacer algo como:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
cavar
fuente
1
Esta es la respuesta correcta. Creé una función de utilidad para hacerlo en varios lugares de mi código: propagateEvent (component, eventName, value) {while (component) {component. $ Emit (eventName, value); componente = componente. $ padre; }}
ccleve
2
Esta es una respuesta elegante, muy simple de usar y fácil de leer. Y no se rompe al agregar niveles adicionales de envoltura.
Dean
4

Este es el único caso cuando uso bus de eventos ! Para pasar datos de la comunicación secundaria anidada profunda, a la comunicación no directamente parental.

Primero : cree un archivo js (lo nombre eventbus.js) con este contenido:

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

Segundo : en su componente hijo, emita un evento:

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

Tercero : En el padre escuche ese evento:

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

Nota: Si ya no desea ese evento, anule el registro:

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

INFO: No es necesario leer la opinión personal a continuación

No me gusta usar vuex para la comunicación entre nietos y abuelos (o un nivel de comunicación similar).

En vue.js para pasar datos de abuelos a nietos, puede usar provide / inject . Pero no hay nada parecido para lo contrario. (nieto a abuelo) Así que uso el bus de eventos siempre que tengo que hacer ese tipo de comunicación.

roli roli
fuente
2

Hice un breve mixin basado en la respuesta de @digout. Quiere ponerlo, antes de la inicialización de su instancia de Vue (nuevo Vue ...) para usarlo globalmente en el proyecto. Puede usarlo de manera similar a un evento normal.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
kubaklamca
fuente
esta solución es la que utilicé para mi implementación, pero agregué un parámetro adicional targetRefque detiene la propagación en el componente al que se dirige. La whilecondición incluiría entonces && vm.$refs[targetRef]: también necesitaría incluir ese refatributo en el componente de destino.En mi caso de uso, no necesitaba hacer un túnel hasta la raíz, lo que evitaba que se dispararan algunos eventos y tal vez un par de preciosos nanosegundos de time
mcgraw
2

Los componentes de VueJS 2 tienen una $parentpropiedad que contiene su componente principal.

Ese componente principal también incluye su propia $parentpropiedad.

Luego, acceder al componente "abuelo" es una cuestión de acceder al componente "padre del padre":

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

De todos modos, esto es un poco complicado , y recomiendo usar un administrador de estado global como Vuex o herramientas similares, como han dicho otros respondedores.

rogervila
fuente
1

Haciendo caso omiso de las respuestas de @kubaklam y @ digout, esto es lo que uso para evitar emitir en cada componente principal entre el nieto y el abuelo (posiblemente 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)
    }
  }
}

Cuando se crea un componente con nietos lejanos en el que no desea que muchos / ningún componente esté vinculado a la tienda, pero desea que el componente raíz actúe como un almacén / fuente de verdad, esto funciona bastante bien. Esto es similar a la filosofía de acciones de descarga de datos de Ember. La desventaja es que si desea escuchar ese evento en todos los padres intermedios, esto no funcionará. Pero luego puede usar $ propogateEmit como en la respuesta anterior de @kubaklam.

Editar: la máquina virtual inicial debe establecerse en el componente y no en el padre del componente. Es decir let vm = thisy nolet vm = this.$parent

Mella
fuente
0

Realmente me encanta la forma en que esto se maneja creando una clase que está vinculada a la ventana y simplificando la configuración de transmisión / escucha para que funcione donde sea que se encuentre en la aplicación 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);  
    }

}

Ahora puede disparar / transmitir / lo que sea desde cualquier lugar llamando a:

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

... y puedes escuchar a un padre, abuelo, lo que quieras llamando a:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
fylzero
fuente
1
Recomendaría encarecidamente no adjuntar propiedades aleatorias al objeto de ventana, ya que es muy fácil sobrescribir propiedades existentes o entrar en conflicto con bibliotecas de terceros existentes. En cambio, cualquiera que use Vue para resolver este problema debería usar la respuesta de @roli roli
HMilbradt
1
No estoy seguro de entender completamente o de estar de acuerdo con esta preocupación. La vinculación al prototipo es un buen enfoque, pero la vinculación a la ventana es tan común, si no más, común y probablemente una forma más estándar de manejar esto. Usted nombra la propiedad, por lo que es sencillo evitar conflictos de nombres. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Esta es también la solución propuesta que Jeff Way usa en Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodios/13
fylzero