使用React.js的Shuffle.js实现

时间:2019-05-16 11:19:37

标签: reactjs sorting search filtering

我正在尝试使用react.js激活shuffle.js组件功能(搜索,过滤和排序)。但是,网站上的文档非常有限。我知道我需要添加搜索输入和一些按钮来完成我想要的操作,但是我不确定如何连接搜索框输入和其他按钮事件来操纵光电栅格(或容器中的其他元素),即被反应渲染。

我已经导入shuffle.js作为节点模块,并在react页面上对其进行了初始化。他们提供的基本代码似乎可以正常工作并显示照片网格,但是,仅此而已。我也想实现搜索,过滤和排序功能,但是在react.js中没有有关如何执行此操作的文档。下面的代码显示了photogrid的实现,但仅此而已。

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 :(得分:0)

仅从文档来看,我还没有尝试过,但是应该可以。 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 */ } );
  })
 }

在构建fiddle之后进行编辑。 回调函数查看data-namedata-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>
  )
}

答案 1 :(得分:0)

与pawel的回答相反,我认为该库在DOM上运行。

经典输入处理程序使用setState方法在状态内保存值。作为状态更改的结果,React刷新/更新了虚拟DOM中的视图(使用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 */ } );
}

在构造函数中初始化所有参数(fe filterCategoriessearchTermsortBysortOrder),并在一个this.shuffle.filter()调用中使用它们(第二个参数用于排序对象)。准备一些帮助程序以创建组合的过滤功能(过滤和搜索的混合),排序要容易得多。

setState可用于clear all filters按钮-强制重新渲染-记住清除处理程序中的所有参数。


更新

用于排序顺序声明

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,仅在更改某项时显示重置等)。

通过传递this.shuffle作为道具,您可以简单地在父级中调用搜索/过滤器/排序。