How to render "N items selected" rather than the list of N selected items with react-select
Asked Answered
A

4

15

I'm looking into using react-select as a selector for a city-picker, where users can pick 1 or multiple cities to filter some data on. Here is a screenshot of it rendered in my page: city selector

The city list can be large, and I don't want the selector to grow outside of its blue container if a large number are selected at once. Here is what happens when I simulate that now:

enter image description here

I'm not a huge fan of that! One alternative I can think of is to render "4 cities selected" instead of the entire list. This will have a predictable size on the page.

How can this be done with react-select?

Auriol answered 18/1, 2018 at 14:35 Comment(2)
have you tried setting the autosize prop to false ?Blesbok
@MassimilianoJanes : autosize property is not working . Please refer this github.com/JedWatson/react-select/issues/860Corduroys
E
11

Note: this answer is for react-select v1. See the answer by NearHuscarl for a solution for v3.

Rendering "N items selected"

This can be achieved with the valueRenderer and className props and a minimal amount of CSS.

Here I'm showing the first three selections normally, and then "N items selected" when 4+ items have been selected. It makes no sense to show the remove selection icon (×) besides "N items selected," so I also removed that (with CSS).

class App extends React.Component {
  state = {
    value: [],
  }
  className = () => {
    const baseClassName = 'my-react-select';
    
    if (this.state.value.length <= 3) {
      return baseClassName;
    }
    
    return `${baseClassName} ${baseClassName}--compact`;
  }
  handleChange = (value) => {
    this.setState({ value });
  }
  renderValue = (option) => {
    // The first three selections are rendered normally
    if (this.state.value.length <= 3) {
      return option.label;
    }

    // With more selections, render "N items selected".
    // Other than the first one are hidden in CSS.
    return <span>{this.state.value.length} items selected</span>;
  }
  render() {
    return (
      <Select
        className={this.className()}
        multi
        onChange={this.handleChange}
        options={[
          { value: 'zero',  label: 'Zero' },
          { value: 'one',   label: 'One' },
          { value: 'two',   label: 'Two' },
          { value: 'three', label: 'Three' },
          { value: 'four',  label: 'Four' },
          { value: 'five',  label: 'Five' },
          { value: 'six',   label: 'Six' },
          { value: 'seven', label: 'Seven' },
          { value: 'eight', label: 'Eight' },
          { value: 'nine',  label: 'Nine' },
        ]}
        value={this.state.value}
        valueRenderer={this.renderValue}
      />
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
.my-react-select {
  /* Custom styles */
}

.my-react-select--compact .Select-value:first-child {
  font-style: italic;
}
.my-react-select--compact .Select-value:first-child .Select-value-icon,
.my-react-select--compact .Select-value:nth-child(n+2) {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<script src="https://unpkg.com/[email protected]/prop-types.js"></script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-input-autosize.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-select.js"></script>

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/react-select.css">

<div id="root"></div>

Alternative approach

Looking at your screenshots, it looks like there's space to show up to four selections without making the selector overflow. Instead of showing "N items selected" when 4+ cities have been selected, you could show the first 3 selections normally and then "+N more." Like this:

  • City A
  • City A, City B
  • City A, City B, City C
  • City A, City B, City C, + 1 more
  • City A, City B, City C, + 2 more
  • City A, City B, City C, + 3 more
  • etc.

From UX perspective, I think it's good to show the first 3 or so selections normally. It's confusing if every selection is suddenly hidden behind the text "4 items selected" after the 4th city is selected.

This solution is very similar than the first one. The className prop is now simply a string. The renderValue method and the CSS selectors are a bit different.

class App extends React.Component {
  state = {
    value: [],
  }
  handleChange = (value) => {
    this.setState({ value });
  }
  renderValue = (option) => {
    // The first three values are rendered normally
    if (this.state.value.indexOf(option) < 3) {
      return option.label;
    }

    // Render the rest as "+ N more". 
    // Other than the first one are hidden in CSS.
    return <span>+ {this.state.value.length - 3} more</span>;
  }
  render() {
    return (
      <Select
        className='my-react-select'
        multi
        onChange={this.handleChange}
        options={[
          { value: 'zero',  label: 'Zero' },
          { value: 'one',   label: 'One' },
          { value: 'two',   label: 'Two' },
          { value: 'three', label: 'Three' },
          { value: 'four',  label: 'Four' },
          { value: 'five',  label: 'Five' },
          { value: 'six',   label: 'Six' },
          { value: 'seven', label: 'Seven' },
          { value: 'eight', label: 'Eight' },
          { value: 'nine',  label: 'Nine' },
        ]}
        value={this.state.value}
        valueRenderer={this.renderValue}
      />
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
/* If you change the amount of how many selections are shown normally,
 * be sure to adjust these selectors accordingly. */
.my-react-select .Select-value:nth-child(4) {
  font-style: italic;
}
.my-react-select .Select-value:nth-child(4) .Select-value-icon,
.my-react-select .Select-value:nth-child(n+5) {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<script src="https://unpkg.com/[email protected]/prop-types.js"></script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-input-autosize.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-select.js"></script>

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/react-select.css">

<div id="root"></div>

Here's another approach of showing the selections:

  • City A
  • City A, City B
  • City A, City B, City C
  • City A, City B, City C, City D
  • City A, City B, City C, + 2 more
  • City A, City B, City C, + 3 more
  • etc.

From UX perspective, it's a bit silly to show "+ 1 more" instead of showing the value, so in my opinion this is the best option.

The renderValue method is once again a bit different. The CSS selectors are now a bit uglier and more complex, but they work.

class App extends React.Component {
  state = {
    value: [],
  }
  handleChange = (value) => {
    this.setState({ value });
  }
  renderValue = (option) => {
    // The first four values are rendered normally
    if (this.state.value.length <= 4) {
      return option.label;
    }

    // The first 3 values are rendered normally when
    // more than 4 selections have been made
    if (this.state.value.indexOf(option) < 3) {
      return option.label;
    }

    // Render the rest as "+ N more".
    // Other than the first one are hidden in CSS.
    return <span>+ {this.state.value.length - 3} more</span>;
  }
  render() {
    return (
      <Select
        className='my-react-select'
        multi
        onChange={this.handleChange}
        options={[
          { value: 'zero',  label: 'Zero' },
          { value: 'one',   label: 'One' },
          { value: 'two',   label: 'Two' },
          { value: 'three', label: 'Three' },
          { value: 'four',  label: 'Four' },
          { value: 'five',  label: 'Five' },
          { value: 'six',   label: 'Six' },
          { value: 'seven', label: 'Seven' },
          { value: 'eight', label: 'Eight' },
          { value: 'nine',  label: 'Nine' },
        ]}
        value={this.state.value}
        valueRenderer={this.renderValue}
      />
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
/* If you change the amount of how many selections are shown normally,
 * be sure to adjust these selectors accordingly. */
.my-react-select .Select-value:nth-child(4):not(:nth-last-child(2)) {
  font-style: italic;
}
.my-react-select .Select-value:nth-child(4):not(:nth-last-child(2)) .Select-value-icon,
.my-react-select .Select-value:nth-child(n+5) {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<script src="https://unpkg.com/[email protected]/prop-types.js"></script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-input-autosize.js"></script>
<script src="https://unpkg.com/[email protected]/dist/react-select.js"></script>

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/react-select.css">

<div id="root"></div>
Equipage answered 22/1, 2018 at 8:18 Comment(10)
the above solution didnt work for me. It is actually overriding the value being set in the first if loop and it is displaying +1 more multiple time. Any inputs please. this is the code which i am using renderValue =(option) =>{ if(this.state.value.length<2){ return option.label } return <span>+ {this.state.value.length - 1} more</span>; }Corduroys
@RakeshNallam Did you adjust and apply those CSS styles? The :nth-child(n+5) selector hides selections starting from the fifth selection. You have < 2 in your if condition, so I assume you want to display the first two selections normally. If so, change the CSS selectors to match that. (Thanks for the heads-up, I'll update my answer with a note regarding this.)Equipage
The CSS was not getting applied. I fixed that issue. The issue with I have right now is when two or more item are selected. It is only executing the default return case i.e. return <span>+ {this.state.value.length - 3} more</span>; as a result when two values are selected. Its showing Two More , Three More etc.Corduroys
@RakeshNallam Oh, now I see the cause of your problem. You have if (this.state.value.length < 2), but you should have if (this.state.value.indexOf(option) < 1) (like in my code) so that the first selection is rendered normally (return option.label;).Equipage
Thanks for the awesome helpCorduroys
This looks exactly what I want, but how do I achieve it with the new API where I should use components instead?Weimaraner
@TomasJansson I have never used react-select v3 or v4, so unfortunately I have no idea.Equipage
@MatiasKinnunen, thanks for reply. The way I did it was to create a ValueContainer component where I extract the childrens I want to render. inspired what you posted here.Weimaraner
@TomasJansson Nice. Post your solution as an answer? :-)Equipage
@MatiasKinnunen, I should have scrolled down before I did my solution, it is basically the same as the answer below :)Weimaraner
G
11

Here is my updated answer using react-select 3.x. No css involved. I override the ValueContainer's children with my custom multi-values messages. Before that you need to import the following things:

import React from "react";
import Select, { components } from "react-select";

Variation 1

Display generic message: n items selected

const ValueContainer = ({ children, ...props }) => {
  let [values, input] = children;

  if (Array.isArray(values)) {
    const plural = values.length === 1 ? "" : "s";
    values = `${values.length} item${plural} selected`;
  }

  return (
    <components.ValueContainer {...props}>
      {values}
      {input}
    </components.ValueContainer>
  );
};

Result

enter image description here

Variation 2

Display a number of named items before showing the generic message: item1, item2, item3 and n others selected

const ValueContainer = ({ children, ...props }) => {
  let [values, input] = children;

  if (Array.isArray(values)) {
    const val = (i: number) => values[i].props.children;
    const { length } = values;

    switch (length) {
      case 1:
        values = `${val(0)} selected`;
        break;
      case 2:
        values = `${val(0)} and ${val(1)} selected`;
        break;
      case 3:
        values = `${val(0)}, ${val(1)} and ${val(2)} selected`;
        break;
      default:
        const plural = values.length === 3 + 1 ? "" : "s";
        const otherCount = length - 3;
        values = `${val(0)}, ${val(1)}, ${val(
          2
        )} and ${otherCount} other${plural} selected`;
        break;
    }
  }
  return (
    <components.ValueContainer {...props}>
      {values}
      {input}
    </components.ValueContainer>
  );
};

enter image description here

Usage

<Select
  {...}
  isMulti
  hideSelectedOptions={false}
  components={{ ValueContainer }}
/>

Live Demo

Edit react-select Custom Multi-Value Messages

Goat answered 29/8, 2020 at 12:18 Comment(5)
thanks for this fix...there is one issue where the container is not closing on blur, could you please address this.Timmi
@Timmi Sorry about that, just remove the closeMenuOnSelect prop, it should work fine now.Goat
thanks for the response, removing that prop does not do any good, now it closes every time a value is selected which is inconvenient for multi selectTimmi
@Timmi okay, looks like the component doesn't work properly unless I hoist the definition. This should definitely work now (I'll retire right on the spot if it doesn't).Goat
@Timmi I've added the type definition of the custom component in my Codesandbox.Goat
C
0

In my opinion, I will override css to make size fixed. It's the matter of css. I will inspect element and make changes. enter image description here

Let's see above example. This is the css you need to adjust:

.Select-control {
background-color: #fff;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
color: #333;
cursor: default;
display: table;
border-spacing: 0;
border-collapse: separate;
height: 36px;
outline: none;
overflow: hidden;
position: relative;
width: 100%;

}

Now remove display: table; and add a proper height. enter image description here

Consecrate answered 22/1, 2018 at 8:1 Comment(0)
O
0

An easier way to achieve this when using a controlled input in 2024:

<Select
  // everything else, N = max to display, should not be >0
  isMulti
  controlShouldRenderValue={values.length <= N}
  hideSelectedOptions={false}  // otherwise can't unselect, only clear all
  placeholder={values.length ? `${values.length} values selected` : 'Placeholder'}
  values={values}
/>
        
Ouellette answered 28/7 at 23:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.