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'));
Как только на элементе формы 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'));
Обратите внимание, что 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'));
В целом, это делает поведение тегов <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'));
Обратите внимание на то, как мы использовали синтаксис 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 библиотекой. В таких ситуациях вам,
возможно, стоит рассмотреть неконтролируемые компоненты,
как альтернативный метод реализации форм ввода.