Практическая работа 5
Все привычные методы для запуска проекта и тестирования остаются с нами без изменений.
Для дополнительной помощи в разработке на React поставьте плагин React Developer Tools для браузера.
Для дополнительной помощи в разработке на Redux поставьте плагин Redux DevTools
Задача 1
Напишите компонент Accordion
, который принимает список панелей с заголовками и контентов, и создает.. ну.. аккордеон:
https://codepen.io/taniarascia/pen/BpwOKZ
Компонент принимает одну проперти tabs
- массив объектов с двумя полями: header
и content
.
Структура HTML которую он должен рендерить уже записана в методе render()
компонента.
Вначале все табы должны быть закрыты (класс d.none
на всех card-body
)
При клике на заголовок любой табы он должен стать выделенным (класс active
на card-header
),
а контент табы - открыться (класс d-none
должен отсутствовать на card-body
).
Может быть открыто несколько таб одновременно.
Вам будет гораздо проще решить задание если вы заведете дополнительный компонент TabItem
,
представляющий одну табу (заголовок + контент)
Задача 2
Напишите компонент Calculator
, позволяющий ввести два числа и произвести с ними арифметические операции.
Внешний вид калькулятора уже готов в методе render()
компонента.
При нажатии на кнопки add/subtract/multiply/divide в поле result должен появиться ответ.
Поля ввода позволяют вводить только цифры, если пользователь пытается ввести букву - это должно игнорироваться.
Попытки ввода в поле результата тоже должны игнорироваться.
Кнопка "Clear" должна, как нетрудно догадаться, сбрасывать все поля ввода в значение 0
.
Опять же, вам будет проще справиться с заданием если вы выделите два очень простых компонента - Button
и Input
,
делегировав им задачи ввода данных и обработки нажатий на кнопку, а компоненту Calculator
останется только разобраться с общим каркасом верстки, хранением состояния и арифметикой второго класса.
Задача 3
Напишите компонент Tabs
, который работает.. ну.. как табы.
Однако кроме того чтобы просто рендерить список заголовков и переключать контент,
он должен позволять настраивать вид заголовков и контента.
Tabs
принимает три проперти:
-
tabs
- структура такого же формата как и в задаче 1; -
headerTpl
- функция, которая принимает в качестве аргумента объект с полямиitem
(одна таба изtabs
) иindex
(порядковый номер табы) и возвращает элементReact
, описывающий внешний вид заголовка табы. Например:tab =>
${tab.index}. ${tab.item.header}`` - выведет не только заголовок табы, но и ее индекс -
contentTpl
- функция, которая принимает в качестве аргумента такой же объект как иheaderTpl
, и возвращает элементReact
, описывающий внешний вид контента табы. Например:tab => <h4>{tab.item.content}</h4>
- выведет контент в тегахh4
.
Пропишите defaultProps
компонента так, чтобы можно было опустить проперти headerTpl
и contentTpl
.
В этом случае должен рисоваться просто tab.item.header
для headerTpl
и tab.item.content
для contentTpl
.
На странице компонент Tabs
вызывается три раза с разыми значениями headerTpl
и contentTpl
.
Исходный HTML как всегда прописан в методе render()
.
Сначала пропишите логику переключения табов (класс active
для заголовков и класс d-none
для невидимых таб),
затем добавьте в ваш JSX вызов функций headerTpl
и contentTpl
.
Задача 4 (*)
Напишите виджет прогноза погоды. Виджет состоит двух частей: краткого прогноза на ближайшие 7 дней, и подробного прогноза на один день, который открывается если пользователь нажимает на один из дней в кратком прогнозе.
Разбиение на компоненты и черновая верстка уже готовы, осталось всего ничего: вызов API для получения данных, подстановка этих данных в верстку и управление состоянием с помощью Redux.
Подстановка данных
Начните с подстановки реальных данных в верстку.
Для этого напрямую возьмите данные из вспомогательного файла api/data-generator
.
Он экспортирует две переменных: daysShort
- массив с кратким прогнозов для семи дней,
и days
- объект вида dt
-> forecast
, представляющий полный прогноз по конкретному дню.
Заметьте, что мы испольузем dt
(timestamp) в качестве уникального идентификатора дня.
Форматировать даты удобно с помощью метода Date::toLocaleDateString
.
Также вам придется округлять температуру, подставлять иконки соответствующие погоде и их описания.
Изменить нужно компоненты WeatherDay
и WeatherDetails
; компонент Weather
пока что не требует изменений.
Redux: общий вид хранилища
У нас будет следующая структура Redux Store:
{
dayForecast: {
'1523912400000': {
data: {
dt: 1523912400000,
temp: {
min: 6.78,
max: 21.71
},
. . . // структура полного прогноза погоды по дню
},
loading: false, // прогноз погоды по этому дню уже загрузился
error: false // не было ошибок при загрузке
},
'1523998800000': {
data: null, // если была неудачная попытка загрузить, то null
loading: false,
error: true // была ошибка при загрузке
},
'1524085200000': {
data: null,
loading: true, // запрос послан, но еще не пришел
error: false
}
},
weekForecast: [
. . . // структура краткого прогноза погоды по 7 дням
],
selectedDt: 1524085200000, // timestamp выбранного дня. Если ничего не выбрано, то null
weekLoading: false, // Загружается ли краткий прогноз погоды
weekError: false // Была ли ошибка при загрузке краткого прогноза погоды
}
Как видно, мы собираемся обрабатывать статус загрузки и ошибки при загрузке для краткого прогноза погоды на неделю,
а также для полного прогноза погоды для каждого отдельного дня.
Экшены и редьюсеры для загрузки полного прогноза по дням уже готовы, внимательно ознакомьтесь с ними (actions/day-forecast
, reducers/day-forecast
)
Weather
Компонент - подключите поля
weekLoading
,weekError
иweekForecast
из хранилища Redux к компонентуWeather
(функцияmapStateToProps
) - дополните метод
render
обработкой состояния загрузки данных и состояния ошибки Во время загрузки данных компонент должен содержать следующую верстку:
<div className="weather">
<span className="fa fa-spinner fa-spin"></span>
</div>
При ошибке мы должны отобразить текст и кнопку для перезагрузки данных (метод onClick
добавится позже):
<div className="weather">
<div className="error">Error occurred during data fetch. Try to <button>reload</button></div>
</div>
- проверьте работу компонента в каждом из состояний. Это можно сделать, изменяя значения по умолчанию в каждом из редьюсеров
reducers/week-forecast
- напишите экшены для загрузки краткого прогноза погоды в
actions/week-forecast
, по аналогии с экшенами изday-forecast
. Здесь нужно использовать функциюgetWeekForecast
изapi/index
, которая оборачивает в промис соответствующие данные из генератора данных - напишите редьюсеры краткого прогноза погоды в
reducers/week-forecast
, ни в коем случае не используя аналогию с редьюсерами вday-forecast
: ваши редьюсеры получатся гораздо более простыми и не должны использоватьObject.assign
, spread или другую магию - подключите экшен
fetchWeekForecast
к компонентуWeather
(функцияmapDispatchToProps
) - добавьте вызов этого экшена в lifecycle-метод
componentDidMount
(это неплохое место чтобы начать загрузку данных), и в обработчикonClick
кнопки "Reload" - напишите
propTypes
для полей и экшенов привязанных к компоненту - удалите импорт
daysShort
из генератора данных и замените эту переменную в методеrender
на значение из Redux
Теперь компонент Weather
полностью готов, он должен отображать спиннер при загрузке,
сообщение об ошибке в случае неудачной загрузки данных, и данные из генератора данных если загрузка оказалась успешной.
При нажатии кнопки "Reload" должна выполниться повторная попытка загрузки данных.
WeatherDay
Компонент Этот компонент отвечает за отображение краткого прогноза по одному дню.
Также он должен иметь класс active
, если его день соответствует выбранному для подробного отображения в WeatherDetails
(selectedDt
в хранилище)
При клике на компонент он должен изменять день, выбранный для отображения в WeatherDetails
- подключите поле
selectedDt
из хранилища к компоненту (mapStateToProps
) - дополните метод
render
: еслиselectedDt
совпадает с полемdt
отображаемого дня, компонент должен иметь дополнительный css-классactive
- напишите тело action creator'а
openDayDetails
(actions/day-forecast
). Он должен отправлять экшен типаOPEN_DAY_DETAILS
, и содержатьdt
выбранного дня. ЕслиopenDayDetails
вызывается с тем жеdt
что и текущее значениеselectedDt
в хранилище, то полеdt
нужно установить вnull
- таким образом, при повторном выборе одного и того же дня мы убираем этот день из подробного просмотра. - напишите очень простой редьюсер
selectedDt
(reducers/day-forecast
), который по экшенуOPEN_DAY_DETAILS
устанавливает значениеselectedDt
. - подключите action creator
openDayDetails
к компонентуWeatherDay
(mapDispatchToProps
) - добавьте вызов этого экшена в обработчик клика на компоненте
- напишите
propTypes
для полей и экшенов привязанных к компоненту
Теперь компонент WeatherDay
полностью готов, он должен добавлять и убирать класс active
при нажатии на него.
WeatherDetails
Компонент Компонент отвечает за отображение подробного прогноза по отдельному дню. При этом если в нашем хранилище еще нет данных по этому дню, то нужно их запросить; если же они уже загружены - то нужно использовать эти данные.
- подключите поле
selectedDt
и полеforecast
, содержащее полный прогноз по текущему дню и поляloading/error
. Вам нужно получить его из объектаdayForecast
по ключуselectedDt
. - дополните метод
render
обработкой различных состояний компонента. ЕслиselectedDt === null
, это значит что ни один день не выбран в верхнем списке, и компонент ничего не должен отображать. Во время загруки данных (forecast.loading
) нужно вывести крутящийся спиннер:
<div className="details">
<span className="fa fa-spinner fa-spin"></span>
</div>
Если произошла ошибка (forecast.error
) нужно вывести дополнительную кнопку "Reload":
<div className="details">
<div className="error">Error occurred during data fetch. Try to <button>reload</button></div>
</div>
Иначе выводятся данные полного прогноза погоды из объекта forecast.data
.
- подключите экшен
fetchDayForecast
к компоненту. Использовать lifecycle-методcomponentDidMount
не удастся, т.к. наш компонент отображает различные данные в зависимости отselectedDt
, и компонент при этом не пересоздается. Поэтому добавьте вызов этого экшена в методcomponentDidUpdate
, в который React передает старыеprops
после ререндера компонента. Проверьте чтоselectedDt
действительно был изменен при апдейтеprops
, и вызовите подключенный экшен. Также добавьте вызов этого экшена в обработчик клика на кнопке "Reload". - удалите импорт
data-generator
- напишите
propTypes
для полей и экшенов привязанных к компоненту
Теперь компонент WeatherDetails
полностью готов, должен отображать спиннер/ошибки загрузки, и загруженные данные
полного прогноза погоды по выбранному дню.
Итоги подведем
Все готово. Вы восхитительны!
Задача 5 (**)
Добавьте к задаче 4 дополнительный компонент - ErrorLogger
, который отображает список ошибок, возникших при загрузке данных.
Заведите дополнительный редьюсер, который будет отслеживать поле error
в любых экшенах, и складывать все ошибки в отдельный массив errors
в хранилище.
Новой ошибке при этом должен присваиваться новый уникальный id
, по которому ее затем можно будет найти.
Массив errors
должен быть связан с компонентом ErrorLogger
, который выводит список ошибок с помощью
bootstrap alerts: https://getbootstrap.com/docs/4.0/components/alerts/
У каждого алерта при этом должна быть кнока "close", по которой она должна удаляться из списка ошибок.
Не стоит использовать для этого jquery-компоненты бутстрапа; лучше взять у бутстрапа только верстку и css.
Для удаления ошибки из списка напишите новый экшен DELETE_ERROR
и редьюсер для него, который будет удалять ошибку с заданным id
из списка errors
.