VueJs 2.0 émet un événement du petit-enfant vers son grand parent

87

Il semble que Vue.js 2.0 n'émet pas d'événements d'un petit-enfant à son composant grand-parent.

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

Ce JsFiddle résout le problème https://jsfiddle.net/y5dvkqbd/4/ , mais en émettant deux événements:

  • Un du petit-enfant au composant intermédiaire
  • Puis émettant à nouveau du composant intermédiaire au grand parent

L'ajout de cet événement intermédiaire semble répétitif et inutile. Existe-t-il un moyen d'émettre directement aux grands-parents dont je ne suis pas au courant?

BassMHL
la source

Réponses:

64

Vue 2.4 a introduit un moyen de passer facilement des événements dans la hiérarchie en utilisant vm.$listeners

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

Contient des v-onécouteurs d'événement de portée parent (sans .nativemodificateurs). Cela peut être transmis à un composant interne via v-on="$listeners"- utile lors de la création de composants wrapper transparents.

Voir l'extrait ci-dessous en utilisant v-on="$listeners"dans le grand-childcomposant dans le childmodèle:

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
la source
J'ai trouvé que c'était le moyen le plus propre sans introduire vuex / bus / root. Cela fera passer toutes les fonctions d'émission vers le haut. Vous pouvez toujours exploiter les émissions des enfants en ayant @ someEmit = "someEmitHandler" avant v-on = "$ listeners"
Mathias Haugsbø
39

La communauté Vue privilégie généralement l'utilisation de Vuex pour résoudre ce type de problème. Des modifications sont apportées à l'état de Vuex et la représentation DOM en découle simplement, éliminant le besoin d'événements dans de nombreux cas.

À part cela, réémettre serait probablement le meilleur choix suivant, et enfin, vous pourriez choisir d'utiliser un bus événementiel comme détaillé dans l'autre réponse hautement votée à cette question.

La réponse ci-dessous est ma réponse originale à cette question et n'est pas une approche que je prendrais maintenant, ayant plus d'expérience avec Vue.


C'est un cas où je pourrais être en désaccord avec le choix de conception de Vue et recourir au DOM.

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

et dans parent,

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

Sinon, oui, vous devez réémettre ou utiliser un bus.

Remarque: j'ai ajouté du code dans la méthode doEvent pour gérer IE; ce code pourrait être extrait de manière réutilisable.

Bert
la source
Cela se comporte différemment pour IE? n'était pas au courant qu'il y avait des divergences de navigateur avec vue ...
BassMHL
@BassemLhm Vue fonctionne bien avec IE. Le problème avec IE n'est pas Vue, c'est une solution DOM et vous ne pouvez pas faire de new Event () dans IE. Vous devez document.createEvent (). Je peux ajouter le support IE si nécessaire.
Bert
Cela n'a pas de sens d'installer vuex juste pour un cas simple.
Adam Orlov
@AdamOrlov Je suis d'accord avec vous.
Bert
Une solution plus simple: stackoverflow.com/a/55650245/841591
digout
28

NOUVELLE RÉPONSE (mise à jour nov-2018)

J'ai découvert que nous pouvions le faire en tirant parti de la $parentpropriété du composant grand-enfant:

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

Beaucoup plus propre et plus simple.

BassMHL
la source
15
Notez que cela ne fonctionne que si la relation est enfant -> grand-parent. Cela ne fonctionne pas si l'enfant peut être imbriqué à des niveaux arbitraires profonds.
Qtax
Voir ma réponse stackoverflow.com/a/55650245/841591 en réponse au commentaire de @Qtax
digout
7
Vous ne voulez pas que ce genre de choses se produise dans votre grand projet. Vous mettez l '«enfant» dans un transitionou tout autre composant de wrapper et il se cassera, vous laissant avec un gros point d'interrogation dans votre tête.
Adam Orlov
@AdamOrlov Je suis d'accord, c'est une mauvaise pratique. Veuillez gérer des événements comme celui-ci en utilisant un magasin Vuex.
Fabian von Ellerts
C'est plus simple: stackoverflow.com/a/55650245/841591
digout
26

Oui, vous avez raison, les événements ne vont que d'un enfant à l'autre. Ils ne vont pas plus loin, par exemple d'enfant en grand-parent.

La documentation Vue aborde (brièvement) cette situation dans la section Communication non parent-enfant .

L'idée générale est que dans le composant grands-parents, vous créez un Vuecomposant vide qui est transmis des grands-parents aux enfants et petits-enfants via des accessoires. Le grand-parent écoute alors les événements et les petits-enfants émettent des événements sur ce «bus d'événements».

Certaines applications utilisent un bus d'événements global au lieu d'un bus d'événements par composant. L'utilisation d'un bus d'événements global signifie que vous devrez avoir des noms d'événements ou un espace de noms uniques pour que les événements n'entrent pas en conflit entre les différents composants.

Voici un exemple de mise en œuvre d'un bus d'événements global simple .

Sly_cardinal
la source
16

Une autre solution sera activée / émise au nœud racine :

S'utilise vm.$root.$emitchez petit-enfant , puis utilise vm.$root.$onchez l'ancêtre (ou partout où vous le souhaitez).

Mise à jour : parfois, vous souhaitez désactiver l'écouteur dans certaines situations spécifiques, utilisez vm. $ Off (par exemple: 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
la source
La solution la plus élégante pour moi.
Ovilia le
Belle solution! Cela a bien fonctionné pour moi dans une application principalement côté serveur (Laravel) avec quelques composants Vue (je ne voulais pas introduire de couche Vuex). Mon problème spécifique était un composant de bouton personnalisé qui a déclenché un modal Buefy avec un composant de formulaire. Une fois le formulaire soumis, je voulais désactiver le bouton, mais les événements personnalisés émis depuis le formulaire n'atteindraient pas le bouton puisque le Modal était le parent direct du formulaire.
Richard
Cela ne détruit pas l'auditeur lors de la destruction. Est-ce que ça va? Ce serait formidable si vous pouviez ajouter cette logique, donc je sais comment faire cela aussi.
mesqueeb
14

Si vous voulez être flexible et simplement diffuser un événement à tous les parents et à leurs parents de manière récursive jusqu'à la racine, vous pouvez faire quelque chose comme:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
déterrer
la source
1
C'est la bonne réponse. J'ai créé une fonction utilitaire pour le faire à plusieurs endroits dans mon code: propagateEvent (component, eventName, value) {while (component) {component. $ Emit (eventName, value); composant = composant. $ parent; }}
ccleve
2
C'est une réponse élégante, très simple à utiliser et facile à lire. Et ce n'est pas cassé en ajoutant des niveaux supplémentaires d'emballage.
Dean
4

C'est le seul cas où j'utilise le bus événementiel !! Pour transmettre des données d'un enfant imbriqué profond à une communication non directement parent.

Premièrement : créez un fichier js (je le nomme eventbus.js) avec ce contenu:

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

Deuxièmement : dans votre composant enfant, émettez un événement:

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

Troisièmement : Dans le parent écoutez cet événement:

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

Remarque: si vous ne voulez plus cet événement, veuillez le désinscrire:

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

INFO: Pas besoin de lire l'avis personnel ci-dessous

Je n'aime pas utiliser vuex pour la communication grand-enfant à grand-parent (ou niveau de communication similaire).

Dans vue.js pour transmettre des données de grand-parent à petit-enfant, vous pouvez utiliser provide / inject . Mais il n'y a pas quelque chose de similaire pour le contraire. (petit-enfant à grand-parent) J'utilise donc le bus événementiel chaque fois que je dois faire ce genre de communication.

roli roli
la source
2

J'ai fait un court mixin basé sur la réponse @digout. Vous voulez le mettre, avant l'initialisation de votre instance Vue (nouvelle Vue ...) pour l'utiliser globalement dans le projet. Vous pouvez l'utiliser de la même manière qu'un événement normal.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
kubaklamca
la source
cette solution est ce que j'ai utilisé pour mon implémentation mais j'ai ajouté un paramètre supplémentaire targetRefqui arrête la propagation sur le composant que vous ciblez. La whilecondition inclurait alors && vm.$refs[targetRef]- vous auriez également besoin d'inclure cet refattribut sur le composant ciblé.Dans mon cas d'utilisation, je n'avais pas besoin de creuser un tunnel jusqu'à la racine, évitant ainsi quelques événements de déclenchement et peut-être quelques précieuses nanosecondes de time
mcgraw
2

Les composants VueJS 2 ont une $parentpropriété qui contient leur composant parent.

Ce composant parent comprend également sa propre $parentpropriété.

Ensuite, pour accéder au composant "grand-parent", il s'agit d'accéder au composant "parent du parent":

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

Quoi qu'il en soit, c'est un peu délicat , et je recommande d'utiliser un gestionnaire d'état global comme Vuex ou des outils similaires, comme l'ont dit d'autres intervenants.

rogervila
la source
1

En écartant les réponses de @kubaklam et @ digout, voici ce que j'utilise pour éviter d'émettre sur chaque composant parent entre le petit-enfant et le grand-parent (éventuellement éloigné):

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

Lors de la création d'un composant avec des petits-enfants éloignés où vous ne voulez pas que beaucoup / aucun composant soit lié au magasin, mais que vous voulez que le composant racine agisse comme un magasin / source de vérité, cela fonctionne très bien. Ceci est similaire à la philosophie des actions vers le bas des données d'Ember. L'inconvénient est que si vous voulez écouter cet événement sur chaque parent entre les deux, cela ne fonctionnera pas. Mais alors vous pouvez utiliser $ propogateEmit comme dans la réponse ci-dessus par @kubaklam.

Edit: la vm initiale doit être définie sur le composant et non sur le parent du composant. Ie let vm = thiset paslet vm = this.$parent

pseudo
la source
0

Je creuse vraiment la façon dont cela est géré en créant une classe liée à la fenêtre et en simplifiant la configuration de diffusion / écoute pour qu'elle fonctionne où que vous soyez dans l'application 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);  
    }

}

Maintenant, vous pouvez simplement tirer / diffuser / n'importe quoi de n'importe où en appelant:

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

... et vous pouvez écouter chez un parent, grand-parent, tout ce que vous voulez en appelant:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
fylzero
la source
1
Je recommande vivement de ne pas attacher de propriétés aléatoires à l'objet window, car il est très facile d'écraser des propriétés existantes ou d'entrer en conflit avec des bibliothèques tierces existantes. Au lieu de cela, quiconque utilise Vue pour résoudre ce problème devrait plutôt utiliser la réponse de
@roli roli
1
Je ne suis pas sûr de bien comprendre ou d'accord avec cette préoccupation. La liaison au prototype est une bonne approche, mais la liaison à la fenêtre est tout aussi, sinon plus, courante et probablement une manière plus standard de gérer cela. Vous nommez la propriété, il est donc simple d'éviter les conflits de noms. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… C'est aussi la solution proposée par Jeff Way sur les Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero