1
\$\begingroup\$

I am new to react (and javascript in general) but am enjoying learning it. I have decided to make a little multiselect component which should work as follows:

I start with my data in one list, when I select an item from the list it goes into a second list (I can 'unselect' as well).

I also can filter the first list with a text input.

Before you review, I realize that there are likely existing solutions for this functionality that I could use, but I am just doing this to learn.

codepen here https://codepen.io/str8wavedave/pen/ZqWXQp

class MultiSelect extends React.Component {
  render(){
    return(
      <ul>
      {this.props.objects.map((object, index) => <li onClick = {() => this.props.onClickFunction(index)}>{object.name}</li>)}
      </ul>
    );
  }
}

class DoubleMultiselect extends React.Component {
  state = {
    value: '',
    filteredUnselectedObjects: this.props.objects,
    unselectedObjects: this.props.objects,
    selectedObjects: [],
  }
  filterList = (event) => {
    var updatedList = this.state.unselectedObjects;
    updatedList = updatedList.filter(function(item){
      return item.name.toLowerCase().search(
      event.target.value.toLowerCase()) !== -1;
    });
    this.setState({
      filteredUnselectedObjects: updatedList,
      value: event.target.value
    })
  }
  handleSelect = (index) => {
    var obj = this.state.filteredUnselectedObjects[index]
    var newSelected = this.state.selectedObjects.slice()
    newSelected.push(obj)

    var newUnselected = this.state.unselectedObjects.slice()
    var originalIndex = newUnselected.indexOf(obj);
    newUnselected.splice(originalIndex, 1)

    var newFilteredUnselected = this.state.filteredUnselectedObjects.slice()
    newFilteredUnselected.splice(index, 1)

    this.setState({
      selectedObjects: newSelected,
      unselectedObjects: newUnselected,
      filteredUnselectedObjects: newFilteredUnselected
    });
  }

  handleUnselect = (index) => {
    var obj = this.state.selectedObjects[index]
    var newUnselected = this.state.unselectedObjects.slice()
    newUnselected.push(obj)

    var newFilteredUnselected = this.state.filteredUnselectedObjects.slice()
    if(obj.name.toLowerCase().search(this.state.value.toLowerCase()) !== -1)
    {
      newFilteredUnselected.push(obj)
    }
    var newSelected = this.state.selectedObjects.slice()
    newSelected.splice(index, 1)
    this.setState({
      unselectedObjects: newUnselected,
      selectedObjects: newSelected,
      filteredUnselectedObjects: newFilteredUnselected
    });
  }

  render() {  
    return(
      <div>
        <input type="text" onChange={this.filterList}/>
        <MultiSelect objects={this.state.filteredUnselectedObjects} onClickFunction={this.handleSelect}/>
        <MultiSelect objects={this.state.selectedObjects} onClickFunction={this.handleUnselect}/>
      </div>
    );
  }
}
const PLAYERS =[
  {name: 'Charlie', id: 1},
  {name: 'David', id: 2},
  {name: 'Eric', id: 3},
  {name: 'Emily', id: 4},
  {name: 'Peter', id: 5},
  {name: 'Sam', id: 6},
  {name: 'Doug', id: 7}
]
React.render(<DoubleMultiselect objects={PLAYERS}/>, document.getElementById('app'));
\$\endgroup\$
1
\$\begingroup\$

Overall this is good, but there are few things we can do improve this and make it more "modular".

Lets say someone wants to use your component and instead of an array of player objects, they have just an array of player names, this introduces a couple of problems.

  1. The prop to pass this in the list is called objects which doesn't reflect the type of data that they have and their list is long so they can't manually change it.
  2. Your component is expecting the data passed in objects to be an array of objects.

How can we resolve this?

Lets first rename the prop from objects -> data, this is better because it generalizes what this prop receives and the person using this can possibly assume you are able to handle multiple data types.

Next, we should update places where we are assuming the data to be an object and instead check to see if it is an object before handling it. One example is in <MultiSelect />, where you are iterating over each item. We can change that to be:

class MultiSelect extends React.Component {
  render() {
    return (
      <ul>
        {this.props.data.map((item, index) => (
          <li onClick={() => this.props.onClickFunction(index)}>
            {typeof item === "object" ? item.name : item}
          </li>
        ))}
      </ul>
    );
  }
}

Now we can display data from an array of strings!

But what if the person using your component was using an array of objects? Unfortunately there is no documentation so when he created the objects he used the property name firstName instead of name. This now breaks the code we re-wrote above, but we can fix it...

Lets set a new prop on <DoubleMultiselect /> called something like nameField, which would be a string that is equal to the object property to display in the list.

So now wherever we were accessing the name property of the player we can replace it with this.props.nameField, re-writing the code above can now be:

class MultiSelect extends React.Component {
  render() {
    return (
      <ul>
        {this.props.data.map((item, index) => (
          <li onClick={() => this.props.onClickFunction(index)}>
            {typeof item === "object" ? item[this.props.nameField]: item}
          </li>
        ))}
      </ul>
    );
  }
}

We've now added support for dynamic data, allowing anyone with any type of data to use this!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.