5.5 Список приёмов
Этот раздел может оказаться для вас крайне полезным. Здесь мы познакомимся с популярными мощными библиотеками и создадим базовые переиспользуемые компоненты. На основе последних, мы построим - список и форму фильтрации данных, а также организуем их правильное взаимодействие.
Прежде чем мы приступим к созданию списка приёмов, нам необходим какой-то базовый компонент, отображающий этот самый список. Его можно написать самому, а можно использовать какой-нибудь популярный плагин.
Совет!
Когда вы ищете какой-нибудь плагин React для реализации вашей задачи, всегда заходите в Git и проверяйте две важные вещи: частота обновлений и размер сообщества. Два этих фактора гарантируют, что ваш плагин будет поддерживаться, развиваться и избавляться от багов. Хорошо когда обновления происходят раз в неделю или месяц, а не год. Иначе, если вы наткнётесь на какой-то баг, исправят его не скоро, а вам придется отказаться от использования такого плагина после значительных потерь времени. Не стоит добавлять в проект непопулярные, плохо поддерживаемые плагины - лучше написать самому!Если вам не удалось найти ничего подходящего, кроме плохо поддерживаемых плагинов, и вы решили написать свой - вы всегда можете подсмотреть или перенять подходы у найденных. Просто в этом случае вы сами сможете поддерживать код и исправлять ошибки, что избавляет вас от рисков.
Сейчас перед нами стоит задача отобразить список приёмов у всех врачей с возможностью фильтрации:
Для решения этой задачи предлагаю вашему вниманию плагин . Это очень мощная библиотека, которая предоставляет таблицу с богатым ассортиментом возможностей. Чтобы познакомиться с ней достаточно посмотреть . Здесь собраны все типовые случаи использования: пагинация, сортировка, стили строк и столбцов, события и многое другое.
Совет!
Не поленитесь уделить время и пройтись по всем пунктам демо. Узнайте все возможности таблицы, чтобы не изобретать заново велосипед и понапрасну тратить своё время. Вполне вероятно, что ваш случай уже реализован.
Вы могли заметить, что в названии плагина использовано слово bootstrap. Я не случайно
порекомендовал именно этот плагин. Он является переделкой своего предшественника
react-bootstrap-table
, который в свою очередь является умной React таблицей для bootstrap.
В нашей ситуации это очень кстати. Тем не менее вы можете использовать и любой другой
плагин для отображения таблицы.
Чтобы отобразить список приёмов нам понадобится базовый компонент таблицы. Он, конечно, не является обязательным. Однако лучше всего иметь такой компонент-обёртку с заданным API.
Совет!
Все подключаемые плагины лучше оборачивать в собственные компоненты-обёртки со своим API. Такая стратегия даёт массу преимуществ. Во-первых, при необходимости плагин можно поменять на другой. Во-вторых функционал плагина можно расширить. В-третьих такие компоненты можно использовать повторно в других проектах. Компоненты-обёртки, а также компоненты, написанные с нуля и составляют вашу собственную библиотеку. Все они находятся в папке/components
.
Давайте добавим модуль react-bootstrap-table-next в проект:
Затем создадим наш первый базовый компонент-обёртку вокруг плагина:
import React, { Component } from 'react'
import cn from 'classname'
import PropTypes from 'prop-types'
import BootstrapTable from 'react-bootstrap-table-next'
import './Table.scss'
const NO_DATA_TEXT = 'Данных нет'
export default class Table extends Component {
static propTypes = {
columns: PropTypes.arrayOf(PropTypes.object), // дескрипторы столбцов таблицы
data: PropTypes.arrayOf(PropTypes.object), // данные таблицы
keyField: PropTypes.string, // имя уникального столбца
noDataText: PropTypes.string,
hasHover: PropTypes.bool,
hasOptions: PropTypes.bool,
hasBorders: PropTypes.bool,
isStriped: PropTypes.bool,
expandRow: PropTypes.object,
className: PropTypes.string,
containerClass: PropTypes.string,
getRowStyle: PropTypes.func
}
static defaultProps = {
data: [],
columns: [],
keyField: 'id',
noDataText: NO_DATA_TEXT,
isRemote: true,
isStriped: true,
isLoading: false,
hasHover: false,
hasHeader: false,
hasBorders: false,
getRowStyle: function() { return null }
}
getRowStyle = (row, rowIndex) => {
return this.props.getRowStyle(row, rowIndex)
}
render() {
const {
data,
columns,
keyField,
expandRow,
className,
containerClass,
isStriped,
hasBorders,
hasHover,
noDataText,
} = this.props
return (
<div className={cn('TableContainer', containerClass)}>
<BootstrapTable
expandRow={expandRow}
data={data}
columns={columns}
keyField={keyField}
classes={cn('Table', className)}
headerClasses={'Table-Header'}
striped={isStriped}
hover={hasHover}
bordered={hasBorders}
rowStyle={this.getRowStyle}
noDataIndication={noDataText}
/>
</div>
)
}
}
В компоненте сначала идут статические члены: propTypes
и
defaultProps
. В них мы определяем типы свойств и их значения
по умолчанию соответственно. Далее отрисовываем сам библиотечный компонент
<BootstrapTable>
, передавая ему необходимые свойства. Их
может быть больше - тут представлен лишь небольшой перечень.
Такие компоненты, как список всегда показывают какие-либо данные. Очень часто при разработке приложения серверная часть по каким-то причинам может быть не готова. Не смотря на это разработка должна продолжаться, и заказчик должен видеть результат. Необходима демо-версия приложения с фиктивными (фэйковыми, стабовыми) данными, которые в процессе разработки сервера будут заменяться реальными. Более того, можно реализовать возможность переключаться между фиктивными и реальными данными.
Давайте создадим файл с фиктивными данными и назовём его MockData.js
в папке /lib. В нём мы создадим данные для нашего списка приёмов:
export const appointments = [
{
date: 1560422694514,
clientName: 'Должанский Николай Сергеевич',
status: 'Завершён',
holderName: 'Иванов Иван Иванович',
compliences: 'Боль в правом ухе',
diagnosis: 'Застужено правое ухо'
},
{
date: 1560422694514,
clientName: 'Пертов Пётр Генадьевич',
status: 'Завершён',
holderName: 'Иванов Иван Иванович',
compliences: 'Боль в горле',
diagnosis: 'Ангина'
}
]
Даты в данных лучше всегда указывать числом. Дело в том, что дату в виде числа можно отформатировать любым требуемым образом. Даты в виде строк далеко не такие гибкие.
Поскольку базовый компонент таблицы и фиктивные данные готовы, самое время создать компонент списка приёмов:
import React, { Component } from 'react'
import { map } from 'underscore'
import Moment from 'react-moment'
import './Appointments.scss'
import Table from '../Table/Table'
import Header from '../Header/Header'
import { ReactComponent as Appointment } from '../../images/appointment.svg'
import { appointments as data } from '../../lib/MockData'
const TITLE = 'Приёмы'
export default class Appointments extends Component {
render() {
return (
<div className='Appointments'>
<Header
title={TITLE}
userName='Иванов Иван Иванович'
className='Appointments-Header'
renderIcon={() => (
<Appointment className='Header-Icon' />
)}
/>
<div className='Appointments-Body'>
<Table
data={data}
columns={[
{
dataField: 'date',
text: 'Дата',
headerStyle: {
width: '200px'
},
formatter: (v, row) => {
return (
<Moment date={v} format='DD.MM.YYYY HH.mm'/>
)
}
},
{
dataField: 'clientName',
text: 'Клиент'
},
{
dataField: 'status',
text: 'Статус'
},
{
dataField: 'holderName',
text: 'Принимающий'
},
{
dataField: 'compliences',
text: 'Жалобы'
},
{
dataField: 'diagnosis',
text: 'Диагноз'
}
]}
/>
</div>
</div>
)
}
}
Мы отрисовали компонент хидера и базовый компонент таблицы, передав в неё фиктивные данные.
Обратите внимание на свойство formatter
в списке дексрипторов
столбцов columns таблицы. С помощью этого свойства него мы можем
форматировать вывод: результатом отрисовки ячейки таблицы будет то,
что возвращает функция formatter
. Поскольку мы имеем дело с датами
в виде long, то прежде чем их отобразить, нам необходимо придать им
читабельный вид. Для этого я использовал популярную библиотеку moment.js,
а точнее её react-версию :
"moment": "2.24.0",
"react-moment": "0.9.2"
Вот рабочий пример:
Как вы могли заметить, не хватает формы для фильтрации приёмов. Именно ей мы займёмся в следующем разделе!
5.5.5.1 Библиотека reactstrap
Прежде чем приступить к реализации формы фильтра, хочу познакомить вас с библиотекой . Здесь собраны основные компоненты bootstrap для React. Прочитайте документацию и узнайте, что вы можете использовать в своих приложениях. Уверен, что польза будет ощутимая.
5.5.5.2 Базовые компоненты элементов формы
Форма фильтра имеет следующий вид:
Как видно из рисунка она состоит из четырёх элементов: двух полей выбора даты, текстового поля и чекбокса. Самое время создать соответствующие базовые компоненты.
Текстовое поле:
import React, { Component } from 'react'
import cn from 'classname'
import PropTypes from 'prop-types'
import {Label, Input, FormGroup} from 'reactstrap'
import './TextField.scss'
export default class TextField extends Component {
static propTypes = {
type: PropTypes.oneOf(['text','textarea', 'email', 'password', 'date']),
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
className: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func
}
static defaultProps = {
type: 'text',
value: '',
onChange: function () {}
}
onChange = e => {
const value = e.target.value
const { name, onChange: cb } = this.props
cb(name, value)
}
render () {
const {
type,
name,
label,
value,
className,
placeholder
} = this.props
return (
<FormGroup className={cn('TextField', className)}>
{label ? (
<Label className='TextField-Label'>
{label}
</Label>
) : null}
<Input
type={type}
name={name}
value={value}
placeholder={placeholder}
className='TextField-Input'
onChange={this.onChange}
/>
</FormGroup>
)
}
}
Стоит отметить, что здесь использованы такие компоненты
библиотеки reactstrap: <FormGroup>
, <Label>
и <Input >
. Компонент <FormGroup >
использован
в соответствии с правилами построения форм в reactstrap. В остальном же всё
просто - мы делаем компонент-обёртку с нашими свойствами и дополнительными возможностями.
Кстати это известный паттерн проектирования под названием декоратор.
Наш компонент <TextField>
является контролируемым: он не содержит
состояния, а текущее значение value устанавливается родителем.
Следующий наш компонент <CheckboxField>
:
import React, {Component} from 'react'
import cn from 'classname'
import PropTypes from 'prop-types'
import {Label, Input, FormGroup} from 'reactstrap'
import './CheckboxField.scss'
class CheckboxField extends Component {
static propTypes = {
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func
}
static defaultProps = {
value: false,
onChange: function () {}
}
onChange = e => {
const value = e.target.checked
const { name, onChange: cb } = this.props
cb(name, value)
}
render() {
const {
label,
value,
className
} = this.props
return (
<FormGroup check className={cn('CheckboxField', className)}>
<Label
check
onClick={this.onClick}
className='CheckboxField-Label'>
<Input
type='checkbox'
value={value}
onClick={this.onChange}
className='CheckboxField-Checkbox'
/>
{label}
</Label>
</FormGroup>
)
}
}
export default CheckboxField;
Как видно, он очень похож на компонент <TextField>
.
Ну и наконец компонент <DateField>
:
import React, {Component} from 'react'
import cn from 'classname'
import PropTypes from 'prop-types'
import DatePicker from 'react-datepicker'
import {FormGroup, Label} from 'reactstrap'
import './DateField.scss'
export default class DateField extends Component {
static propTypes = {
name: PropTypes.string,
label: PropTypes.string,
hasTime: PropTypes.bool,
placeholder: PropTypes.string,
dateFormat: PropTypes.string,
timeFormat: PropTypes.string,
timeInterval: PropTypes.number,
className: PropTypes.string,
onChange: PropTypes.func
}
static defaultProps = {
hasTime: false,
dateFormat: 'dd/MM/yyyy',
// формат времени, отображающийся в выпадающем списке
timeFormat: 'HH:mm',
// шаг выбора времени
timeInterval: 30,
onChange: function () {}
}
onChange = (value) => {
const { name, onChange: cb } = this.props
cb(name, value)
}
render () {
const {
name,
label,
value,
dateFormat,
hasTime,
timeFormat,
timeInterval,
onChange,
className,
placeholder
} = this.props
return (
<FormGroup className={cn('DateField', className)}>
<div>
{label ? (
<Label className='DateField-Label'>
{label}
</Label>
) : null}
<DatePicker
name={name}
selected={value}
dateFormat={dateFormat}
timeFormat={timeFormat}
showTimeSelect={hasTime}
timeIntervals={timeInterval}
onChange={this.onChange}
placeholderText={placeholder}
className='DateField-Input form-control'
/>
</div>
</FormGroup>
)
}
}
Он немного более интересен, так как здесь я использовал популярную библиотеку react-datepicker. Она отлично поддерживается и решает большой спектр задач. Стоит изучить весь список её опций, чтобы понимать масштаб возможностей. Такая библиотека нужна практически в каждом проекте.
Что ж, все необходимые базовые компоненты формы готовы. Теперь давайте
добавим больше фиктивных данных в MockData.js
, чтобы лучше
протестировать работу фильтра:
export const appointments = [
{
date: 1556863200000,
clientName: 'Должанский Николай Сергеевич',
status: 'Завершён',
holderName: 'Иванов Иван Иванович',
compliences: 'Боль в правом ухе',
diagnosis: 'Застужено правое ухо'
},
{
date: 1560778200000,
clientName: 'Пертов Пётр Генадьевич',
status: 'Завершён',
holderName: 'Иванов Иван Иванович',
compliences: 'Боль в горле',
diagnosis: 'Ангина'
},
{
date: 1560256200000,
clientName: 'Буйкевич Галина Петровна',
status: 'Завершён',
holderName: 'Нестеров Валерий Викторович',
compliences: 'Головные боли',
diagnosis: 'Мигрень'
},
{
date: 1561017600000,
clientName: 'Астафьева Ирина Михайловна',
status: 'Завершён',
holderName: 'Сидоров Генадий Павлович',
compliences: 'Тошнота',
diagnosis: 'Ротавирус'
}
]
Итак, сейчас у нас всё готово для того, чтобы реализовать фильтр в компоненте
<Appointments>
. Давайте сделаем это:
import React, { Component } from 'react'
import {Form} from 'reactstrap'
import Moment from 'react-moment'
import {map, filter} from 'underscore'
import Table from '../Table/Table'
import Header from '../Header/Header'
import TextField from '../Form/TextField/TextField'
import DateField from '../Form/DateField/DateField'
import CheckboxField from '../Form/CheckboxField/CheckboxField'
import './Appointments.scss'
import { ReactComponent as Appointment } from '../../images/appointment.svg'
import { appointments as data } from '../../lib/MockData'
const TITLE = 'Приёмы'
const USER = 'Иванов Иван Иванович'
export default class Appointments extends Component {
state = {
filter: {
startDate: null,
endDate: null,
clientName: '',
onlyMe: false
}
}
onChangeFilterField = (name, value) => {
const { filter } = this.state
this.setState({
filter: {...filter, ...{[name]: value}}
})
}
onChangeFilterDateField = (name, value) => {
const { filter } = this.state
this.setState({
filter: {...filter, ...{[name]: value && value.getTime()}}
})
}
render() {
const {
startDate,
endDate,
clientName,
onlyMe,
} = this.state.filter
let filtered = filter(data, o => {
return (startDate ? o.date >= startDate : true) &&
(endDate ? o.date <= endDate : true) &&
(clientName ? (clientName.length > 2 ? o.clientName.includes(clientName) : true) : true) &&
(onlyMe ? o.holderName === USER : true)
})
return (
<div className='Appointments'>
<Header
title={TITLE}
userName={USER}
className='Appointments-Header'
bodyClassName='Appointments-HeaderBody'
renderIcon={() => (
<Appointment className='Header-Icon' />
)}
/>
<div className='Appointments-Body'>
<div className='Appointments-Filter'>
<Form className='Appointments-FilterForm'>
<DateField
hasTime
name='startDate'
value={startDate}
dateFormat='dd/MM/yyyy HH:mm'
timeFormat='HH:mm'
placeholder='С'
className='Appointments-FilterField'
onChange={this.onChangeFilterDateField}
/>
<DateField
hasTime
name='endDate'
value={endDate}
dateFormat='dd/MM/yyyy HH:mm'
timeFormat='HH:mm'
placeholder='По'
className='Appointments-FilterField'
onChange={this.onChangeFilterDateField}
/>
<TextField
name='clientName'
value={clientName}
placeholder='Клиент'
className='Appointments-FilterField'
onChange={this.onChangeFilterField}
/>
<CheckboxField
name='onlyMe'
label='Только я'
value={onlyMe}
className='Appointments-FilterField'
onChange={this.onChangeFilterField}
/>
</Form>
</div>
<Table
data={filtered}
className='AppointmentList'
columns={[
{
dataField: 'date',
text: 'Дата',
headerStyle: {
width: '150px'
},
formatter: (v, row) => {
return (
<Moment date={v} format='DD.MM.YYYY HH.mm'/>
)
}
},
{
dataField: 'clientName',
text: 'Клиент',
headerStyle: {
width: '300px'
}
},
{
dataField: 'status',
text: 'Статус'
},
{
dataField: 'holderName',
text: 'Принимающий',
headerStyle: {
width: '300px'
}
},
{
dataField: 'compliences',
text: 'Жалобы',
headerStyle: {
width: '200px'
}
},
{
dataField: 'diagnosis',
text: 'Диагноз',
headerStyle: {
width: '200px'
}
}
]}
/>
</div>
</div>
)
}
}
Давайте проанализируем наш обновлённый код.
Во-первых у компонента <Appointments>
появилось
состояние state
, в котором есть переменная filter
,
хранящая данные фильтра. Во-вторых для изменения состояния фильтра
здесь используются два метода onChangeFilterField
и onChangeFilterDateField
.
Так сделано потому, что во второй приходит объект даты, а не примитивное значение, как в текстовом
поле или чекбоксе. Мы могли бы использовать и один метод, но тогда нам нужно было бы
делать определение типа переменной value
, что определённо
выглядело бы не так аккуратно.
Далее была добавлена сама логика фильтрации данных:
let filtered = filter(data, o => {
return (startDate ? o.date >= startDate : true) &&
(endDate ? o.date <= endDate : true) &&
(clientName ? (clientName.length > 2 ? o.clientName.includes(clientName) : true) : true) &&
(onlyMe ? o.holderName === USER : true)
})
Ничего сложного: четыре условия для каждого поля, соединённых
оператором && (логическое И). Обратите внимание на clientName.length > 2
,
это сделано для того, чтобы заставить фильтр срабатывать только тогда,
когда пользователь введёт больше 2 символов в поле (это не обязательно).
В остальном всё знакомо. Изучите и протестируйте полную рабочую версию примера: