Translate

Поиск по этому блогу

пятница, 22 февраля 2019 г.

React - Интерактивная таблица. (II).

Это будет вторая часть, посвященная созданию динамической интерактивной таблицы на React.js



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

В этой части нам нужно добавить клиентскую пагинацию (максимум 50 элементов на странице) добавить фильтрацию - поле поиска элемента по каким-то вводимым символам. Будем показывать отфильтрованные данный из этой таблицы. которые будут соответствовать запросу.

В этом разделе нам нужно предоставить пользователю возможность выбора большого (1000 элементов) или малого (32 элемента) объемов данных для загрузки на страницу с таблицей.

Начнем с конца и предоставим пользователю такое право с помощью двух кнопок, которые будут показываться сразу на экране при загрузке самого приложения, и уже при клике на которые, пользователь может загрузик в таблицу большие (1000 элементов) или малые (32) данные.

На этом этапе код по ссылке: react-table-abc
ci -m "data output from the table row to the page"

Выбор данных для загрузки.

В коде, который мы написали в прошлый раз, мы грузим сразу маленький объем данным по ссылке в жизненном цикле компонента асинхронно.(см. код компонента App в componentDidMount())

Начнем с того, что в нашем стейте компонента App мы добавим новый флаг isModeSelected: false и изменим значение флага отвечающего за немедленную загрузку данных на страницу isLoading на false


  state ={
    isModeSelected: false,
    isLoading: false,
    data: [],
    sort: 'asc',  // 'desc'
    sortField: 'id',
    row: null,
  }



Теперь, для того, чтобы таблицы у нас не рисовались, мы воспользуемся данным флагом и в методе render проверим. Если мы не выбрали никакой мод, то есть this.state.isModeSelected мы вырисовываем простой каркас приложения с новым компонентом - ModeSelector.


if(!this.state.isModeSelected){
  return (
    <div className="container">
      <ModeSelector />
    </div>
  )
}



Создаем компонент ModeSelector Как вы догадались, в папке src создадим папку ModeSelector и в ней js-файл с таким же именем.

Хорошей практикой считается хранить компоненты в отдельной папке Components, но если их немного, как в этом проекте, то мы можем смело хранить их в src.

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

Если коротко, то здесь мы создали простой компонент с использованием {} для создания тела функции, потому что в нем мы будем хранить две константы с ссылками на большие и малые данные.

Вывели две кнопки, классы для их стилизации, я взял у бутстрапа, и навесили на них событие onClick={()=>props.onSelect(smallUrl)}.

Функцию onSelect мы получаем из props в нее мы передаем константу с url-адресом. Это написание - второй способ передать параметры в функцию. В первой части таблиц, мы использовали метод bind(), а в этот раз по типу замыкания. Мы передадим сюда функцию в которой обратимся к методу onSelect из propsи передадим адрес.


import React from 'react';

export default props =>{
    const smallUrl = `http://www.filltext.com/?rows=32&id={number|1000}&firstName={firstName}&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`;
    const bigUrl = `http://www.filltext.com/?rows=1000&id={number|1000}&firstName={firstName}&delay=3&lastName={lastName}&email={email}&phone={phone|(xxx)xxx-xx-xx}&address={addressObject}&description={lorem|32}`;
    return (
        <div style={{display:'flex', justifyContent:'center', padding: '50px 0'}}>
            <button onClick={()=>props.onSelect(smallUrl)} className="btn btn-success">32 элемента</button>
            <button onClick={()=>props.onSelect(bigUrl)} className="btn btn-danger">1000 элементов</button>
        </div>
    )
}


Для более красивого отображения кнопок по центру, добавили инлайн стили.

Теперь нам нужно подключить вновь созданный компонент в App, передать в него название свойства onSelect и написать сам метод, который будет передаваться как свойство onSelect, внутри компонента App.

Подключаем просто:

import ModeSelector from './ModeSelector/ModeSelector';

Передаем в пропсы - еще проше:


if(!this.state.isModeSelected){
  return (
    <div className="container">
      <ModeSelector onSelect={this.modeSelectHandler} />
    </div>
  )
}



Теперь нам нужно написать саму функцию modeSelectHandler в теле компонента:

  modeSelectHandler = url => {
     console.log(url)
  }


Как, мы и договорились, функция будет получать url , который и выведем в консоли.

Посмотрим на работу приложения в консоли:



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

На этом этапе код по ссылке: react-table-abc
ci -m "Creating a component with buttons for links. Stylization, output to the console url - addresses."

Вывод данных на страницу.

Теперь нам необходимо, после клика скрыть этот элемент и загрузить необходимые данные.

Это мы можем сделать в методе modeSelectHandler изменив состояние компонента, а именно флаги isModeSelected и isLoading на true

И создадим там же какой-либо метод, куда мы и передадим url - this.fetchData(url)


  modeSelectHandler = url => {
    // console.log(url)
    this.setState({
      isModeSelected: true,
      isLoading: true,
    })
    this.fetchData(url)
  }



Самый простой способ реализовать метод fetchData(url) это переписать под него жизненный цикл компонента componentDidMount(). Мы просто изменим это название на название метода. Единственное что, так это то, что он будет принимать некий url, по которому мы должны сделать запрос.


  async fetchData(url) {
    const response = await fetch(url)
    const data = await response.json()

    this.setState({
      isLoading: false,
      data: _.orderBy(data, this.state.sortField, this.state.sort)
    })

  }


Изменения стейта оставили без изменений. Все изменения я отметил в коде красным.

Теперь можно посмотреть на приложение в браузере.

При клике будет показываться лоудер и загружаться большая или малая таблица.

На этом этапе код по ссылке: react-table-abc
ci -m "Data output table by clicking on the buttons"

Рефактринг кода

Немного поменяем метод onSort.

Сейчас он выглядит так:


  onSort = sortField => {
    
    const cloneData = this.state.data.concat();
    const sortType = this.state.sort === 'asc' ? 'desc' : 'asc';
    const orderedData = _.orderBy(cloneData, sortField, sortType);

    this.setState({
      data: orderedData,
      sort: sortType,
      sortField
    })
  }



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


  onSort = sortField => {
    
    const cloneData = this.state.data.concat();
    const sort = this.state.sort === 'asc' ? 'desc' : 'asc';
    const data = _.orderBy(cloneData, sortField, sort);

    this.setState({
      data,
      sort,
      sortField
    })
  }


Теперь мы можем записать установщик новых стейтов в одну строку:


    onSort = sortField => {
      const cloneData = this.state.data.concat();
      const sort = this.state.sort === 'asc' ? 'desc' : 'asc';
      const data = _.orderBy(cloneData, sortField, sort);
      this.setState({ data, sort, sortField})
   }



На этом этапе код по ссылке: react-table-abc
ci -m "Refactoring onSort function"

Пагинация

Делать пагинацию самостоятельно с ноля мы не буем, так как есть готовые компоненты, а логика там фиксированная, поэтому мы воспользуемся почти готовым решением.

а) Создание компонента пагинации

Можно посмотреть на то, как реализована пагинация в бутстрапе - pagination Bootstrap - здесь есть готовая верстка. Отсюда мы возьмем названия нужных нам классов, в дальнейшем, для некоторой стилизации нашего компонента пагинации.

Для создания пагинации мы будем использовать готовый npm-модуль - react-paginate. Вы можете все почитать на странице, а проще сразу посмотреть на демонстрацию его работы. по ссылке со страницы.

Устанавливаем модуль:

npm install react-paginate --save


Импортируем данный компонент в App:

import ReactPaginate from 'react-paginate';

Сам компонент мы скопируем из этого же кода на странице-демо.


        <ReactPaginate
          previousLabel={'previous'}
          nextLabel={'next'}
          breakLabel={'...'}
          breakClassName={'break-me'}
          pageCount={this.state.pageCount}
          marginPagesDisplayed={2}
          pageRangeDisplayed={5}
          onPageChange={this.handlePageClick}
          containerClassName={'pagination'}
          subContainerClassName={'pages pagination'}
          activeClassName={'active'}
        />



Этот компонент пагинации, нам нужно поставить после таблицы.

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

Количество элементов определим в костанту - pageSize в методе render

this.state.data.length > pageSize

переименуем в скопированном компоненте метод handlePageClick в pageChangeHandler и создадим его в компоненте.

Этот метод будет вызываться при нажатии на кнопку изменения страницы.


  pageChangeHandler = page => (
    console.log(page)
  )



Напишем его в компоненте App и передадим в него некий объект - page и посмотрим на него в консоли при загрузке 1000 элементов:



При клике на страницу нам приходит объект selected с номером страницы. Обраите внимание, что нумерация страниц начинается с ноля. То есть, при клике на 5, в selected придет -4, при клике на 6 - 5, соответственно.

Пагинация выглядит очень неважно. Если посмотреть в раздел Elements разработчика, то можно заметить, что её html-структура очень похожа на разметку Bootstrap.



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

На странице компонента мы можем посмотреть те свойства, которые мы можем использовать для стилизации нашего компонента.


      {
        this.state.data.length > pageSize
        ? <ReactPaginate
        previousLabel={'<'}
        nextLabel={'>'}
        breakLabel={'...'}
        breakClassName={'break-me'}
        pageCount={20}
        marginPagesDisplayed={2}
        pageRangeDisplayed={5}
        onPageChange={this.pageChangeHandler}
        containerClassName={'pagination'}
        activeClassName={'active'}
        pageClassName="page-item"
        pageLinkClassName="page-link"
        previousClassName="page-item"
        nextClassName="page-item"
        previousLinkClassName="page-link"
        nextLinkClassName="page-link"
      /> : null
      }



pageCount={20} пока что сделали = 20, но потом будем задавать динамически.

Строку subContainerClassName={'pages pagination'} убрал полностью. Она нам не нужна.

Добавил стили бутстрапа этим свойствам компонента. Отметил красным.

Посмотрим в браузер и увидим, что нужным элементам нашего компонента мы добавили классы бутстрап.



На этом можно закончить стилизацию компонента.

На этом этапе код по ссылке: react-table-abc
ci -m "creation and stylization of pagination."

б) Связь пагинации с стейтом.

Зная, что номер страницы мы получим в объекте selected, мы можем получить его сразу методом деструктуризации компонента функции - ({selected}).

В теле мы будем изменять номер страницы, поэтому в стейт по умолчанию добавим новый флаг currentPage с номером - 0


  class App extends Component {
  state ={
    isModeSelected: false,
    isLoading: false,
    data: [],
    sort: 'asc',  // 'desc'
    sortField: 'id',
    row: null,
    currentPage: 0,
  }



В теле функции мы будем изменять это поле:


    pageChangeHandler = ({selected}) => (
    this.setState({currentPage: selected})
  )



в) Оживляем пагинацию.

Посмотрим на логику отображения таблицы. У нас есть лимит полей для страницы:

const pageSize = 50;

и в стейтах поле data: [], содержащее все элементы. Поэтому мы в методе render() создадим отдельную переменную, которую мы будем передавать прямо в таблицу.

Нам очень важно не изменять поле data, иначе мы сотрем все данные.

Тысяча элементов пусть хранится в стейте, а отображать мы будем только 50.

Для этого мы создадим переменную

const displayData = _.chunk(filteredData, pageSize)[this.state.currentPage]

Эта константа будет хранить определенную часть данных, которая будет зависеть от номера текущей станицы (currentPage из стейта). Здесь мы воспользовались методом _.chunk библиотеки Lodash.

Этот метод принимает первым аргументом массив, а вторым - число элементов под массива (длина под массивов). Более подробно об этом вы можете прочитать в документации Lodash ._chunk.

Здесь мы получаем результирующий массив данных таблиицы:

_.chunk(filteredData, pageSize)

и нам остается обратиться к его индексу, чтобы забрать нужную часть -

[this.state.currentPage]

Таким образом мы получим в переменную displayData нужные данные. И теперь нам остается передать эту переменную в data таблицы:


  {
    this.state.isLoading 
    ? <Loader />
    : <Table 
          data={displayData}
          onSort={this.onSort}
          sort={this.state.sort}
          sortField={this.state.sortField}
          onRowSelect={this.onRowSelect}
      />
  }


Можно посмотреть на приложение в браузере. Мы увидим, что на одну страницу загрузилось ровно 50 элементов.



Теперь при изменении страницы, будет динамически меняться содержимое таблицы.

На этом этапе код по ссылке: react-table-abc
ci -m "Pagination is connected."

г) Синхронизация номера страницы с состоянием нашего стейта.

Это будет необходимо, потому что в будущем это потребуется. Для этого нам нужно воспользоваться свойством forcePage со страницы компонента пагинации npm react-paginate.

Этот параметр позволяет переписывать текущую страницу с родительским свойством. С помщь. него мы синхронизируем номер страницы с состоянием стейта.

Добавим его в самый низ записи компонента:


{
    this.state.data.length > pageSize
    ? <ReactPaginate
    previousLabel={'<'}
    nextLabel={'>'}
    breakLabel={'...'}
    breakClassName={'break-me'}
    pageCount={pageCount}
    marginPagesDisplayed={2}
    pageRangeDisplayed={5}
    onPageChange={this.pageChangeHandler}
    containerClassName={'pagination'}
    activeClassName={'active'}
    pageClassName="page-item"
    pageLinkClassName="page-link"
    previousClassName="page-item"
    nextClassName="page-item"
    previousLinkClassName="page-link"
    nextLinkClassName="page-link"
    forcePage={this.state.currentPage}
  /> : null
  }



На этом этапе стоит убедиться. что пагинация работает и ничего не сломалось. Работают фильтры и при загрузке 32 элемента, пагинации нет, а при загрузке 1000 эл. пагинация, фильтрация и вывод отдельного компонента на страницу продолжаю работать.

На этом этапе код по ссылке: react-table-abc
ci m "Synchronize page number with state"

Добавляем поиск.

Для этого создадим еще один "глупый" компонент с именем TableSearch. Это подразумеваем создание одноименной папки и файла с расширением .js в папке src.

В этом компоненте мы будем использовать React-Hocks, потому что мы хотим взаимодействовать с состоянием компонента не смотря на то, что он "глупый".

Я, в ближайшее время напишу пост по основным хукам Реакт, а пока что приведу компонент полностью и попробую далее объяснить его действие.


import React, {useState} from 'react'

export default props => {
    const [value, setValue] = useState('')
    const valueChangeHandler = event => {
        setValue(event.target.value)
      }

    return (
        <div className="input-group mb-3 mt-3">
             <div className="input-group-prepend">
                 <button 
                    className="btn btn-outline-secondary"
                    onClick={() => props.onSearch(value)} >Search</button>
            </div>
            <input 
                type="text" 
                className="form-control"
                onChange={valueChangeHandler} 
                value={value}
            />
        </div>
    )
}



Все что возвращает этот компонент, есть ничто иное как обычное поле поиска с кнопкой, которое я взял с сайта Bootstrap Button addons.

Поменял class на className и добавил закрывающий / для input.

Импортируем наш новый компонент в App:

import TableSearch from './TableSearch/TableSearch';

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


{
    this.state.isLoading 
    ? <Loader />
    : <React.Fragment>
        <TableSearch />
        <Table 
          data={displayData}
          onSort={this.onSort}
          sort={this.state.sort}
          sortField={this.state.sortField}
          onRowSelect={this.onRowSelect}
        />
      </React.Fragment>

  }



В компоненте TableSearch.js cсейчас только метод return



import React from 'react'

export default props => {

    return (
        <div className="input-group mb-3 mt-3">
             <div className="input-group-prepend">
                 <button className="btn btn-outline-secondary" >Search</button>
            </div>
            <input 
                type="text" 
                className="form-control"
            />
        </div>
    )
}



Для отступа сверху добавили только mt-3.

Если посмотреть в браузер, то можно увидеть готовый, но совершенно нерабочий компонент.



На этом этапе код по ссылке: react-table-abc
ci -m "Add layout new component - search field"

Хук заменяющий стейт в реакт

Теперь нам нужно сделать так, чтобы мы что-то писали в поле поиска и после нажатия на кнопку, в наших таблицах происходила фильтрация элементов. То есть так, чтобы только соответствующие поисковому запросу элементы показывались на странице.

Так как нам придется отслеживать наш input, то нам потребуется стейт.

И коль скоро, мы используем реакт 16.8, то мы будем использовать хуки.

Импортируем useState


import React, {useState} from 'react'

export default props => {
    const [value, setValue] = useState('')
    const valueChangeHandler = event => {
        setValue(event.target.value)
      }

    return (
        <div className="input-group mb-3 mt-3">
             <div className="input-group-prepend">
                 <button 
                    className="btn btn-outline-secondary"
                    onClick={() => props.onSearch(value)} >Search</button>
            </div>
            <input 
                type="text" 
                className="form-control"
                onChange={valueChangeHandler} 
                value={value}
            />
        </div>
    )
}



value - это и будет само состояние.

Добавили метод, который будет отслеживать изменение состояния (импута) - valueChangeHandler.

В теле компонента создадим саму эту функцию, которая будет менять "стейт", точнее нашу переменную value, с помощью setValue.

Вот таким образом мы смогли написать изменение стейта в "глупом" компоненте используя реакт-хуки.

Теперь, нам нужно при нажатии на кнопку, передавать состояние данной строки. Для этого кнопке добавим:

onClick={() => props.onSearch(value)}

И так как мы получаем onSearch с пропертями, то нам этот метод нужно передать в компоненте App.


{
    this.state.isLoading 
    ? <Loader />
    : <React.Fragment>
        <TableSearch onSearch={this.searchHandler} />
        <Table 
          data={displayData}
          onSort={this.onSort}
          sort={this.state.sort}
          sortField={this.state.sortField}
          onRowSelect={this.onRowSelect}
        />
      </React.Fragment>

  }



В теле компонента App создадим сам метод - searchHandler


 searchHandler = search =>(
    console.log(search)
  )



Проверим в консоли, правильно ли мы получаем строку поиска и что она из себя представляет.

Идем в браузер. Вписываем какую-либо строку, например - User, получаем ее в консоли, после нажатия на кнопку поиска.



На этом этапе код по ссылке: react-table-abc
ci -m"We receive data from impu search in the console."

Добавляем фильтрацию элементов для поиска.

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

Вначале создадим поле search:'' в стейте компонента App


  state ={
    isModeSelected: false,
    isLoading: false,
    data: [],
    search: '',
    sort: 'asc',  // 'desc'
    sortField: 'id',
    row: null,
    currentPage: 0,
  }



Теперь в методе searchHandler мы будем изменять состояние и сбрасывать текущую страницу, для того, чтобы поиск шел по всему документу корректно. - currentPage: 0


  searchHandler = search => {
    this.setState({search, currentPage: 0})
  }



Именно для корректной работы поиска мы создавали параметр forcePage={this.state.currentPage}, ранее.

Мы изменяем стейт и реакт будет заходить в метод рендер, после того, как мы будем изменять search.

Теперь в зависимости от search нам нужно показывать data. То есть в переменную displayData положить данные не только по количеству отсортированные, но и отфильтрованные по полю поиска.

Для этого в рендере, рядом с этой констатой, мы создадим еще одну:

const filteredData = this.getFilteredData()

В нее положим данные, которые мы будем получать, например из getFilteredData()

Теперь напишем этот метод в теле компонента App


  getFilteredData(){
    const {data, search} = this.state

    if (!search) {
      return data
    }

    return data.filter(item => {
      return item['firstName'].toLowerCase().includes(search.toLowerCase())
        || item['lastName'].toLowerCase().includes(search.toLowerCase())
        || item['email'].toLowerCase().includes(search.toLowerCase())
    })
  }



Здесь мы заберем из стейта два поля деструктуризацией - const {data, search} = this.state

Далее, прстая проверка. Если в поле search ничего нет, то возвращаем обычные данные - data

Если в search что-то есть, то мы будем отфильтровывать данные таблицы.

Переменная item попадет в массив, если будет соответствовать условиям фильтрации, иначе мы ее убираем.

Теперь мы берем под-поля - firstName, lastName, email и проверяем, есть ли там нужная подстрока?

Для корректной работы мы приведем все к нижнему регистру. - .toLowerCase

Для проверки на подстроку, мы воспользуемся методом includes(), который вернет true если содержится. Иначе false.

Если нужна фильтрация, только по определенному полю, то ненужные поля можно просто удалить.

Теперь нам остается поменять pageCount={20} в ретурне App компонента - ReactPaginate на pageCount={pageCount} и высчитать его значение динамически:

В рендер метод добавим, ниже const filteredData = this.getFilteredData(), потому что кол-во страниц будет зависить от этой константы:

const pageCount = Math.ceil(filteredData.length / pageSize)

Здесь, я думаю, что все понятно. Округляем, потому что может быть дробное число. Делим количество отфильтрованных элементов на кол-во элементов на странице и получаем - кол-во страниц, которые будут показаны для пагинации отфильтрованных элементов.

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

На этом этапе код по ссылке: react-table-abc
ci -m "add filtering in search"

Да, и если возникли трудности в коде, не знаете где ошибка, то - debager пишем в коде, и далее, в хроме дебажим все по шагам.

Готовое приложение здесь

Первая часть <-

Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.


Удачного кодирования!                                                                                                                                                              

Телеграм канал - Full Stack JavaScript Developer

3 комментария:

  1. const displayData = _.chunk(filteredData, pageSize)[this.state.currentPage]

    вот эта штука не работает...

    ОтветитьУдалить
    Ответы
    1. Все работает, если не тупо переписывать. filteredData - сюда вставляем state.data (данные по которым идет фильтрация)

      Удалить



Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.Пишите мне - kolesnikovy70 почта gmail.com