ReactJS - Adicionar ouvinte de evento personalizado ao componente

87

Em javascript antigo, eu tenho o DIV

<div class="movie" id="my_movie">

e o seguinte código javascript

var myMovie = document.getElementById('my_movie');
myMovie.addEventListener('nv-enter', function (event) {
     console.log('change scope');
});

Agora que tenho um componente React, dentro deste componente, no método render, estou retornando meu div. Como posso adicionar um ouvinte de evento para meu evento personalizado? (Estou usando esta biblioteca para aplicativos de TV - navegação )

import React, { Component } from 'react';

class MovieItem extends Component {

  render() {

    if(this.props.index === 0) {
      return (
        <div aria-nv-el aria-nv-el-current className="menu_item nv-default">
            <div className="indicator selected"></div>
            <div className="category">
                <span className="title">{this.props.movieItem.caption.toUpperCase()}</span>
            </div>
        </div>
      );
    }
    else {
      return (
        <div aria-nv-el className="menu_item nv-default">
            <div className="indicator selected"></div>
            <div className="category">
                <span className="title">{this.props.movieItem.caption.toUpperCase()}</span>
            </div>
        </div>
      );
    }
  }

}

export default MovieItem;

Atualização # 1:

insira a descrição da imagem aqui

Eu apliquei todas as idéias fornecidas nas respostas. Eu defini a biblioteca de navegação para o modo de depuração e sou capaz de navegar em meus itens de menu apenas com base no teclado (como você pode ver na captura de tela, fui capaz de navegar para Filmes 4), mas quando foco um item no menu ou pressione Enter, não vejo nada no console.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class MenuItem extends Component {

  constructor(props) {
    super(props);
    // Pre-bind your event handler, or define it as a fat arrow in ES7/TS
    this.handleNVFocus = this.handleNVFocus.bind(this);
    this.handleNVEnter = this.handleNVEnter.bind(this);
    this.handleNVRight = this.handleNVRight.bind(this);
  }

  handleNVFocus = event => {
      console.log('Focused: ' + this.props.menuItem.caption.toUpperCase());
  }

  handleNVEnter = event => {
      console.log('Enter: ' + this.props.menuItem.caption.toUpperCase());
  }

  handleNVRight = event => {
      console.log('Right: ' + this.props.menuItem.caption.toUpperCase());
  }

  componentDidMount() {
    ReactDOM.findDOMNode(this).addEventListener('nv-focus', this.handleNVFocus);
    ReactDOM.findDOMNode(this).addEventListener('nv-enter', this.handleNVEnter);
    ReactDOM.findDOMNode(this).addEventListener('nv-right', this.handleNVEnter);
    //this.refs.nv.addEventListener('nv-focus', this.handleNVFocus);
    //this.refs.nv.addEventListener('nv-enter', this.handleNVEnter);
    //this.refs.nv.addEventListener('nv-right', this.handleNVEnter);
  }

  componentWillUnmount() {
    ReactDOM.findDOMNode(this).removeEventListener('nv-focus', this.handleNVFocus);
    ReactDOM.findDOMNode(this).removeEventListener('nv-enter', this.handleNVEnter);
    ReactDOM.findDOMNode(this).removeEventListener('nv-right', this.handleNVRight);
    //this.refs.nv.removeEventListener('nv-focus', this.handleNVFocus);
    //this.refs.nv.removeEventListener('nv-enter', this.handleNVEnter);
    //this.refs.nv.removeEventListener('nv-right', this.handleNVEnter);
  }

  render() {
    var attrs = this.props.index === 0 ? {"aria-nv-el-current": true} : {};
    return (
      <div ref="nv" aria-nv-el {...attrs} className="menu_item nv-default">
          <div className="indicator selected"></div>
          <div className="category">
              <span className="title">{this.props.menuItem.caption.toUpperCase()}</span>
          </div>
      </div>
    )
  }

}

export default MenuItem;

Deixei algumas linhas comentadas porque em ambos os casos não consigo fazer com que as linhas do console sejam registradas.

Atualização # 2: esta biblioteca de navegação não funciona bem com o React com suas tags Html originais, então eu tive que definir as opções e renomear as tags para usar aria- * para não impactar o React.

navigation.setOption('prefix','aria-nv-el');
navigation.setOption('attrScope','aria-nv-scope');
navigation.setOption('attrScopeFOV','aria-nv-scope-fov');
navigation.setOption('attrScopeCurrent','aria-nv-scope-current');
navigation.setOption('attrElement','aria-nv-el');
navigation.setOption('attrElementFOV','aria-nv-el-fov');
navigation.setOption('attrElementCurrent','aria-nv-el-current');
Thiago
fonte
@O Estou basicamente usando o exemplo deste arquivo ( github.com/ahiipsa/navigation/blob/master/demo/index.html )
Thiago
Você não precisa pré-vincular em seu construtor ( this.handleNVEnter = this.handleNVEnter.bind(this)) e usar inicializadores de propriedade ES7 com funções de seta ( handleNVEnter = enter => {}) porque as funções de seta grande são sempre vinculadas. Se você pode usar a sintaxe ES7, siga em frente.
Aaron Beall
1
Obrigado Aaron. Consegui resolver o problema. Vou aceitar sua resposta, pois agora estou usando sua solução, mas também tinha que fazer outra coisa. Como as tags HTML da biblioteca Nagivation não funcionam bem com React, tive que definir os nomes das tags na configuração lib para usar o prefixo aria- *, o problema é que os eventos também foram disparados usando o mesmo prefixo, definindo o evento como aria -nv-enter fez o truque! Está funcionando bem agora. Obrigado!
Thiago
Eu recomendaria mudar aria-*para data-*porque os atributos ARIA são de um conjunto padrão, você não pode criar o seu próprio. Os atributos de dados podem ser definidos de forma mais arbitrária para o que você quiser.
Marcy Sutton

Respostas:

86

Se você precisar lidar com eventos DOM ainda não fornecidos pelo React , deverá adicionar ouvintes DOM depois que o componente for montado:

Atualização: entre o React 13, 14 e 15, foram feitas alterações na API que afetam minha resposta. Abaixo está a forma mais recente de usar React 15 e ES7. Veja o histórico de respostas para versões anteriores.

class MovieItem extends React.Component {

  componentDidMount() {
    // When the component is mounted, add your DOM listener to the "nv" elem.
    // (The "nv" elem is assigned in the render function.)
    this.nv.addEventListener("nv-enter", this.handleNvEnter);
  }

  componentWillUnmount() {
    // Make sure to remove the DOM listener when the component is unmounted.
    this.nv.removeEventListener("nv-enter", this.handleNvEnter);
  }

  // Use a class arrow function (ES7) for the handler. In ES6 you could bind()
  // a handler in the constructor.
  handleNvEnter = (event) => {
    console.log("Nv Enter:", event);
  }

  render() {
    // Here we render a single <div> and toggle the "aria-nv-el-current" attribute
    // using the attribute spread operator. This way only a single <div>
    // is ever mounted and we don't have to worry about adding/removing
    // a DOM listener every time the current index changes. The attrs 
    // are "spread" onto the <div> in the render function: {...attrs}
    const attrs = this.props.index === 0 ? {"aria-nv-el-current": true} : {};

    // Finally, render the div using a "ref" callback which assigns the mounted 
    // elem to a class property "nv" used to add the DOM listener to.
    return (
      <div ref={elem => this.nv = elem} aria-nv-el {...attrs} className="menu_item nv-default">
        ...
      </div>
    );
  }

}

Exemplo no Codepen.io

Aaron Beall
fonte
2
Você está usando mal findDOMNode. No seu caso var elem = this.refs.nv;é o suficiente.
Pavlo
1
@Pavlo Hm, você está certo, parece que isso mudou na v.14 (para retornar o elemento DOM em vez do elemento React como na v.13, que é o que estou usando). Obrigado.
Aaron Beall
2
Por que preciso "Certifique-se de remover o listener DOM quando o componente for desmontado"? Existe alguma fonte de que eles criarão um vazamento?
Fen1kz
1
@levininja Clique no edited Aug 19 at 6:19texto abaixo da postagem, que o levará ao histórico de revisão .
Aaron Beall
1
@ NicolasS.Xu React não fornece nenhuma API de despacho de evento personalizado, pois espera-se que você use propriedades de retorno de chamada (veja esta resposta ), mas você pode usar o DOM padrão nv.dispatchEvent()se precisar.
Aaron Beall
20

Você poderia usar componentDidMount e componentWillUnmount métodos:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class MovieItem extends Component
{
    _handleNVEvent = event => {
        ...
    };

    componentDidMount() {
        ReactDOM.findDOMNode(this).addEventListener('nv-event', this._handleNVEvent);
    }

    componentWillUnmount() {
        ReactDOM.findDOMNode(this).removeEventListener('nv-event', this._handleNVEvent);
    }

    [...]

}

export default MovieItem;
vbarbarosh
fonte
Olá @vbarbarosh, atualizei a pergunta com mais detalhes
Thiago
4

Em primeiro lugar, os eventos personalizados não funcionam bem com os componentes React nativamente. Então você não pode apenas dizer <div onMyCustomEvent={something}>na função de renderização e tem que pensar sobre o problema.

Em segundo lugar, depois de dar uma olhada na documentação da biblioteca que você está usando, o evento é realmente disparado document.body, portanto, mesmo que funcione, seu manipulador de eventos nunca será disparado.

Em vez disso, em componentDidMountalgum lugar em seu aplicativo, você pode ouvir nv-enter adicionando

document.body.addEventListener('nv-enter', function (event) {
    // logic
});

Em seguida, dentro da função de retorno de chamada, acesse uma função que altere o estado do componente ou o que você quiser fazer.

dannyjolie
fonte
2
Você poderia ajudar a explicar por que "eventos personalizados não funcionam bem com componentes React nativamente"?
codeful.element
1
@ codeful.element, este site tem algumas informações sobre isso: custom-elements-everywhere.com/#react
Paul-Hebert