2.10 Формы


Работа элементов HTML-форм в React немного отличается от работы других DOM-элементов. Это связано с тем, что элементы форм по своей природе обладают некоторым внутренним состоянием. К примеру, данная форма в нативном HTML принимает только имя:


Код
        
  <form>
    <label>
      Name: <input type="text" name="name" />
    </label>
    <input type="submit" value="Submit" />
  </form>
    

Представленная форма имеет поведение HTML-формы по умолчанию: просмотр новой страницы, когда пользователь посылает форму. Если такое поведение вам необходимо и в React, то оно работает как обычно. Но в большинстве случаев нам удобно иметь JavaScript-функцию, которая имеет доступ к данным, которые пользователь ввел в форму и обрабатывает её отправку. Для этой цели, есть стандартный подход, под названием «контролируемые компоненты».




По умолчанию в HTML элементы формы, такие как <input>, <textarea> и <select>, хранят свое собственное состояние и обновляют его на основании пользовательского ввода. Но в React модифицируемое состояние, как правило, является собственностью компонентов и обновляется только с помощью setState().

Мы можем скомбинировать обе эти особенности, делая состояние React “единственным источником достоверной информации (истины)”. В свою очередь React-компонент, который отрисовывает форму, также контролирует, что происходит на этой форме в ответ на последующий ввод пользователя. Элемент ввода формы (например, input), значение которого контролируется React, в этом случае называется «контролируемый компонент».

К примеру, если в предыдущем примере мы хотим делать лог имени, когда форма отправляется, мы можем написать форму как контролируемый компонент:


Код
        
  class LoginForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {login: '', password: ''};

      this.onChangeLogin = this.onChangeLogin.bind(this);
      this.onChangePassword = this.onChangePassword.bind(this);
      this.onSubmit = this.onSubmit.bind(this);
    }

    onSubmit(event){
      alert(`${this.state.login}, добро пожаловать!`);
      event.preventDefault();
    }

    onChangePassword(event){
      this.setState({password: event.target.value});
    }

    onChangeLogin(event) {
      this.setState({login: event.target.value});
    }

    render() {
      return (
        <form onSubmit={this.onSubmit}>
          <p><label> Логин: <input type="text" name="login" value={this.state.login}
                           onChange={this.onChangeLogin}/></label></p>
          <p><label> Пароль: <input type="password" name="password" value={this.state.password}
                            onChange={this.onChangePassword}/></label></p>
          <p><input type="submit" value="Submit" /></p>
        </form>
      );
    }
  }

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

Посмотреть в CodePen


Как только на элементе формы c name="login" изменяется атрибут value, срабатывает onChangeLogin, изменяя значение состояния компонента this.state.login. Далее происходит перерисовка. Таким образом отображаемое значение всегда будет равно this.state.login, делая состояние React единственным источником достоверной информации. Когда пользователь что-нибудь печатает, обработчик onChangeLogin срабатывает на каждое нажатие клавиши, изменяя состояние компонента, что в свою очередь приводит к обновлению значения на экране.



В подходе «контролируемый компонент», любая модификация состояния имеет соответствующий обработчик. Это делает простым изменение или проверку данных, вводимых пользователем. К примеру, если мы хотим, чтобы логин вводился только в верхнем регистре, мы можем написать onChangeLogin как:


Код
        
  onChangeLogin(event) {
     this.setState({login: event.target.value.toUpperCase()});
  }
    




В нативном HTML элемент <textarea> определяет введенный в него текст по его потомкам:


Код
        
  <textarea>
    Дорогие посетители сайта! Желаем вам приятного изучения React.
  </textarea>
    

В React элемент <textarea> вместо потомков использует значение атрибута value и в коде ничем не отличается однострочного элемента input:

Код
        
  class MessageForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {email: '', message: 'Текст сообщения'};

      this.onChangeEmail = this.onChangeEmail.bind(this);
      this.onChangeMessage = this.onChangeMessage.bind(this);
      this.onSubmit = this.onSubmit.bind(this);
    }

    onSubmit(event){
      alert(`Сообщение успешно отправлено получателю "${this.state.email}"`);
      event.preventDefault();
    }

    onChangeMessage(event){
      this.setState({message: event.target.value});
    }

    onChangeEmail(e) {
      this.setState({email: e.target.value});
    }

    render() {
      return (
        <form onSubmit={this.onSubmit}>
          <p><label> email получателя: <input type="text" name="email" value={this.state.email}
                           onChange={this.onChangeEmail}/></label></p>
          <p><label>Текст сообщения: <textarea type="text" name="message" value={this.state.message}
            onChange={this.onChangeMessage}/></label></p>
          <p><input type="submit" value="Submit" /></p>
        </form>
      );
    }
  }

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

Посмотреть в CodePen


Обратите внимание, что this.state.value инициализируется в конструкторе, поэтому textarea показывается уже с некоторым текстом.




В нативном HTML тег <select> создает выпадающий список. К примеру данный HTML создает выпадающий список языков программирования:


Код
        
  <select>
    <option value="C++">C++</option>
    <option value="Java">Java</option>
    <option value="C#">C#</option>
    <option selected value="JavaScript">JavaScript</option>
    <option value="Scala">Scala</option>
  </select>
    

Обратите внимание, что по умолчанию выбрана опция “JavaScript”, так как задан атрибут selected. React вместо атрибута selected, использует атрибут value на корневом теге select. В контролируемом компоненте это удобнее, потому что этот атрибут нужно обновлять только в одном месте. Например:


Код
        
  class LanguageForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {language: 'JavaScript'};

      this.onChangeSelect = this.onChangeSelect.bind(this);
      this.onSubmit = this.onSubmit.bind(this);
    }

    onChangeSelect(event) {
      this.setState({language: event.target.value});
    }

    onSubmit(event) {
      alert(`Вы выбрали язык: ${this.state.language}`);
      event.preventDefault();
    }

    render() {
      return (
        <form onSubmit={this.onSubmit}>
          <label>
            Выберите язык программирования:
            <select value={this.state.language} onChange={this.onChangeSelect}>
              <option value="C++">C++</option>
              <option value="Java">Java</option>
              <option value="C#">C#</option>
              <option value="JavaScript">JavaScript</option>
              <option value="Scala">Scala</option>
            </select>
          </label>
          <input type="submit" value="Submit" />
        </form>
      );
    }
  }

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

Посмотреть в CodePen


В целом, это делает поведение тегов <input type="text">, <textarea> и <select> очень похожим – они все принимают атрибут value, который вы можете использовать, чтобы реализовать контролируемый компонент.

Также в атрибут value вы можете передать массив. Это позволяет выбрать в теге select сразу несколько опций.


Код
        
  <select multiple={true} value={['B', 'C']}>
    




В нативном HTML тег <input type="file"/> позволяет пользователю выбирать один или несколько файлов из хранилища своего устройства для загрузки на сервер, а также манипулировать собой с помощью JavaScript через File API.


Код
        
  <input type="file" />
    

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




Когда вам нужно обрабатывать множество контролируемых элементов input, вы можете добавить атрибут name на каждый элемент и позволить функции-обработчику выбрать, что делать, на основании значения event.target.name.

Например:


Код
        
  class PersonForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {sex: 'female', firstName: '', lastname: '', email: '', phone: ''};
      this.onChangeInput = this.onChangeInput.bind(this);
    }

    onChangeInput(event) {
      const name = event.target.name;
      this.setState({[name]: value});
    }

    render() {
      return (
        <form>
          <label>First Name: <input name="firstName"  type="text"
                               value={this.state.firstName} onChange={this.onChangeInput}/></label>
          <label> Last Name: <input name="lastName"  type="text"
                               value={this.state.lastName} onChange={this.onChangeInput}/></label>
          <label> Email: <input name="email"  type="email"
                               value={this.state.email} onChange={this.onChangeInput}/></label>
          <label> Phone: <input name="phone"  type="tel"
                               value={this.state.phone} onChange={this.onChangeInput}/></label>
          <label> Sex: <select name="sex"  value={this.state.sex} onChange={this.onChangeInput}>
              <option value="male">Male</option>
              <option value="female">Female</option>
            </select>
          </label>
        </form>
      );
    }
  }

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

Посмотреть в CodePen


Обратите внимание на то, как мы использовали синтаксис ES6 вычисляемого имени свойства, чтобы обновить ключ состояния в соответствии с данным именем тега input:


Код
        
  this.setState({
    [name]: value
  });
    

Это эквивалент данного ES5 кода:


Код
        
  var partialState = {};
  partialState[name] = value;
  this.setState(partialState);
    

Поскольку setState() делает слияние частичного состояния в текущее автоматически, нам лишь нужно вызывать его с изменившейся частью.




Если вы укажете значение этого атрибута на контролируемом компоненте, то пользователь не сможет его изменять. Если вы указали value, но input все еще редактируемый, вы могли случайно установить value в undefined или null.

Следующий код демонстрирует это. (Сначала input заблокирован, но становится редактируемым после короткой задержки.)


Код
        
  ReactDOM.render(<input value="hi" />, mountNode);

  setTimeout(function() {
    ReactDOM.render(<input value={null} />, mountNode);
  }, 1000);
    




Для многих использовать контролируемые компоненты бывает утомительно. Ведь вам необходимо написать обработчик для каждого случая изменения ваших данных и пропустить все состояния input-ов через React-компонент. Это может сильно раздражать, когда вы переписываете предшествующую кодовую базу на React, или интегрируете React-приложение с не-React библиотекой. В таких ситуациях вам, возможно, стоит рассмотреть неконтролируемые компоненты, как альтернативный метод реализации форм ввода.