【问题标题】:Shuffle.js implementation with React.js使用 React.js 实现 Shuffle.js
【发布时间】:2019-10-03 15:52:09
【问题描述】:

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

我已将 shuffle.js 作为节点模块导入并在反应页面上对其进行了初始化。他们提供的基本代码似乎可以正常工作并显示照片网格,但仅此而已。我还想实现搜索、过滤和排序功能,但是在 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;

photogrid 现在什么都不做,只显示我无法搜索、过滤和排序的照片。

【问题讨论】:

    标签: reactjs sorting search filtering


    【解决方案1】:

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

    【讨论】:

    • 搜索确实有效。如果用于过滤和排序的按钮存在 DOM 并且附加了事件,您知道如何在其中添加排序和过滤选项吗?我很惊讶有 react.js 的组件,但它并没有真正记录如何在 react 中实现它。
    • 按照示例使其与过滤器和搜索一起使用并不复杂,但是正如@xadm 在另一个答案中所述,它是围绕 React 进行黑客攻击而不是利用其优势。也就是说,有一些有价值的库可以在真实的 DOM 上运行,如果没有更好的选择,我们有 rherefs 逃生舱可以将它们与 React 一起使用。
    【解决方案2】:

    与pawel 的回答相反,我认为这个库在DOM 上运行。这使得这反应不友好

    经典输入处理程序使用setState 方法将值保存在状态中。作为状态更改的效果,react 刷新/更新虚拟 DOM 中的视图(使用 render() 方法)。之后,react 会更新真实 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 */ } );
    }
    

    在构造函数中初始化所有参数(例如 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 您可以将所有过滤(搜索栏、网格操作)移动(封装)到单独的组件中。

    状态更新将强制仅对“工具”进行重新渲染,而不影响在真实 DOM 中通过 shuffle 管理的元素(父级未重新渲染)。这样,您可以通过使用条件渲染来避免手动 css 操作(“活动”)(加上更多可能性 - 分别列出活动过滤器、显示顺序 asc/desc、仅在某事更改时显示重置等)。

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

    【讨论】:

    • 由于我缺乏反应方面的专业知识,我不太可能按照您的指示进行操作,也许您可​​以提供一些指导?我将能够启动构造函数字段,但不确定如何在过滤器方法中使用参数。
    • 试试吧,你可以从搜索开始(按照 pawel 的方法,但使用this. 而不是this.state.,不带setState)。排序也应该很容易。回来提出一个新问题/问题(带有工作示例 - 代码沙盒或堆栈闪电战)。
    • 嗨 xadm,我添加了我的代码并且排序工作正常(除了按钮单击会不断反转顺序,只执行一次)。然而过滤器似乎不起作用。我使用 console.log 检查存储在状态中的内容(如果没有状态就无法工作),并且来自按钮的数据值未存储在状态中,因此当我将其传递给 _handleCategory1Change 和 _handleCategory2Change 时,它​​什么也不做。希望这有意义吗?另外,我不应该在反应中使用 forEach 吗?
    • itemPassesFilters 中重复的变量名称cat1cat2
    • 如果这就是您的意思,我更改了 itemPassesFilters() 中的 cat1 和 cat2。还是不行。
    猜你喜欢
    • 2022-06-21
    • 1970-01-01
    • 2014-07-02
    • 1970-01-01
    • 2021-12-22
    • 2021-06-16
    • 1970-01-01
    • 2017-01-06
    • 1970-01-01
    相关资源
    最近更新 更多