Добавить обработчик событий в элемент React.DOM динамически

Я работаю с компонентом RadioButtonGroup, который похож на ввод радио, но с кнопками:

enter image description here

Было бы хорошо, если бы использовать компонент было легко:

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

Фактический вопрос: Как я могу достичь этого наиболее элегантно с помощью React? (я нашел другой аналогичный вопрос, но он не точно ответьте на это).

Моя первая интуиция заключалась в том, чтобы добавить и удалить обработчики onClick внутри компонента, чтобы удалить код плиты котла от пользователя компонента. Другой вариант, который приходит мне на ум, заключается в том, чтобы дать, например, <p> вместо элементов кнопок и поместите их внутри элементов кнопки, которые будут созданы внутри компонента RadioButtonGroup. Мне это не нравится, потому что это не делает семантически понятным семантически по сравнению с прохождением кнопок.

Здесь выглядит (явно не работающий) компонент:

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

Ответы

Ответ 1

Вы не хотите связываться с обработчиками кликов на каждой кнопке, просто слушайте клик по контейнеру. Затем обновите состояние, на основе которого будет нажата кнопка.

Кроме того, с помощью React лучше всего сохранить все ваши вещи DOM в функции рендеринга. В этом случае, определяя имя класса элемента.

Вот как это могло бы работать:

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

Ответ 2

Чтобы получить api, подобное этому (аналогично <input/>), нам нужно использовать addon cloneWithProps.

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

Все это делает каждый из них, добавляет обработчик кликов и условно добавляет к нему классName 'active'. Вы легко можете (и должны) изменить его, чтобы принять активное имя класса в качестве опоры.

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

демонстрация

Если вы не хотите использовать cloneWithProps, вы можете использовать оболочку div, но стиль может быть немного сложнее.

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

Причина, по которой все использует индекс, заключается в том, что вы передаете реагирующие элементы, которые непрозрачны. Там нет чистого способа получить данные из этих кнопок, но мы знаем их индекс, потому что мы повторяем их, используя React.Children.map. Альтернативный api будет выглядеть следующим образом:

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

Здесь мы можем перебирать this.props.options и передавать ключ к обратному вызову onChange и принимать, например, 'test1' в качестве значения prop.

Ответ 3

Это мой код. Я не знаю, является ли это лучшим способом, так как я начал использовать реакцию всего 5 часов назад. Но он отлично работает.

var LeftPane = React.createClass({
    getInitialState: function() {
        return {list:[{name:"Item 1",isactive:1},
                      {name:"Item 2",isactive:0},
                      {name:"Item 3",isactive:0},                     
                      {name:"Item 4",isactive:0},
                      {name:"Item 5",isactive:0}]};
    },
    clickHandler: function(index) {
        var current_list = this.state.list;
        current_list.map(function(record,i){
          if(record.isactive==1){
            if(i!=index){
              record.isactive =0;
            }else{
              return;
            }
          }else{
            if(i==index){
              record.isactive =1;
            }
          }
        });
        this.setState({data:current_list}); 
    },   
    render: function() {
        var active_item = "list-group-item active"
        var non_active_item ="list-group-item";
        var _this = this;
        return (              
          <div className="list-group">
            {this.state.list.map(function(record,i) {                          
                if(record.isactive==1){
                return <a className={active_item} onClick={_this.clickHandler.bind(null, i)}>{record.name}</a>
                }else{
                return <a className={non_active_item} onClick={_this.clickHandler.bind(null, i)}>{record.name}</a>
                }                  
            })}
          </div>
        </div>
        )
    }
});

Ответ 4

Я бы не хотел добавлять обработчик onClick для каждого элемента кнопки

React использует шаблон делегирования событий, поэтому вы можете свободно добавлять столько обработчиков onClick, сколько нужно.

Другим способом, если вы просто хотите сделать код понятным, вы можете создать свой собственный компонент кнопки и передать _onActiveChange для него реквизиты, но я не уверен, что это необходимо в вашем случае.

И помните, что React работает с синтетическими событиями, и в некоторых случаях использование setState внутри собственных обработчиков событий может вызвать непредсказуемое поведение.