Реализация Shuffle.js с React.js - PullRequest
2 голосов
/ 16 мая 2019

Я пытаюсь активировать функциональность компонента shuffle.js (поиск, фильтрация и сортировка) с помощью response.js. Однако документация на сайте очень ограничена. Я знаю, что мне нужно добавить поисковый ввод и некоторые кнопки, чтобы делать то, что я хочу, но я не уверен, как соединить вход моего окна поиска и другие события кнопок, чтобы манипулировать фотосеткой (или другими элементами в контейнере), которая отрисовывается по реакции.

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

import React, {Component} from "react";
import Shuffle from 'shufflejs';

class PhotoGrid extends React.Component {

  constructor(props) {
    super(props);

    const grayPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
    const blackPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
    const greenPixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO02Vz4HwAE9AJhcLBN6AAAAABJRU5ErkJggg==';


    this.state = {
      photos: [{
          id: 4,
          src: grayPixel
        },
        {
          id: 5,
          src: blackPixel
        },
        {
          id: 6,
          src: greenPixel
        },
      ],
      searchTerm: '',
      sortByTitle: '',
      sortByDate: '',
      sortByPopularity: '',
      filterCategory: ''

    };

    this.filters = {
      cat1: [],
      cat2: [],
    };

    this.wb = this.props.dataWB;

    this.element = React.createRef();
    this.sizer = React.createRef();
    this._handleSearchKeyup = this._handleSearchKeyup.bind(this);
    this._handleSortChange = this._handleSortChange.bind(this);
    this._handleCategory1Change = this._handleCategory1Change.bind(this);
    this._handleCategory2Change = this._handleCategory2Change.bind(this);
    this._getCurrentCat1Filters = this._getCurrentCat1Filters.bind(this);
    this._getCurrentCat2Filters = this._getCurrentCat2Filters.bind(this);

  }

  /**
   * Fake and API request for a set of images.
   * @return {Promise<Object[]>} A promise which resolves with an array of objects.
   */
  _fetchPhotos() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve([{
            id: 4,
            username: '@stickermule',
            title:'puss',
            date_created: '2003-09-01',
            popularity: '233',
            category1:'animal',
            category2:'mammals',
            name: 'Sticker Mule',
            src: 'https://images.unsplash.com/photo-1484244233201-29892afe6a2c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=14d236624576109b51e85bd5d7ebfbfc'
          },
          {
            id: 5,
            username: '@prostoroman',
            date_created: '2003-09-02',
            popularity: '232',
            category1:'industry',
            category2:'mammals',
            title:'city',
            name: 'Roman Logov',
            src: 'https://images.unsplash.com/photo-1465414829459-d228b58caf6e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=7a7080fc0699869b1921cb1e7047c5b3'
          },
          {
            id: 6,
            username: '@richienolan',
            date_created: '2003-09-03',
            popularity: '231',
            title:'nature',
            category1:'art',
            category2:'insect',
            name: 'Richard Nolan',
            src: 'https://images.unsplash.com/photo-1478033394151-c931d5a4bdd6?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=3c74d594a86e26c5a319f4e17b36146e'
          }
        ]);
      }, 300);
    });
  }

  _whenPhotosLoaded(photos) {
    return Promise.all(photos.map(photo => new Promise((resolve) => {
      const image = document.createElement('img');
      image.src = photo.src;

      if (image.naturalWidth > 0 || image.complete) {
        resolve(photo);
      } else {
        image.onload = () => {
          resolve(photo);
        };
      }
    })));
  }

  _handleSortChange(evt) {
    var value = evt.target.value.toLowerCase();

    function sortByDate(element) {
      return element.getAttribute('data-created');
    }

    function sortByPopularity(element) {
      return element.getAttribute('data-popularity');
    }

    function sortByTitle(element) {
      return element.getAttribute('data-title').toLowerCase();
    }

    let options;
    if (value == 'date-created') {
      options = {
        reverse: true,
        by: sortByDate,
      };
    } else if (value == 'title') {
      options = {
        by: sortByTitle,
      };
    } else if (value == 'popularity') {
      options = {
        reverse: true,
        by: sortByPopularity,
      };
    } else if (value == 'default') {
      this.shuffle.filter('all');
    } else {
      options = {};
    }

    this.shuffle.sort(options);
  };

  _getCurrentCat1Filters = function () {
    return this.filters.cat1.filter(function (button) {
      return button.classList.contains('active');
    }).map(function (button) {
      console.log('button value: '+button.getAttribute('data-value'))
      return button.getAttribute('data-value');
    });
  };

  _getCurrentCat2Filters = function () {
    return this.filters.cat2.filter(function (button) {
      return button.classList.contains('active');
    }).map(function (button) {
      console.log('button value: '+button.getAttribute('data-value'))
      // console.log('button value: '+button.getAttribute('data-value'))
      return button.getAttribute('data-value');
    });
  };



  _handleCategory1Change = function (evt) {
    var button = evt.currentTarget;
    console.log(button)
    // Treat these buttons like radio buttons where only 1 can be selected.
    if (button.classList.contains('active')) {
      button.classList.remove('active');
    } else {
      this.filters.cat1.forEach(function (btn) {
        btn.classList.remove('active');
      });

      button.classList.add('active');
    }

    this.filters.cat1 = this._getCurrentCat1Filters();
    console.log('current cat contains : '+this.filters.cat1);

    this.filter();
  };

  /**
   * A color button was clicked. Update filters and display.
   * @param {Event} evt Click event object.
   */
  _handleCategory2Change = function (evt) {
    var button = evt.currentTarget;

    // Treat these buttons like radio buttons where only 1 can be selected.
    if (button.classList.contains('active')) {
      button.classList.remove('active');
    } else {
      this.filters.cat2.forEach(function (btn) {
        btn.classList.remove('active');
      });

      button.classList.add('active');
    }

    this.filters.cat2 = this._getCurrentCat2Filters();
    console.log('current cat contains : '+this.filters.cat2); 

    this.filter();
  };

  filter = function () {
    if (this.hasActiveFilters()) {
      this.shuffle.filter(this.itemPassesFilters.bind(this));
    } else {
      this.shuffle.filter(Shuffle.ALL_ITEMS);
    }
  };

  itemPassesFilters = function (element) {
    var cat1 = this.filters.cat1;
    var cat2 = this.filters.cat2;
    var cat1 = element.getAttribute('data-category1');
    var cat2 = element.getAttribute('data-category2');

    // If there are active shape filters and this shape is not in that array.
    if (cat1.length > 0 && !cat1.includes(cat1)) {
      return false;
    }

    // If there are active color filters and this color is not in that array.
    if (cat2.length > 0 && !cat2.includes(cat2)) {
      return false;
    }

    return true;
  };

  /**
   * If any of the arrays in the `filters` property have a length of more than zero,
   * that means there is an active filter.
   * @return {boolean}
   */
  hasActiveFilters = function () {
    return Object.keys(this.filters).some(function (key) {
      return this.filters[key].length > 0;
    }, this);
  };




  _handleSearchKeyup(event) {
    this.setState({
      searchTerm: event.target.value.toLowerCase()
    }, () => {
      this.shuffle.filter((element) => {
        return element.dataset.name.toLowerCase().includes(this.state.searchTerm) || element.dataset.username.toLowerCase().includes(this.state.searchTerm);
      })
    })
  }

  componentDidMount() {
    // The elements are in the DOM, initialize a shuffle instance.
    this.shuffle = new Shuffle(this.element.current, {
      itemSelector: '.js-item',
      sizer: this.sizer.current,
    });

    // Kick off the network request and update the state once it returns.
    this._fetchPhotos()
      .then(this._whenPhotosLoaded.bind(this))
      .then((photos) => {
        this.setState({
          photos
        });
      });
  }

  componentDidUpdate() {
    // Notify shuffle to dump the elements it's currently holding and consider
    // all elements matching the `itemSelector` as new.
    this.shuffle.resetItems();
  }

  componentWillUnmount() {
    // Dispose of shuffle when it will be removed from the DOM.
    this.shuffle.destroy();
    this.shuffle = null;
  }



  render() {
      return (
          <div>
              <div id='searchBar'>
                  <input type="text" className='js-shuffle-search' onChange={ this._handleSearchKeyup } value={ this.state.searchTerm } />
              </div>

              <div id='gridActions'>
                <h2>Filter By cat 1</h2>
                  <button onClick={ this._handleCategory1Change } value='all'>All</button>
                  <button onClick={ this._handleCategory1Change } value='art'>Art</button>
                  <button onClick={ this._handleCategory1Change } value='industry'>Industry</button>
                  <button onClick={ this._handleCategory1Change } value='animal'>Animal</button>

                <h2>Filter By cat 2</h2>
                  <button onClick={ this._handleCategory2Change } value='all'>All</button>
                  <button onClick={ this._getCurrentCat1Filters } value='mammals'>Mammals</button>
                  <button onClick={ this._getCurrentCat2Filters } value='insects'>Insects</button>

                  <h2>Sort By</h2>
                  <button onClick={ this._handleSortChange } value='default'>Default</button>
                  <button onClick={ this._handleSortChange } value='date-created'>By Date</button>
                  <button onClick={ this._handleSortChange } value='title'>By Title</button>
                  <button onClick={ this._handleSortChange } value='popularity'>By Popularity</button>
              </div>

              <div ref={ this.element } id='grid' className="row my-shuffle-container shuffle"> {
                  this.state.photos.map(image =>
              <PhotoItem { ...image } />)} 
              <div ref={ this.sizer } className="col-1@xs col-1@sm photo-grid__sizer"></div> 
              </div>
          </div> 
          );
        }
      }


      function PhotoItem({id, src, category1, category2, date_created, popularity, title, name,  username }) {
        return ( 
          <div key={id} 
              className="col-lg-3 js-item" 
              data-name={name}
              data-title={title}
              data-date-created={date_created}
              data-popularity={popularity}
              data-category1={category1}
              data-cetagory2={category2}
              data-username={username}>
              <img src={src} style={{width : "100%",height :"100%"}}/>
          </div>
        )
      }

export default PhotoGrid;

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

Ответы [ 2 ]

0 голосов
/ 16 мая 2019

В отличие от ответа Павла, я думаю, что эта библиотека работает на DOM. Это делает это не реагировать дружественным .

Классические обработчики ввода сохраняют значения в состоянии, используя метод setState. В качестве реакции на изменение состояния программа обновляет / обновляет представление (используя метод render()) в виртуальном DOM. После этого реагируйте обновлениями реального DOM для синхронизации с виртуальным.

В этом случае lib манипулирует реальными элементами DOM - вызов render() (принудительно setState()) перезапишет ранее сделанные Shuffle изменения. Чтобы избежать этого, мы должны избегать использования setState.

Просто сохраните параметры фильтра и сортировки непосредственно в экземпляре компонента (используя this):

_handleSearchKeyup(event){
   this.searchTerm = event.target.value;
   this.shuffle.filter((element) => { /* use this.searchTerm to return matching elements */ } );
}

Инициализируйте все параметры (например, filterCategories, searchTerm, sortBy и sortOrder) в конструкторе и используйте их в одном вызове this.shuffle.filter() (второй параметр для объекта сортировки) при каждом изменении параметра. Подготовьте некоторый помощник для создания комбинированной функции фильтрации (сочетание фильтрации и поиска), сортировка намного проще.

setState можно использовать для кнопки clear all filters - принудительное повторное отображение - не забудьте удалить все параметры в обработчике.


UPDATE

Для порядка сортировки укажите

this.reverse = true; // in constructor
this.orderBy = null;

погрузчики

_handleSortOrderChange = () => {
  this.reverse = !this.reverse
  // call common sorting function
  // extracted from _handleSortChange
  // this._commonSortingFunction()
}

_handleSortByChange = (evt) => {
  this.orderBy = evt.target.value.toLowerCase();
  // call common sorting function
  // extracted from _handleSortChange
  // this._commonSortingFunction()
}

_commonSortingFunction = () => {
  // you can declare sorting functions in main/component scope
  let options = { reverse: this.reverse }      
  const value = this.orderBy;
  if (value == 'date-created') {
    options.by = sortByDate // or this.sortByDate
  } else if (value == 'title') {
    options.by = sortByTitle
  //...
  //this.shuffle.sort(options);

Вы также можете сохранить готовый options объект сортировки в экземпляре компонента (this.options), обновленном обработчиками. Это значение может использоваться _commonSortingFunction() для вызова this.shuffle.sort, но также и для фильтрации функций (второй параметр).

реверсивная кнопка (без привязки)

<button onClick={this._handleSortOrder}>Reverse order</button>

ОБНОВЛЕНИЕ 2

Если вы хотите работать с «нормальной» реакцией, setState вы можете переместить (инкапсулировать) всю фильтрацию (searchBar, gridActions) в отдельный компонент.

Обновление состояния приведет к принудительному пересчету только для «инструментов», не затрагивая элементы, управляемые в реальном DOM случайным образом (родительский объект не перерисовывается). Таким образом, вы можете избежать ручных манипуляций css («активными») с помощью условного рендеринга (плюс еще много возможностей - перечислять активные фильтры отдельно, показывать порядок asc / desc, показывать сброс только при изменении sth и т. Д.).

Передав this.shuffle в качестве реквизита, вы можете просто вызвать поиск / фильтр / сортировку в родительском.

0 голосов
/ 16 мая 2019

Только судя по документации, я еще не пробовал, но должно работать.Экземпляр Shuffle имеет метод filter, который принимает строку или массив строк для фильтрации элементов по «группам» или функцию обратного вызова для выполнения более сложного поиска.Вы должны вызвать this.shuffle.filter после обновления состояния вашего компонента, то есть:

_handleSearchKeyup(event){
   this.setState({searchTerm : event.target.value}, () => {
     this.shuffle.filter((element) => { /* use this.state.searchTerm to return matching elements */ } );
  })
 }

Отредактировано после построения скрипка .Функция обратного вызова просматривает атрибуты data-name и data-username, чтобы проверить, содержат ли они строку поиска

_handleSearchKeyup(event){
  this.setState({searchTerm : event.target.value.toLowerCase()}, () => {
    this.shuffle.filter((element) => {
      return ( 
        element.dataset.name.toLowerCase().includes(this.state.searchTerm) || 
        element.dataset.username.toLowerCase().includes(this.state.searchTerm)
       );
     })
  })
}

Для работы вышеупомянутого вам также необходимо добавить эти атрибуты в узлы DOM, поэтому обновитеPhotoItem компонент:

function PhotoItem({ id, src, name, username }) {
  return (
       <div key={id} 
            className="col-md-3 photo-item" 
            data-name={name} 
            data-username={username}>
            <img src={src} style={{width : "100%",height :"100%"}}/>
       </div>
  )
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...