131

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');
4
  • @The I am basically using the example from this file (github.com/ahiipsa/navigation/blob/master/demo/index.html) Commented Mar 23, 2016 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. Commented Mar 23, 2016 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! Commented Mar 23, 2016 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. Commented Jan 27, 2017 at 20:20

5 Answers 5

115

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

Sign up to request clarification or add additional context in comments.

14 Comments

You are misusing findDOMNode. In your case var elem = this.refs.nv; is enough.
@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.
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?
@levininja Click the edited Aug 19 at 6:19 text under the post, which takes you to revision history.
@NicolasS.Xu React doesn't provide any custom event dispatching API as you are expected to use callback props (see this answer), but you can use standard DOM nv.dispatchEvent() if you need to.
|
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;

1 Comment

Hi @vbarbarosh, I updated the question with more details
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.

2 Comments

Could you help elaborate on the reason why "custom events don't play well with React components natively"?
@codeful.element, this site has some info on that: custom-elements-everywhere.com/#react
3

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({...});
  }

Comments

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;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.