2

I'm trying to activate shuffle.js component functionality (search, filter and sort) with react.js. However, the documentation on the website is very limited. I know that I need to add a search input and some buttons to do what I want, yet I'm not sure how to connect my search box input and other button events to manipulate the photogrid (or other elements within a container) that is being rendered by react.

I have imported shuffle.js as node module and initialised it on the react page. The basic code that they provide seems to be working and displays the photo grid, however, that's pretty much it. I also want to implement the search, filtering and sorting functionality but there isn't documentation on how to do that in react.js. The code below shows the photogrid implementation but nothing else.

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;

The photogrid right now does nothing, just displays photos which I'm unable to search, filter and sort.

2 Answers 2

1

Only judging by the documentation, I haven't tried it yet, but should work. The instance of Shuffle has a filter method, which takes a string, or an array of strings, to filter the elements by "groups", or a callback function to perform more complicated search. You should call this.shuffle.filter after updating the state of your component, i.e.:

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

Edited after building a fiddle. The callback function looks at data-name and data-username attributes to check if they contain the search string

_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)
       );
     })
  })
}

For the above to work you also need to add these attributes to the DOM nodes, so update the PhotoItem component:

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>
  )
}
Sign up to request clarification or add additional context in comments.

2 Comments

the search indeed works. Would you know how add sort and filter option in there if the buttons to filter and sort were present DOM and events were attached to it? I'm surprised there is component for react.js yet it's not really documented how to implement it in react.
It is not complicated to make it work with filters and search by following the examples, but it's hacking around React instead of using its advantages, as @xadm stated in the other answer. That said there are some valuable libraries out there that operate on the real DOM and we have rhe refs escape hatch to be able to use them with React if there's no better alternative.
0

In opposition to pawel's answer I think that this library operates on DOM. It makes this not react friendly.

Classic input handlers saves values within state using setState method. As an effect to state change react refreshes/updates the view (using render() method) in virtual DOM. After that react updates real DOM to be in sync with virtual one.

In this case lib manipulates on real DOM elements - calling render() (forced by setState()) will overwritte earlier changes made by Shuffle. To avoid that we should avoid using setState.

Simply save filter and sorting parameters directly within component instance (using this):

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

Initialize all the params (f.e. filterCategories, searchTerm, sortBy and sortOrder) in constructor and use them in one this.shuffle.filter() call (second parameter for sort object) on every parameter change. Prepare some helper to create combined filtering function (mix of filtering and searching), sorting is far easier.

setState can be used for clear all filters button - forced rerendering - remember to clear all parameters within handler.


UPDATE

For sorting order declare

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

handlers

_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);

You can also store ready options sorting object in component instance (this.options) updated by handlers. This value can be used by _commonSortingFunction() to call this.shuffle.sort but also by filtering functions (second parameter).

reversing button (no need to bind)

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

UPDATE 2

If you want to work with 'normal' react, setState you can move (encapsulate) all the filtering (searchBar, gridActions) into separate component.

State update will force rerendering only for 'tools', not affecting elements managed in real DOM by shuffle (parent not rerendered). This way you can avoid manual css manipulations ('active') by using conditional rendering (plus many more possibilities - list active filters separately, show order asc/desc, show reset only when sth changed etc.).

By passing this.shuffle as prop you can simply invoke search/filter/sort in parent.

8 Comments

It's unlikely that I will be able to follow your instructions due to my lack of expertise in react, perhaps you would be able to provide some guidance? I'll be able to initiate constructor fields, but not sure how I would use parameters within the filter method.
Just try, you can start with search (following pawel's method but using this. instead this.state., w/o setState). Sorting should be easy, too. Come back with a new question/problems (with working example - codesandbox or stackblitz).
Hi xadm, I added my code and the sort works fine (apart from button click would keep reversing the order, only does it once). Yet the filter doesn't seem to work. I used console.log to check what's being stored in states (couldn't make it work without states) and the data values from the buttons are not stored in states, hence when I pass it to the _handleCategory1Change and _handleCategory2Change it does nothing. Hope that makes sense? Also, am I not suppose to use forEach in react?
duplicated var names cat1, cat2 in itemPassesFilters
I changed the cat1 and cat2 in the itemPassesFilters() if that's what you meant. Still not working.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.