VueJs 2.0 emite evento do componente do neto para o avô

87

Parece que o Vue.js 2.0 não emite eventos de um neto para o componente de seu avô.

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 resolve o problema https://jsfiddle.net/y5dvkqbd/4/ , mas emtting dois eventos:

  • Um do neto ao componente do meio
  • Em seguida, emitindo novamente do componente do meio para o avô

Adicionar este evento intermediário parece repetitivo e desnecessário. Existe uma maneira de emitir diretamente para os avós que eu não conheço?

BassMHL
fonte

Respostas:

64

O Vue 2.4 introduziu uma maneira de facilmente passar eventos para cima na hierarquia usando vm.$listeners

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

Contém v-onouvintes de eventos de escopo pai (sem .nativemodificadores). Isso pode ser passado para um componente interno via v-on="$listeners"- útil ao criar componentes de invólucro transparentes.

Veja o snippet abaixo usando v-on="$listeners"o grand-childcomponente no childmodelo:

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
fonte
Eu descobri que essa é a maneira mais limpa sem introduzir vuex / bus / root. Isso irá passar todas as funções de emissão para cima. Você ainda pode acessar as emissões das crianças tendo @ someEmit = "someEmitHandler" antes de v-on = "$ listeners"
Mathias Haugsbø
39

A comunidade Vue geralmente prefere usar Vuex para resolver esse tipo de problema. As alterações são feitas no estado Vuex e a representação DOM flui apenas a partir dele, eliminando a necessidade de eventos em muitos casos.

Exceto isso, reemitir seria provavelmente a próxima melhor escolha e, por último, você pode escolher usar um barramento de evento conforme detalhado na outra resposta altamente votada a esta pergunta.

A resposta abaixo é minha resposta original a esta pergunta e não é uma abordagem que eu faria agora, tendo mais experiência com Vue.


Este é um caso em que posso discordar da escolha de design do Vue e recorrer ao DOM.

Em 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 em parent,

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

Caso contrário, sim, você tem que reemitir, ou usar um ônibus.

Observação: adicionei código no método doEvent para lidar com o IE; esse código pode ser extraído de forma reutilizável.

Bert
fonte
Isso se comporta de forma diferente para o IE? não sabia que havia discrepâncias no navegador com o vue ...
BassMHL
@BassemLhm Vue está bem com o IE. O problema com o IE não é o Vue, é uma solução DOM e você não pode fazer um novo Event () no IE. Você tem que document.createEvent (). Posso adicionar o suporte do IE, se necessário.
Bert,
Não faz sentido instalar o vuex apenas para um caso simples.
Adam Orlov
@AdamOrlov eu concordo com você.
Bert de
Uma solução mais simples: stackoverflow.com/a/55650245/841591
digout
28

NOVA RESPOSTA (atualização de novembro de 2018)

Descobri que poderíamos realmente fazer isso aproveitando a $parentpropriedade no componente neto:

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

Muito mais limpo e simples.

BassMHL
fonte
15
Observe que isso só funciona se o relacionamento for filho -> avô. Isso não funciona se o filho puder ser aninhado em níveis arbitrários de profundidade.
Qtax
Veja minha resposta stackoverflow.com/a/55650245/841591 em resposta ao comentário de @Qtax
digout
7
Você não quer que esse tipo de coisa aconteça em seu grande projeto. Você coloca o 'filho' em um transitionou em qualquer outro componente do invólucro e ele se quebra, deixando você com um grande ponto de interrogação em sua cabeça.
Adam Orlov
@AdamOrlov Eu concordo, isso é uma prática ruim. Lide com eventos como este usando uma loja Vuex.
Fabian von Ellerts
Isso é mais simples: stackoverflow.com/a/55650245/841591
digout
26

Sim, seus eventos corretos só vão de filho para pai. Eles não vão além, por exemplo, da criança ao avô.

A documentação do Vue (resumidamente) aborda essa situação na seção Comunicação entre pais e filhos .

A ideia geral é que no componente avô você crie um Vuecomponente vazio que é passado do avô aos filhos e netos por meio de acessórios. O avô então escuta os eventos e os netos emitem eventos nesse "barramento de eventos".

Alguns aplicativos usam um barramento de evento global em vez de um barramento de evento por componente. Usar um barramento de evento global significa que você precisará ter nomes de eventos exclusivos ou namespacing para que os eventos não entrem em conflito entre os diferentes componentes.

Aqui está um exemplo de como implementar um barramento de evento global simples .

Sly_cardinal
fonte
16

Outra solução será ligar / emitir no nó raiz :

Usa vm.$root.$emitno neto e depois vm.$root.$onno ancestral (ou em qualquer lugar que você quiser).

Atualizado : às vezes, você gostaria de desativar o ouvinte em algumas situações específicas, use vm. $ Off (por exemplo: 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>

Esfinge
fonte
A solução mais elegante para mim.
Ovilia,
Ótima solução! Isso funcionou bem para mim em um aplicativo principalmente do lado do servidor (Laravel) com alguns componentes Vue (eu não queria introduzir uma camada Vuex). Meu problema específico era um componente de botão personalizado que acionava um modal Buefy com um componente de formulário. Depois que o formulário foi enviado, eu queria desabilitar o botão, mas os eventos personalizados emitidos pelo formulário não alcançariam o botão, pois o Modal era o pai direto do formulário.
Richard,
Isso não destrói o ouvinte ao destruir. Tudo bem? Seria ótimo se você pudesse adicionar essa lógica, então eu sei como fazer isso também.
mesqueeb
14

Se você deseja ser flexível e simplesmente transmitir um evento para todos os pais e seus pais recursivamente até a raiz, você pode fazer algo como:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
desenterrar
fonte
1
Esta é a resposta certa. Eu criei uma função de utilidade para fazer isso em vários lugares em meu código: propagateEvent (componente, eventName, value) {while (component) {component. $ Emit (eventName, value); componente = componente. $ pai; }}
ccleve
2
Esta é uma resposta elegante, muito simples de usar e fácil de ler. E não é quebrado pela adição de níveis adicionais de embalagem.
Dean
4

Este é o único caso quando eu uso o bus de eventos !! Para passar dados do filho aninhado profundo, para o pai não diretamente, comunicação.

Primeiro : crie um arquivo js (eu chamo de eventbus.js) com este conteúdo:

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

Segundo : Em seu componente filho, emita um evento:

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

Terceiro : No pai, ouça esse evento:

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

Observação: se você não quiser mais esse evento, cancele o registro:

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

INFO: Não há necessidade de ler a opinião pessoal abaixo

Não gosto de usar vuex para comunicação de avô para avô (ou nível de comunicação semelhante).

No vue.js, para passar dados do avô para o neto, você pode usar fornecer / injetar . Mas não há algo semelhante para o oposto. (neto para avô) Então, eu uso o ônibus de eventos sempre que tenho que fazer esse tipo de comunicação.

Roli Roli
fonte
2

Fiz um pequeno mixin baseado na resposta @digout. Você quer colocá-lo, antes da inicialização da instância do Vue (novo Vue ...) para usá-lo globalmente no projeto. Você pode usá-lo de forma semelhante ao evento normal.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
Kubaklamca
fonte
esta solução é o que usei para minha implementação, mas adicionei um parâmetro adicional targetRefque interrompe a propagação no componente que você está almejando. A whilecondição, então, incluiria && vm.$refs[targetRef]- você também precisaria incluir esse refatributo no componente de destino. Em meu caso de uso, eu não precisei fazer um túnel até o root, evitando que alguns eventos fossem disparados e talvez alguns preciosos nanossegundos de hora
mcgraw
2

Os componentes VueJS 2 possuem uma $parentpropriedade que contém seu componente pai.

Esse componente pai também inclui sua própria $parentpropriedade.

Então, para acessar o componente "avô" é uma questão de acessar o componente "pai dos pais":

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

De qualquer forma, isso é meio complicado , e eu recomendo usar um gerenciador de estado global como o Vuex ou ferramentas semelhantes, como outros respondentes disseram.

Rogervila
fonte
1

Analisando as respostas de @kubaklam e @digout, isto é o que eu uso para evitar emitir em todos os componentes dos pais entre o neto e o avô (possivelmente 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)
    }
  }
}

Ao construir um componente com netos distantes onde você não deseja que muitos / nenhum componente seja vinculado ao armazenamento, mas deseja que o componente raiz atue como um armazenamento / fonte da verdade, isso funciona muito bem. Isso é semelhante à filosofia de ações para baixo de dados para cima do Ember. O lado negativo é que, se você quiser ouvir esse evento em todos os pais entre eles, isso não funcionará. Mas então você pode usar $ propogateEmit como na resposta acima por @kubaklam.

Editar: a vm inicial deve ser definida para o componente, e não para o pai do componente. Ou seja let vm = thise nãolet vm = this.$parent

usuario
fonte
0

Eu realmente gosto de como isso é tratado, criando uma classe vinculada à janela e simplificando a configuração de transmissão / escuta para funcionar onde quer que você esteja no aplicativo 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);  
    }

}

Agora você pode simplesmente disparar / transmitir / qualquer coisa de qualquer lugar chamando:

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

... e você pode ouvir um pai, avô, o que quiser, chamando:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});
fylzero
fonte
1
Eu recomendo fortemente não anexar propriedades aleatórias ao objeto de janela, uma vez que é muito fácil sobrescrever propriedades existentes ou entrar em conflito com bibliotecas de terceiros existentes. Em vez disso, qualquer pessoa que use o Vue para resolver este problema deve usar a resposta de
@roli roli
1
Não tenho certeza se entendo totalmente ou concordo com essa preocupação. Vincular ao protótipo é uma boa abordagem, mas vincular à janela é tão, se não mais, comum e provavelmente uma maneira mais padrão de lidar com isso. Você nomeia a propriedade, portanto, é simples evitar conflitos de nomenclatura. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Esta também é a solução proposta que Jeff Way usa em Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero