Учебный курс по React, часть 25: практикум по работе с формами +20


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

image

> Часть 1: обзор курса, причины популярности React, ReactDOM и JSX
> Часть 2: функциональные компоненты
> Часть 3: файлы компонентов, структура проектов
> Часть 4: родительские и дочерние компоненты
> Часть 5: начало работы над TODO-приложением, основы стилизации
> Часть 6: о некоторых особенностях курса, JSX и JavaScript
> Часть 7: встроенные стили
> Часть 8: продолжение работы над TODO-приложением, знакомство со свойствами компонентов
> Часть 9: свойства компонентов
> Часть 10: практикум по работе со свойствами компонентов и стилизации
> Часть 11: динамическое формирование разметки и метод массивов map
> Часть 12: практикум, третий этап работы над TODO-приложением
> Часть 13: компоненты, основанные на классах
> Часть 14: практикум по компонентам, основанным на классах, состояние компонентов
> Часть 15: практикумы по работе с состоянием компонентов
> Часть 16: четвёртый этап работы над TODO-приложением, обработка событий
> Часть 17: пятый этап работы над TODO-приложением, модификация состояния компонентов
> Часть 18: шестой этап работы над TODO-приложением
> Часть 19: методы жизненного цикла компонентов
> Часть 20: первое занятие по условному рендерингу
> Часть 21: второе занятие и практикум по условному рендерингу
> Часть 22: седьмой этап работы над TODO-приложением, загрузка данных из внешних источников
> Часть 23: первое занятие по работе с формами
> Часть 24: второе занятие по работе с формами
> Часть 25: практикум по работе с формами

Занятие 43. Практикум. Работа с формами


> Оригинал

?Задание


На этом практическом занятии вам предлагается привести в работоспособное состояние код компонента App, который находится в файле App.js стандартного проекта, создаваемого средствами create-react-app. Вот этот код:

import React, {Component} from "react"

class App extends Component {
    constructor() {
        super()
        this.state = {}
    }
    
    render() {
        return (
            <main>
                <form>
                    <input placeholder="First Name" /><br />
                    <input placeholder="Last Name" /><br />
                    <input placeholder="Age" /><br />
                    
                    {/* Здесь создайте переключатели для выбора пола */}
                    <br />
                    
                    {/* Здесь создайте поле со списком для выбора пункта назначения */}
                    <br />
                    
                    {/* Здесь создайте флажки для указания диетологических ограничений */}
                    <br />
                    
                    <button>Submit</button>
                </form>
                <hr />
                <h2><font color="#3AC1EF">Entered information:</font></h2>
                <p>Your name: {/* Имя и фамилия */}</p>
                <p>Your age: {/* Возраст */}</p>
                <p>Your gender: {/* Пол */}</p>
                <p>Your destination: {/* Пункт назначения */}</p>
                <p>
                    Your dietary restrictions: 
                    {/* Список диетологических ограничений */}
                </p>
            </main>
        )
    }
}

export default App

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

Вот как выглядит то, что компонент выводит сейчас на экран.


Приложение в браузере

?Решение


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

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

this.state = {
    firstName: "",
    lastName: "",
    age: 0,
    gender: "",
    destination: "",
    dietaryRestrictions: []
}

Нужно учитывать то, что в процессе работы над программой может выясниться, что, например, состояние будет удобнее инициализировать иначе. Если мы столкнёмся с чем-то подобным — мы изменим код инициализации состояния. В частности, сейчас некоторые сомнения может вызывать число 0, записанное в свойство age, в котором предполагается хранить возраст, введённый пользователем. Возможно, иначе нужно будет поступить и с системой хранения данных флажков, которая сейчас представлена свойством dietaryRestrictions, инициализированным пустым массивом.

Теперь, после инициализации состояния, займёмся настройкой элементов управления. Так как в коде уже есть описание полей ввода — приступим к работе с ними.

Этим элементам управления понадобится дать имена, настроив их атрибуты name таким образом, чтобы они совпадали с именами свойств состояния, в которых будут храниться данные, введённые в эти поля. У них должен быть атрибут value, значение которого определяется на основе данных, хранящихся в состоянии. При вводе данных в каждое из этих полей нужно передавать введённые данные компоненту, что приводит к необходимости наличия у них обработчика события onChange. Все эти рассуждения приводят к тому, что описание полей выглядит теперь так:

<input 
    name="firstName" 
    value={this.state.firstName} 
    onChange={this.handleChange} 
    placeholder="First Name" 
/>
<br />

<input 
    name="lastName" 
    value={this.state.lastName}
    onChange={this.handleChange} 
    placeholder="Last Name" 
/>
<br />

<input 
    name="age" 
    value={this.state.age}
    onChange={this.handleChange} 
    placeholder="Age" 
/>

В качестве метода, используемого для обработки событий onChange этих полей, указан несуществующий пока this.handleChange. Создадим этот метод:

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

Здесь мы извлекаем из объекта event.target свойства name и value, после чего используем их для установки соответствующего свойства состояния. В данный момент нас такой код универсального обработчика событий устроит, но позже, когда мы доберёмся до работы с флажками, мы внесём в него изменения.

Не забудем о привязке this, выполняемой в конструкторе компонента:

this.handleChange = this.handleChange.bind(this)

Для того чтобы добиться вывода в нижней части страницы данных, вводимых в поля firstName, secondName и age, поработаем с соответствующими элементами <p>, приведя их к следующему виду:

<p>Your name: {this.state.firstName} {this.state.lastName}</p>
<p>Your age: {this.state.age}</p>

Теперь взглянем на то, что у нас получилось.


Приложение в браузере

Как видно, в поле, предназначенном для ввода возраста, не выводится подсказка. Вместо этого выводится то, что задано в свойстве состояния age, то есть — 0. Нам же нужно, чтобы в незаполненном поле выводилась бы подсказка. Попробуем заменить значение age в состоянии на null. После этого оказывается, что форма выглядит так, как нужно, но в консоли выводится следующее предупреждение, касающееся поля age:

Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components

В результате нам нужно будет заменить значение свойства состояния age на пустую строку, приведя код инициализации состояния к следующему виду:

this.state = {
    firstName: "",
    lastName: "",
    age: "",
    gender: "",
    destination: "",
    dietaryRestrictions: []
}

Теперь испытаем форму. Сразу после открытия она будет выглядеть так же, как и в самом начале работы, то есть — в поле age вернётся подсказка. При заполнении полей введённые данные будут выводиться в нижней части страницы.


Приложение в браузере

Как видно, на данном этапе работы всё функционирует так, как ожидается.

Теперь займёмся новыми элементами. Следующим шагом работы над формой будет добавление на неё переключателей.

Обернём переключатели в тег <label>, что позволит не только подписать переключатель, но и сделать так, чтобы щелчок по этой подписи, то есть — по его родительскому элементу, приводил бы к его выбору.

Работая с переключателями стоит помнить о том, что они представляют собой нечто вроде комбинации флажков, имеющих атрибут checked, и текстовых полей, у которых есть атрибут value. Переключатели формируют группу, в которой каждому из переключателей назначают одно и то же имя, при этом свойство переключателей checked устанавливается по условию, настроенному так, чтобы нельзя было бы включить больше одного переключателя, входящего в одну и ту же группу. В качестве обработчика событий onChange переключателей назначим this.handleChange.

В результате код описания переключателей будет выглядеть так:

<label>
    <input 
        type="radio" 
        name="gender"
        value="male"
        checked={this.state.gender === "male"}
        onChange={this.handleChange}
    /> Male
</label>

<br />

<label>
    <input 
        type="radio" 
        name="gender"
        value="female"
        checked={this.state.gender === "female"}
        onChange={this.handleChange}
    /> Female
</label>

Теперь переработаем соответствующий элемент <p>, расположенный в нижней части страницы, следующим образом:

<p>Your gender: {this.state.gender}</p>

После этого форму можно испытать. Сразу после запуска оба переключателя оказываются невыбранными, так как в состоянии хранится значение, которое не позволяет ни одной из проверок, выполняемых при установке их свойства checked, выдать true. После щелчка по одному из них в состояние попадает соответствующее ему значение (хранящееся в атрибуте value переключателя), переключатель оказывается выбранным, соответствующий текст выводится в нижней части формы.


Приложение в браузере

Теперь займёмся работой над полем со списком. Его заготовка выглядит так:

<select>
    <option></option>
    <option></option>
    <option></option>
    <option></option>
</select>

Из этого кода видно, что мы планируем описать поле со списком, содержащим четыре элемента.

У тега <select> и у находящихся в нём тегов <option> есть атрибут value. Однако эти атрибуты несут различный смысл. То значение value, которое назначают элементу <option>, указывает на то, каким должно быть соответствующее свойство состояния при выборе данного элемента. Сюда попадают те строки, которые должны находиться в выпадающем списке. В нашем случае — это некие пункты назначения, например — страны. Запишем их названия с маленькой буквы для того, чтобы их внешний вид соответствовал бы значениям свойств value других имеющихся в коде элементов. После этого код поля со списком будет выглядеть так:

<select value=>
    <option value="germany">Germany</option>
    <option value="norway">Norway</option>
    <option value="north pole">North Pole</option>
    <option value="south pole">South Pole</option>
</select>

Если же говорить об атрибуте value тега <select>, то тут будет указано не некое жёстко заданное значение, а ссылка на соответствующее свойство состояния:

<select value={this.state.destination}>
    <option value="germany">Germany</option>
    <option value="norway">Norway</option>
    <option value="north pole">North Pole</option>
    <option value="south pole">South Pole</option>
</select>

Назначим полю другие атрибуты. В частности — имя, соответствующее имени свойства в состоянии, и обработчик события onChangethis.handleChange.

<select 
    value={this.state.destination} 
    name="destination" 
    onChange={this.handleChange}
>
    <option value="germany">Germany</option>
    <option value="norway">Norway</option>
    <option value="north pole">North Pole</option>
    <option value="south pole">South Pole</option>
</select>

Теперь настроим описание элемента <p>, в который будет выводиться то, что выбрано в поле destination:

<p>Your destination: {this.state.destination}</p>

Если прямо сейчас взглянуть на страницу в браузере, можно увидеть, что в поле автоматически выбран первый элемент списка, но это, очевидно, не приводит к обновлению состояния, так как в строке Your destination: после двоеточия ничего не выводится.


Приложение в браузере

Для того чтобы в состояние попало бы значение germany, нужно, открыв поле со списком, сначала выбрать что-нибудь другое, а потом уже — пункт Germany.

Часто для того чтобы учесть эту особенность полей со списками, в их списках, в качестве первого элемента, помещают нечто вроде элемента с пустым значением и с текстом наподобие -- Please Choose a destination --. В нашем случае это может выглядеть так:

<select 
    value={this.state.destination} 
    name="destination" 
    onChange={this.handleChange}
>
    <option value="">-- Please Choose a destination --</option>
    <option value="germany">Germany</option>
    <option value="norway">Norway</option>
    <option value="north pole">North Pole</option>
    <option value="south pole">South Pole</option>
</select>

Мы остановимся именно на этом варианте настройки поля со списком.

Теперь займёмся, возможно, самой сложной частью нашей задачи, которая связана с флажками. Тут стоит пояснить то, что свойство состояния dietaryRestrictions, которое планируется использовать для работы с флажками, было инициализировано пустым массивом. Теперь, когда дело дошло до работы с элементами управления, возникает такое ощущение, что лучше будет представить это поле в виде объекта. Так будет удобнее работать с сущностями, представляющими отдельные флажки в виде свойств этого объекта с понятными именами, а не в виде элементов массива. Свойства объекта, который теперь будет представлен свойством состояния dietaryRestrictions, будут содержать логические значения, указывающие на то, сброшен ли соответствующий флажок (false) или установлен (true). Теперь код инициализации состояния будет выглядеть так:

this.state = {
    firstName: "",
    lastName: "",
    age: "",
    gender: "",
    destination: "",
    dietaryRestrictions: {
        isVegan: false,
        isKosher: false,
        isLactoseFree: false
    }
}

Как видно, мы планируем создать три флажка. Все они, сразу после загрузки страницы, будут сброшены.

Опишем флажки в коде, возвращаемом компонентом, обернув их в теги <label> и настроив их атрибуты. Вот как будет выглядеть их код:

<label>
    <input 
        type="checkbox"
        name="isVegan"
        onChange={this.handleChange}
        checked={this.state.dietaryRestrictions.isVegan}
    /> Vegan?
</label>
<br />

<label>
    <input 
        type="checkbox"
        name="isKosher"
        onChange={this.handleChange}
        checked={this.state.dietaryRestrictions.isKosher}
    /> Kosher?
</label>
<br />

<label>
    <input 
        type="checkbox"
        name="isLactoseFree"
        onChange={this.handleChange}
        checked={this.state.dietaryRestrictions.isLactoseFree}
    /> Lactose Free?
</label>

В качестве имён флажков использованы имена свойств объекта dietaryRestrictions, а в качестве значений их атрибутов checked — конструкции вида this.state.dietaryRestrictions.isSomething.

Обратите внимание на то, что, хотя в качестве обработчика событий флажков onChange указан уже имеющийся у нас обработчик this.handleChange, мы должны, для обеспечения правильной работы программы, внести в него некоторые изменения.

Взглянем на приложение.


Приложение в браузере

Как видно, флажки на страницу выведены, но компонент пока не содержит всех механизмов, необходимых для обеспечения их правильной работы. Займёмся обработчиком событий.

Здесь нам, для работы с флажками, понадобится извлечь из объекта event.target, в дополнение к уже извлечённым, свойства type и checked. Первое нужно для проверки типа элемента (флажки имеют тип, представленный строкой checkbox), второе — для того чтобы выяснить, установлен флажок или снят. Если оказывается, что обработчик был вызван после взаимодействия пользователя с флажком, используем особую процедуру установки состояния. События других элементов управления будем обрабатывать так же, как и прежде.

При обновлении состояния, нужно учитывать то, что React — система достаточно интеллектуальная, которая, если обновляют лишь часть состояния, автоматически скомбинирует в новом состоянии то, что осталось неизменным, с тем, что изменилось. Но при этом нельзя быть уверенным в том, что так же будет осуществляться и работа со свойствами объектов, которые представляют собой значения свойств состояния. Проверим это, приведя код handleChange к следующему виду. Тут мы исходим из предположения о том, что свойства объекта dietaryRestrictions вполне можно менять по одному:

handleChange(event) {
    const {name, value, type, checked} = event.target
    type === "checkbox" ? 
        this.setState({
            dietaryRestrictions: {
                [name]: checked
            }
        })
    :
    this.setState({
        [name]: value
    }) 
}

Если открыть страницу приложения в браузере, то, сразу после её загрузки, всё будет выглядеть нормально, при попытке, например, ввести имя в поле First Name, всё тоже будет работать как и прежде, но при попытке установки одного из флажков будет выдано следующее предупреждение:
Warning: A component is changing a controlled input of type checkbox to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: fb.me/react-controlled-components

Для того чтобы правильно обновлять содержимое объекта dietaryRestrictions, можно воспользоваться функциональной формой setState, самостоятельно сформировав новую версию состояния. Если бы нам надо было бы управлять большим количеством флажков, то, вероятно, мы так бы и сделали. Но тут мы поступим иначе. А именно, сделаем свойства объекта dietaryRestrictions свойствами состояния, избавившись от этого объекта:

this.state = {
    firstName: "",
    lastName: "",
    age: "",
    gender: "",
    destination: "",
    isVegan: false,
    isKosher: false,
    isLactoseFree: false
}

Теперь поменяем настройки атрибутов флажков, избавившись от dietaryRestrictions:

<label>
    <input 
        type="checkbox"
        name="isVegan"
        onChange={this.handleChange}
        checked={this.state.isVegan}
    /> Vegan?
</label>
<br />

<label>
    <input 
        type="checkbox"
        name="isKosher"
        onChange={this.handleChange}
        checked={this.state.isKosher}
    /> Kosher?
</label>
<br />

<label>
    <input 
        type="checkbox"
        name="isLactoseFree"
        onChange={this.handleChange}
        checked={this.state.isLactoseFree}
    /> Lactose Free?
</label>

И, наконец, отредактируем код элемента, выводящего сведения о диетологических ограничениях, указанных пользователем:

<p>Your dietary restrictions:</p>

<p>Vegan: {this.state.isVegan ? "Yes" : "No"}</p>
<p>Kosher: {this.state.isKosher ? "Yes" : "No"}</p>
<p>Lactose Free: {this.state.isLactoseFree ? "Yes" : "No"}</p>

После этого проверим работоспособность приложения.


Приложение в браузере

Как видно, всё работает так, как ожидается.

Вот полный код компонента App:

import React, {Component} from "react"

class App extends Component {
    constructor() {
        super()
        this.state = {
            firstName: "",
            lastName: "",
            age: "",
            gender: "",
            destination: "",
            isVegan: false,
            isKosher: false,
            isLactoseFree: false
        }
        this.handleChange = this.handleChange.bind(this)
    }
    
    handleChange(event) {
        const {name, value, type, checked} = event.target
        type === "checkbox" ? 
            this.setState({
                [name]: checked
            })
        :
        this.setState({
            [name]: value
        }) 
    }
    
    render() {
        return (
            <main>
                <form>
                    <input 
                        name="firstName" 
                        value={this.state.firstName} 
                        onChange={this.handleChange} 
                        placeholder="First Name" 
                    />
                    <br />
                    
                    <input 
                        name="lastName" 
                        value={this.state.lastName}
                        onChange={this.handleChange} 
                        placeholder="Last Name" 
                    />
                    <br />
                    
                    <input 
                        name="age" 
                        value={this.state.age}
                        onChange={this.handleChange} 
                        placeholder="Age" 
                    />
                    <br />
                    
                    <label>
                        <input 
                            type="radio" 
                            name="gender"
                            value="male"
                            checked={this.state.gender === "male"}
                            onChange={this.handleChange}
                        /> Male
                    </label>
                    
                    <br />
                    
                    <label>
                        <input 
                            type="radio" 
                            name="gender"
                            value="female"
                            checked={this.state.gender === "female"}
                            onChange={this.handleChange}
                        /> Female
                    </label>
                    
                    <br />
                    
                    <select 
                        value={this.state.destination} 
                        name="destination" 
                        onChange={this.handleChange}
                    >
                        <option value="">-- Please Choose a destination --</option>
                        <option value="germany">Germany</option>
                        <option value="norway">Norway</option>
                        <option value="north pole">North Pole</option>
                        <option value="south pole">South Pole</option>
                    </select>
                    
                    <br />
                    
                    <label>
                        <input 
                            type="checkbox"
                            name="isVegan"
                            onChange={this.handleChange}
                            checked={this.state.isVegan}
                        /> Vegan?
                    </label>
                    <br />
                    
                    <label>
                        <input 
                            type="checkbox"
                            name="isKosher"
                            onChange={this.handleChange}
                            checked={this.state.isKosher}
                        /> Kosher?
                    </label>
                    <br />
                    
                    <label>
                        <input 
                            type="checkbox"
                            name="isLactoseFree"
                            onChange={this.handleChange}
                            checked={this.state.isLactoseFree}
                        /> Lactose Free?
                    </label>
                    <br />
                    
                    <button>Submit</button>
                </form>
                <hr />
                <h2><font color="#3AC1EF">Entered information:</font></h2>
                <p>Your name: {this.state.firstName} {this.state.lastName}</p>
                <p>Your age: {this.state.age}</p>
                <p>Your gender: {this.state.gender}</p>
                <p>Your destination: {this.state.destination}</p>
                <p>Your dietary restrictions:</p>
                
                <p>Vegan: {this.state.isVegan ? "Yes" : "No"}</p>
                <p>Kosher: {this.state.isKosher ? "Yes" : "No"}</p>
                <p>Lactose Free: {this.state.isLactoseFree ? "Yes" : "No"}</p>
                
            </main>
        )
    }
}

export default App

Итоги


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

Уважаемые читатели! Скажите, сложно ли было выполнить эту практическую работу?




К сожалению, не доступен сервер mySQL