Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add event handler to React.DOM element dynamically

Tags:

reactjs

I'm working with a RadioButtonGroup component which is like radio input but with buttons:

enter image description here

It would be good if using the component was easy like this:

var SelectThing = React.createClass({
    render: function render() {
        // I would not like to add onClick handler to every button element
        // outside the RadioButtonGroup component
        return (
            <RadioButtonGroup onChange={this._onActiveChange}>
                <button>Thing 1</button>
                <button>Thing 2</button>
                <button>Thing 3</button>
            </RadioButtonGroup>
        )
    },

    _onActiveChange: function _onActiveChange(index) {
        console.log('You clicked button with index:', index);
    }
});

The actual question: How can I achieve that the most elegantly with React? (I found another similar question but it doesn't exactly answer to this).

My first intuition was to add and remove the onClick handlers inside the component to remove boiler plate code from the component's user. Another option that comes to my mind is to give e.g. <p> elements instead of button elements and put them inside button elements which would be created inside the RadioButtonGroup component. I don't like the latter that much because it doesn't make that much sense semantically compared to passing buttons.

Here's what the (obviously not working) component looks like now:

// Radio input with buttons
// http://getbootstrap.com/javascript/#buttons-checkbox-radio
var RadioButtonGroup = React.createClass({
    getInitialState: function getInitialState() {
        return {
            active: this.props.active || 0
        };
    },

    componentWillMount: function componentWillMount() {
        var buttons = this.props.children;
        buttons[this.props.active].className += ' active';

        var self = this;
        buttons.forEach(function(button, index) {
            // How to dynamically set onclick handler for virtual dom
            // element created inside JSX?
            button.addEventListener('onClick', function(event) {
                self._onAnyButtonClick(index, event);
            }
        });
    },

    componentWillUnmount: function componentWillUnmount() {
        var buttons = this.props.children;

        buttons.forEach(function(button, index) {
            button.removeEventListener('onClick');
        });  
    },

    render: function render() {
        return (
            <div className="radio-button-group">
                {buttons}
            </div>
        )
    },

    _onAnyButtonClick: function _onAnyButtonClick(index, event) {
        this.setState({
            active: index
        });

        this.props.onChange(index);
    }
});
like image 399
Kimmo Avatar asked Dec 30 '14 15:12

Kimmo


People also ask

How do you add an event handler to a dynamic button?

To attach event handlers to the dynamically created button, we need to select the button with a class of btn and add an event listener of click . We're saying that onclick of the button, the p tag with a class of moreInfo should display block .

How do you add an event dynamic to an element?

Attaching the event dynamicallyclassName = 'dynamic-link'; // Class name li. innerHTML = dynamicValue; // Text inside $('#links'). appendChild(li); // Append it li. onclick = dynamicEvent; // Attach the event!

Which prop is used to add click event listener on any element in React DOM?

To add the click event in React using plain JavaScript, you need to use addEventListener() to assign the click event to an element. Create one <button> element as ref props so that it can be accessed to trigger the click event.


2 Answers

You don't want to mess with click handlers on each button, just listen for the click on the container. Then update the state based on which child is clicked.

Also, with React it's best to keep all of your DOM stuff in the render function. In this case, defining an element's class name.

Here's how this could work:

var RadioButtonGroup = React.createClass({
    getInitialState: function getInitialState() {
        return {
            active: this.props.active || 0
        };
    },

    clickHandler: function clickHandler(e) {
        // Getting an array of DOM elements
        // Then finding which element was clicked
        var nodes = Array.prototype.slice.call( e.currentTarget.children );
        var index = nodes.indexOf( e.target );
        this.setState({ active: index });
    },

    render: function render() {
        var buttons = this.children.map(function(child, i) {
            if (i === this.state.active) child.props.className += ' active';
            return child;
        }, this);

        return (
            <div className="radio-button-group" onClick={ this.clickHandler }>
                { buttons }
            </div>
        )
    }
});
like image 107
Shawn Avatar answered Nov 03 '22 09:11

Shawn


To get an api like this (similar to <input/>), we need to use the cloneWithProps addon.

  <RadioButtonGroup onChange={this.handleChange} value={this.state.selected}>
    <button>Test 1</button>
    <button>Test 2</button>
    <button>Test 3</button>
  </RadioButtonGroup>

All this does is take each child, add a click handler, and conditionally add a className of 'active' to it. You easily can (and should) modify it to take the active class name as a prop.

var RadioButtonGroup = React.createClass({
  render: function(){
    return <div>{React.Children.map(this.props.children, this.renderItem)}</div>
  },
  renderItem: function(button, index){
    return React.cloneElement(button, {
      className: this.props.value === index ? ' active ' : '',
      onClick: function(){ 
        this.props.onChange(index); 
      }.bind(this),
      key: index
    });
  }
});

demo

If you don't want to use cloneWithProps, you could use a wrapper div, but styling may be a bit more complex.

  renderItem: function(button, index){
    return React.createElement('div', {
      className: this.props.value === index ? ' active ' : '',
      onClick: function(){ 
        this.props.onChange(index); 
      }.bind(this),
      key: index
    }, button);
  }

The reason everything uses index is because you're passing react elements, which are opaque. There's no clean way to get any data out of these buttons, but we do know their index because we're iterating over them using React.Children.map. An alternative api would look like this:

<RadioButtonGroup value={'test1'} onChange={fn} options={{
   test1: <button>Test 1</button>,
   test2: <button>Test 2</button>,
   test3: <button>Test 3</button>
}} />

Here we can iterate over this.props.options, and pass the key to the onChange callback, and take e.g. 'test1' as a value prop.

like image 31
Brigand Avatar answered Nov 03 '22 11:11

Brigand