126

In plain old HTML I have the DIV

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

and the following javascript code

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

Now I have a React Component, inside this component, in the render method, I am returning my div. How can I add an event listener for my custom event? (I am using this library for TV apps - navigation )

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;

Update #1:

enter image description here

I applied all the ideas provided in the answers. I set the navigation library to debug mode and I am able to navigate on my menu items only based on the keyboard (as you can see in the screenshot I was able to navigate to Movies 4) but when I focus an item in the menu or press enter, I dont see anything in the 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;

I left some lines commented because in both cases I am not able to get the console lines to be logged.

Update #2: This navigation library does not work well with React with its original Html Tags, so I had to set the options and rename the tags to use aria-* so it would not impact 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');
dankilev
  • 720
  • 2
  • 10
  • 32
Thiago
  • 5,152
  • 11
  • 35
  • 44
  • @The I am basically using the example from this file (https://github.com/ahiipsa/navigation/blob/master/demo/index.html) – Thiago Mar 23 '16 at 14:14
  • You don't need to both pre-bind in your constructor (`this.handleNVEnter = this.handleNVEnter.bind(this)`) and use ES7 property initializers with arrow functions (`handleNVEnter = enter => {}`) because fat arrow functions are always bound. If you can use ES7 syntax just go with that. – Aaron Beall Mar 23 '16 at 16:05
  • 1
    Thank you Aaron. I Was able to fix the problem. I am going to accept your answer as I am now using your solution but I also had to do something else. Since the Nagivation library HTML tags dont play nice with React, I had to set the tag names in the lib configuration to use aria-* prefix, the problem is that the events were also triggered using the same prefix, so setting the event to aria-nv-enter did the trick! Its working fine now. Thank you! – Thiago Mar 23 '16 at 16:13
  • I would recommend changing `aria-*` to `data-*` because ARIA attributes are from a standard set, you cannot make up your own. Data attributes can be more arbitrarily set to whatever you want. – Marcy Sutton Jan 27 '17 at 20:20

5 Answers5

108

If you need to handle DOM events not already provided by React you have to add DOM listeners after the component is mounted:

Update: Between React 13, 14, and 15 changes were made to the API that affect my answer. Below is the latest way using React 15 and ES7. See answer history for older versions.

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

}

Example on Codepen.io

technomage
  • 9,861
  • 2
  • 26
  • 40
Aaron Beall
  • 49,769
  • 26
  • 85
  • 103
  • 2
    You are misusing `findDOMNode`. In your case `var elem = this.refs.nv;` is enough. – Pavlo Mar 23 '16 at 15:11
  • @Pavlo `refs` gives me a React element (without any `addEventListener` function), not a DOM node. – Aaron Beall Mar 23 '16 at 15:19
  • 2
    @Pavlo Hm, you're right, it seems that this changed in v.14 (to return the DOM element instead of the React element as in v.13, which is what I am using). Thanks. – Aaron Beall Mar 23 '16 at 15:20
  • using `ref=` is discourage you should use the callback as example `
    this.$e = e} />`
    – ncubica Dec 15 '16 at 03:28
  • 2
    Why do I need to "Make sure to remove the DOM listener when the component is unmounted"? Is there any source that they will create a leak? – Fen1kz Apr 26 '17 at 09:50
  • @ncubica Good point, `ref` usage changed again... I'll update the answer. – Aaron Beall May 23 '17 at 15:52
  • I don't know of a way to view answer history in SO; if there is can someone enlighten me? – levininja Dec 08 '17 at 22:53
  • 1
    @levininja Click the `edited Aug 19 at 6:19` text under the post, which takes you to [revision history](https://stackoverflow.com/posts/36181732/revisions). – Aaron Beall Dec 09 '17 at 00:20
  • How do you dispatch event? – Nicolas S.Xu May 21 '18 at 15:40
  • 1
    @NicolasS.Xu React doesn't provide any custom event dispatching API as you are expected to use callback props (see [this answer](https://stackoverflow.com/questions/21951734/react-js-custom-events-for-communicating-with-parent-nodes)), but you can use standard DOM `nv.dispatchEvent()` if you need to. – Aaron Beall May 21 '18 at 18:12
  • React will create a new instance of the ref on every re-render instead of keeping it between renders so you would need to re-add the eventListener on `componentDidUpdate`. You should use `useRef` instead. – Ryan Walker Aug 08 '19 at 00:10
  • what is CustomEvent? – May'Habit Nov 26 '19 at 03:20
  • Is it possible to use the DOM listener, ie the `addEventListener()` method, in the `useEffect ()` hook method instead of the original `componentDidMount ()` method ?? – Petr Fořt Fru-Fru Nov 12 '20 at 13:29
  • In the example, a ref to a `div` element is returned. What if we're returning another React Component instead? You can still get a ref, but Component doesn't seem to implement `addEventListener()`. How to handle this case? – Brent Baccala Nov 30 '20 at 18:40
21

You could use componentDidMount and componentWillUnmount methods:

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
  • 3,502
  • 4
  • 33
  • 43
4

First off, custom events don't play well with React components natively. So you cant just say <div onMyCustomEvent={something}> in the render function, and have to think around the problem.

Secondly, after taking a peek at the documentation for the library you're using, the event is actually fired on document.body, so even if it did work, your event handler would never trigger.

Instead, inside componentDidMount somewhere in your application, you can listen to nv-enter by adding

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

Then, inside the callback function, hit a function that changes the state of the component, or whatever you want to do.

dannyjolie
  • 10,959
  • 3
  • 33
  • 28
2

I recommend using React.createRef() and ref=this.elementRef to get the DOM element reference instead of ReactDOM.findDOMNode(this). This way you can get the reference to the DOM element as an instance variable.

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

class MenuItem extends Component {

  constructor(props) {
    super(props);

    this.elementRef = React.createRef();
  }

  handleNVFocus = event => {
      console.log('Focused: ' + this.props.menuItem.caption.toUpperCase());
  }
    
  componentDidMount() {
    this.elementRef.addEventListener('nv-focus', this.handleNVFocus);
  }

  componentWillUnmount() {
    this.elementRef.removeEventListener('nv-focus', this.handleNVFocus);
  }

  render() {
    return (
      <element ref={this.elementRef} />
    )
  }

}

export default MenuItem;
2

Here is a dannyjolie more detailed answer without need of component reference but using document.body reference.

First somewhere in your app, there is a component method that will create a new custom event and send it. For example, your customer switch lang. In this case, you can attach to the document body a new event :

setLang(newLang) {
    // lang business logic here
    // then throw a new custom event attached to the body :
    document.body.dispatchEvent(new CustomEvent("my-set-lang", {detail: { newLang }}));
  }

Once that done, you have another component that will need to listen to the lang switch event. For example, your customer is on a given product, and you will refresh the product having new lang as argument.

First add/remove event listener for your target component :

  componentDidMount() {
    document.body.addEventListener('my-set-lang', this.handleLangChange.bind(this));
  }

  componentWillUnmount() {
    document.body.removeEventListener('my-set-lang', this.handleLangChange.bind(this));
  }

then define your component my-set-langw handler

  handleLangChange(event) {
    console.log("lang has changed to", event.detail.newLang);
    // your business logic here .. this.setState({...});
  }
Creharmony
  • 31
  • 3