Работа с input

ОБНОВЛЕНИЕ 2018: в учебнике хорошая теория, но ему уже два года. Проверяйте версии пакетов. За выходом нового учебника можно следить в telegram канале или twitter

На канале так же проводятся бесплатные вебинары, публикуются переводы и авторские материалы, присоединяйтесь!

Работа с input

Сперва приберемся:

Удалим лишние console.log'и, удалим обработчик onTotalNewsClick.

Затем создадим компонент - <TestInput />, который будет просто отрисовывать (render) - input перед списком новостей.

Полный листинг нашего кода после данных манипуляций:

js/app.js

var my_news = [
  {
    author: 'Саша Печкин',
    text: 'В четчерг, четвертого числа...',
    bigText: 'в четыре с четвертью часа четыре чёрненьких чумазеньких чертёнка чертили чёрными чернилами чертёж.'
  },
  {
    author: 'Просто Вася',
    text: 'Считаю, что $ должен стоить 35 рублей!',
    bigText: 'А евро 42!'
  },
  {
    author: 'Гость',
    text: 'Бесплатно. Скачать. Лучший сайт - http://localhost:3000',
    bigText: 'На самом деле платно, просто нужно прочитать очень длинное лицензионное соглашение'
  }
];

var Article = React.createClass({
  propTypes: {
    data: React.PropTypes.shape({
      author: React.PropTypes.string.isRequired,
      text: React.PropTypes.string.isRequired,
      bigText: React.PropTypes.string.isRequired
    })
  },
  getInitialState: function() {
    return {
      visible: false
    };
  },
  readmoreClick: function(e) {
    e.preventDefault();
    this.setState({visible: true});
  },
  render: function() {
    var author = this.props.data.author,
        text = this.props.data.text,
        bigText = this.props.data.bigText,
        visible = this.state.visible;

    return (
      <div className='article'>
        <p className='news__author'>{author}:</p>
        <p className='news__text'>{text}</p>
        <a href="#"
          onClick={this.readmoreClick}
          className={'news__readmore ' + (visible ? 'none': '')}>
          Подробнее
        </a>
        <p className={'news__big-text ' + (visible ? '': 'none')}>{bigText}</p>
      </div>
    )
  }
});

var News = React.createClass({
  propTypes: {
    data: React.PropTypes.array.isRequired
  },
  getInitialState: function() {
    return {
      counter: 0
    }
  },
  render: function() {
    var data = this.props.data;
    var newsTemplate;

    if (data.length > 0) {
      newsTemplate = data.map(function(item, index) {
        return (
          <div key={index}>
            <Article data={item} />
          </div>
        )
      })
    } else {
      newsTemplate = <p>К сожалению новостей нет</p>
    }

    return (
      <div className='news'>
        {newsTemplate}
        <strong
          className={'news__count ' + (data.length > 0 ? '':'none') }>Всего новостей: {data.length}</strong>
      </div>
    );
  }
});

// --- добавили test input ---
var TestInput = React.createClass({
  render: function() {
    return (
      <input className='test-input' value='введите значение' />
    );
  }
});

var App = React.createClass({
  render: function() {
    return (
      <div className='app'>
        <h3>Новости</h3>
        <TestInput /> {/* добавили вывод компонента */}
        <News data={my_news} />
      </div>
    );
  }
});

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Напомню про комментарии:

Первый комментарий, добавлен с помощью //, так как данный комментарий не находится внутри JSX. А второй - находится, следовательно имеет вид {/* комментарий */}.

Вообще, код сейчас не работает (но это не из-за комментария). Давайте посмотрим на ошибку внимательно:

Вы предоставили свойство value для поля, у которого нет onChange обработчика. Поэтому отрисовано поле только для чтения. Если поле должно быть изменяемо, используйте defaultValue. Либо установите onChange или readOnly. Проверьте render метод компонента TestInput.

Не могу не любить react за такие подробные сообщения об ошибках.

А вы кстати попробуйте сейчас изменить значение инпута. Ничего не выйдет. Здесь у нас есть два пути, и первый нам известный - использовать какое-нибудь свойство state в качестве динамически изменяемого значения инпута.

Controlled components (контролируемые компоненты)

Для вызова setState, будем использовать событие onChange. Работа с ним не отличается от работы с onClick или другими любыми событиями. Главное - передать функцию-обработчик.

Не торопитесь, давайте подумаем еще раз:

  1. Нам нужно передать функцию обработчик, которая будет изменять какую-то переменную состояния.

  2. Значит нам нужно создать начальное состояние (getInitialState).

  3. Если у нас есть переменная состояния компонента, значит мы хотим, чтобы именно она была в качестве value у нашего инпута.

Сможете сделать сами? Если да - отлично, если нет - решение ниже.

Подсказка #1: так может выглядеть функция-обработчик

...
onChangeHandler: function(e) {
    this.setState({myValue: e.target.value})
},
...

Решение:

var TestInput = React.createClass({
  getInitialState: function() {
    return {
      myValue: ''
    };
  },
  onChangeHandler: function(e) {
    this.setState({myValue: e.target.value})
  },
  render: function() {

    return (
      <input
        className='test-input'
        value={this.state.myValue}
        onChange={this.onChangeHandler}
        placeholder='введите значение'
      />
    );
  }
});

У нас есть placeholder - "введите значение", который будет показываться в момент загрузки страницы, так как наше начальное состояние input'a - пустая строка. При изменении, мы устанавливаем в переменную myValue - то что введено в input. Следовательно - input корректно изменяется.

Обычно, мы хотим по клику отправлять значения инпута...

Задача: По клику на кнопку - показывать alert с текстом инпута.

Попробуйте сами.

Подсказка #1:

Вам необходимо сделать: добавить кнопку, на кнопку "повесить" обработчик onClick, в функции обработчике считывать значение this.state.myValue.

Подсказка #2:

Так как нам необходимо рендерить больше одного элемента, нужно обернуть их в родительский элемент, например в <div></div>

Решение:

var TestInput = React.createClass({
  getInitialState: function() {
    return {
      myValue: ''
    };
  },
  onChangeHandler: function(e) {
    this.setState({myValue: e.target.value})
  },
  onBtnClickHandler: function() {
    alert(this.state.myValue);
  },
  render: function() {
    return (
      <div>
        <input
          className='test-input'
          value={this.state.myValue}
          onChange={this.onChangeHandler}
          placeholder='введите значение'
        />
        <button onClick={this.onBtnClickHandler}>Показать alert</button>
      </div>
    );
  }
});

Предлагаю добавить отступы для .test-input:

css/app.css

...
.test-input {
  margin: 0 5px 5px 0;
}
...

После добавления отступа в данном коде ничего не раздражает. Или нет? Как думаете, что здесь может расстроить борца за оптимизацию?

Ответ: каждый раз, после любого изменения у нас вызывается setState, а значит - полная перерисовка компонента. Не очень приятно. Опять же, чуть больше логики в момент создания компонента и в пору будет расстроиться от "отзывчивого" поля ввода.

Поэтому, наш выбор - это второй путь. Неконтролируемый компонент!

Uncontrolled Components (неконтролируемый компонент)

Главное отличие неконтролируемого компонента от контролируемого в том, что у него нет обработчика изменений, а значит нет постоянных вызовов setState и перерисовок.

Для того чтобы считать значение такого компонента используется вспомогательная функция вспомогательной библиотеки ReactDOM - ReactDOM.findDOMNode, а для того, чтобы можно было найти с помощью нее элемент, используется атрибут ref.

Для неконтролируемого компонента требуется указывать defaultValue.

Начнем по порядку:

  1. Удалим обработчик onChange

  2. Удалим getInitialState

  3. Укажем defaultValue = пустая строка (defaultValue='') вместо value

  4. Добавим атрибут ref, назовем его myTestInput

...
<input
  className='test-input'
  defaultValue=''
  placeholder='введите значение'
  ref='myTestInput'
/>
...

Обновите страницу, попробуйте ввести значение. Работает? Работает!

Теперь нам нужно, научиться считывать значение: перепишите onBtnClickHandler следующим образом:

onBtnClickHandler: function() {
    alert(ReactDOM.findDOMNode(this.refs.myTestInput).value);
},

Метод ReactDOM.findDomNode принимает ссылку (this.refs.МОЙ_ЭЛЕМЕНТ) и возвращает нативный DOM элемент. Для тех кто в танке: $('.my-input') - возвращает jQuery обертку над элементом. У jQuery обертки обычно больше методов и свойств.

Напоследок, давайте поконсолим значения, посмотрим что к чему.

Добавьте атрибут ref для кнопки рядом с инпутом. А затем в обработчике onBtnClickHandler сконсольте this.refs

var TestInput = React.createClass({
  onBtnClickHandler: function() {
    console.log(this.refs);
    alert(ReactDOM.findDOMNode(this.refs.myTestInput).value);
  },
  render: function() {
    return (
      <div>
        <input
          className='test-input'
          defaultValue=''
          placeholder='введите значение'
          ref='myTestInput'
        />
        <button onClick={this.onBtnClickHandler} ref='alert_button'>Показать alert</button>
      </div>
    );
  }
});

В браузере:

Как видите, this.refs содержит все refs компонента. Если попробовать раскрыть свойства какого-нибудь из них - реакт ругнется. Общий посыл: не нужно лезть в DOM. Он прав, обычно такой необходимости нет. А для нашего случая как раз и созданы refs.

В качестве "итого" на этот раз, вам необходимо прочитать первые 3 абзаца из раздела Uncontrolled Components. Сейчас, все должно быть понятно!

За этот урок, мы научились с вами не вызывать дорогой setState и render на "каждый чих".

P.S. конечно, в данном случае никакого выигрыша в производительности нет. Оба подхода хорошо сработают.

Вариант с контролируемыми и неконтролируемыми компонентами, работа с defaultValue и state являются одинаковыми для всех элементов форм.

Очень рекомендую посмотреть страницу документации на англ.языке по элементам форм

Исходный код на данный момент (включая alert и console.log).

Last updated