Ycon Ycon - 3 months ago 6
React JSX Question

Passing list of values as filtertable content

I'm attempting to filter using a list of values with React.

All my "tags" have a "taglevel" to indicate their relevance.

I want it to "cancel out" tags which are the same (ie don't repeat the tag if its' the same).


  • I want the first row to show all tag.name with "taglevel" of 1.

  • I want the second row to show all tag.name with "taglevel" of 2 or more.



I am unable to show and filter on the value "tags". Possibly it is around line 145 of my codepen where I have made the error.

Here is what I am trying to achieve:

enter image description here

I've put this together in a codepen.

http://codepen.io/yarnball/pen/GqbyWr?editors=1010

Without success, I have now tried the following:

I tried filtering using this using:

var LevelFilter = React.createClass({
render: function(){
if (!this.props.tags) return null;
return this.props.tags.filter(tag => tag.taglevel === this.props.targetLevel).map(tag => <a onClick={this.props.onClick}>{tag.name}</a>);
}
});


Then trying to get it in my return here:

render: function(){
...
var getUniqueCategories=[];
PHOTODATA.forEach(function(el){
if(getUniqueCategories.indexOf(el.tag) === -1 ) getUniqueCategories.push(el.tag);
})

return (
<div className="overlay-photogallery">
<div className="filter-panel"><b>Tags with taglevel 1 only to be displayed</b>
{
getUniqueCategories.map(function(el,i){
var boundClick = titleToSelect.bind(null,el);
return <LevelFilter onClick={boundClick} targetLevel={1} tags={el.tag} />
})

}
<a className="resetBtn" onClick={this.resetFilter}> Reset Filter </a>
</div>


My data looks like this:

"title": "Into the Wild",
"tag": [
{
"name": "Movie",
"taglevel": 1,
"id": 1
},
{
"name": "Adventure",
"taglevel": 2,
"id": 30
},
{
"name": "Book",
"taglevel": 1,
"id": 2
}
],
"info": []
}

Answer

TL;DR

You have some serious issues with your array manipulation and your React components.

Remember that React advocates a specific top down structure and you should read up on it some more. Each React Component should use props as much as possible and ideally only 1 top-level component should hold state.


QUICK ways forward:

  1. Pass all the data down and let each level filter make the list unique.
  2. Seriously, split up your components and let them depend on props as much as possible.
  3. Give variables meaningful names. el is not meaningful and in your case refers to PHOTO items in the PHOTODATA array, tags in a PHOTO and then you use element to mean something else again. Don't go to over the top, but at least be able to identify what the variable is supposed to do.

I've given in and made a codepen with a much updated structure. The behaviour may not be exactly what you're looking for, but look at the code and how it is organised and how information is shared and passed between components.

http://codepen.io/anon/pen/AXGGLy?editors=1010


UPDATE To allow multiple filters two methods should be updated:

selectTag: function (tag) {
    this.setState({
      displayedCategories: this.state.displayedCategories.concat([tag])
    });
}

tagFilter: function (photo) {
  return this.props.displayedCategories.length !== 0 && 
    this.props.displayedCategories.every(function(thisTag) {
      return photo.tag.some(function (photoTag) {
        return photoTag.id === thisTag.id &&
          photoTag.taglevel === thisTag.taglevel;
      });
    });
},

selectTag now appends to the displayedCategories array rather than replacing it.

tagFilter now checks that at least one filter has been applied (remove this.props.displayedCategories.length !== 0 to disable this) so that it doesn't display all by default and then checks that every selected filter is present in each photo, thus making the components additive.

There are further improvements that could be made, such as to disable a level when a filter is applied at that level (one choice per level) or to show a list of applied filters, either through colour on the buttons or a tag list above the results.

(codepen updated with these latest changes)


Ok, there are a few problems with your codepen.

First, on line 137 you extract the tag array from the object:

if(getUniqueCategories.indexOf(el.tag) === -1 ) getUniqueCategories.push(el.tag);

Then, on 146 you extract it again:

return <LevelFilter onClick={boundClick} targetLevel={1} tags={el.tag} />

and again for level 2:

return <LevelFilter onClick={boundClick} targetLevel={2} tags={el.tag} />

For both of these it should be:

return <LevelFilter onClick={boundClick} targetLevel={n} tags={el} />

Which then allows another problem to manifest itself, which is that LevelFilter doesn't return a valid React component (an array is not valid).

return this.props.tags.filter(tag => tag.taglevel === this.props.targetLevel).map(tag => <a onClick={this.props.onClick}>{tag.name}</a>);

should be

return (
      <div>            
          {
            this.props.tags
                .filter(tag => tag.taglevel === this.props.targetLevel)
                .map(tag => <a onClick={this.props.onClick}>{tag.name}</a>)
          }
      </div>
    );

After these changes you should have a much closer attempt to where you want to be.

There are further issues you will need to look into, things like your boundClick function won't work correctly because you only have a list of tags, not PHOTODATA.

However, just a final thought. You might want to break your React components up a little more.


For reference, here is the full code listing from the codepen:

var PHOTODATA = [{
        "title": "Into the Wild",
        "tag": [
            {
                "name": "Movie",
                "taglevel": 1,
                "id": 1
            },
            {
                "name": "Adventure",
                "taglevel": 2,
                "id": 30
            },
            {
                "name": "Book",
                "taglevel": 1,
                "id": 2
            }
        ],
        "info": []
    },{
        "title": "Karate Kid",
        "tag": [
            {
                "name": "Movie",
                "taglevel": 1,
                "id": 1
            },
            {
                "name": "Adventure",
                "taglevel": 2,
                "id": 30
            },
            {
                "name": "Kids",
                "taglevel": 3,
                "id": 4
            }
        ],
        "info": []
    },
        {
        "title": "The Alchemist",
        "tag": [
            {
                "name": "Book",
                "taglevel": 1,
                "id": 2
            },
            {
                "name": "Adventure",
                "taglevel": 2,
                "id": 30
            },
            {
                "name": "Classic",
                "taglevel": 2,
                "id": 4
            },
            {
                "name": "Words",
                "taglevel": 4,
                "id": 4
            }
        ],
        "info": []
    }];


var PhotoGallery = React.createClass({

    getInitialState: function() {
      return {
        displayedCategories: []
      };
    },

    selectTag: function (tag) {
        this.setState({
          displayedCategories: this.state.displayedCategories.concat([tag])
        });
    },

    resetFilter: function(){
      this.setState({
        displayedCategories: []
      });
    },

    render: function(){
        var uniqueCategories = PHOTODATA.map(function (photo) {
            return photo.tag; // tag is a list of tags...
        }).reduce(function (uniqueList, someTags) {
            return uniqueList.concat(
              someTags.filter(function (thisTag) {
                return !uniqueList.some(function(uniqueTag) {
                  return uniqueTag.id === thisTag.id && uniqueTag.taglevel === thisTag.taglevel
                });
              })
            );
        }, []);
        return (
            <div className="overlay-photogallery">
                <div className="filter-panel"><b>Tags with taglevel 1 only to be displayed</b>
                    <PhotoGalleryLevel level={1} tags={uniqueCategories} displayedCategories={this.state.displayedCategories} selectTag={this.selectTag} />
                    <a className="resetBtn" onClick={this.resetFilter}> Reset Filter </a>
                </div>
                <div className="filter-panel"><b>Tags with taglevel 2 only to be displayed</b>
                  <PhotoGalleryLevel level={2} tags={uniqueCategories} displayedCategories={this.state.displayedCategories} selectTag={this.selectTag} />
                </div>
                <div className="PhotoGallery">
                    <PhotoDisplay displayedCategories={this.state.displayedCategories} photoData={PHOTODATA} />
                </div>
            </div>
            );
    }
});

var PhotoGalleryLevel = React.createClass({
  render: function () {
    var filteredTags = this.props.tags.filter(function (tag) {
      return tag.taglevel === this.props.level;
    }.bind(this));
    var disabled = this.props.displayedCategories.some(function (tag) {
      return tag.taglevel === this.props.level;
    }.bind(this));
    return (
      <div>
        {filteredTags.map(function (tag){
          return <PhotoGalleryButton tag={tag} selectTag={this.props.selectTag} disabled={disabled} />;
        }.bind(this))}
      </div>
    );
  }
});

var PhotoGalleryButton = React.createClass({
  onClick: function (e) {
    this.props.selectTag(this.props.tag);
  },

  render: function () {
    return (
      <a className={this.props.disabled} onClick={this.onClick}>{this.props.tag.name}</a>
    );
  }
});

var PhotoDisplay = React.createClass({
  getPhotoDetails: function (photo) {
    console.log(this.props.displayedCategories, photo);
      return (
        <Photo title={photo.title} name={photo.name} tags={photo.tag} />
      );
  },

  tagFilter: function (photo) {
    return this.props.displayedCategories.length !== 0 && 
      this.props.displayedCategories.every(function(thisTag) {
        return photo.tag.some(function (photoTag) {
          return photoTag.id === thisTag.id &&
            photoTag.taglevel === thisTag.taglevel;
        });
      });
  },

  render: function () {
    return (
      <div>
        {this.props.photoData.filter(this.tagFilter).map(this.getPhotoDetails)}
      </div>
    );
  }
});

var Photo =  React.createClass({
    getTagDetail: function (tag){
      return (
        <li>{tag.name} ({tag.taglevel})</li>
      );
    },

  sortTags: function (tagA, tagB) {
    return tagA.taglevel - tagB.taglevel;
  },

    render: function(){
        return (
            <div className="photo-container" data-title={this.props.title} >
                {this.props.title}
                <ul>
                    {this.props.tags.sort(this.sortTags).map(this.getTagDetail)}
                </ul>
            </div>
        );
    }
});

ReactDOM.render(<PhotoGallery />, document.getElementById('main'));