Redux-form: V6 - How to create a checkbox group

Created on 24 May 2016  路  30Comments  路  Source: redux-form/redux-form

I need to create a slightly complicated checkbox group. I was able to get this working in V5 quite easily by accessing property values within my form code. But, I don't see how to do this in V6.

I have an array of vectorDatasets that I pass into my form using mapStateToProps/connect. Each vectorDataset has an array of vectorFiles. I create a checkbox for each vectorFile.

My model has a list of spatialAverages. Each spatialAverage has a vectorFile. In my form, a vectorFile checkbox should be checked if the array of spatialAverages contains a spatialAverage with spatialAverage.vectorFile.id === vectorDataset.id.

My data looks like this:

  {
    regions: [
      {
        spatialAverages: [
          {
            vectorFile: {
              id
              ...
            }
          }
          ...
        ]
      }
      ...
    ]
    ...
  }

Here's my attempt so far...

<FieldArray name={`${region}.spatialAverages`} component={spatialAverages =>
  <div className="form-section">
    <h3>Spatial Averages</h3>
    {vectorDatasets.map((vectorDataset, i) => (
      <div key={i}>
        <h4>{vectorDataset.datasetId}</h4>
        {vectorDataset.files.map((vectorFile, j) =>
          <div key={j} className="form-checkbox horizontal-group">
            <label>
              <input type="checkbox"
                     name="spatialAverages"
                     value={vectorFile.id}
              />
              {vectorFile.name}
            </label>
          </div>
        )}
      </div>
    ))}
  </div>
}/>

Any suggestions as to how I might achieve this in V6?

docs question

Most helpful comment

import React from 'react';
import { FormGroup, ControlLabel, HelpBlock, Label } from 'react-bootstrap';

const Error = ({ meta : {touched, error} }) =>  (touched && error ? <HelpBlock>{error}</HelpBlock> : null);

const CheckboxGroup = ({ label, required, name, options,  input, meta}) => (
  <FormGroup controlId={name}>
    <ControlLabel>{label} { required && <Label bsStyle="info">required</Label> }</ControlLabel>
      { options.map((option, index) => (
        <div className="checkbox" key={index}>
          <label>
            <input type="checkbox"
                   name={`${name}[${index}]`}
                   value={option.name}
                   checked={input.value.indexOf(option.name) !== -1}
                   onChange={event => {
                     const newValue = [...input.value];
                     if(event.target.checked) {
                       newValue.push(option.name);
                     } else {
                       newValue.splice(newValue.indexOf(option.name), 1);
                     }

                     return input.onChange(newValue);
                   }}/>
            {option.name}
          </label>
        </div>))
      }
    <Error meta={meta} />
  </FormGroup>
);

export default CheckboxGroup;`

All 30 comments

Hey,

I wanted to achieve something similar and ended up creating an adapter:

// adapter.jsx
import React from 'react';
import { Field } from 'redux-form';

export const CHECKBOX = 'CHECKBOX';
export const CHECKBOXES = 'CHECKBOXES';

export default function adapter(key, props) {
  switch (key) {
    case CHECKBOX:
      return (
        <input type="checkbox" {...props} />
      );

    case CHECKBOXES:
      return (
        <div>
          {props.options.map((option, index) => (
            <Field
              key={index}
              name={`${props.name}[${option.value}]`}
              label={option.label}
              checked={!!props.value[option.value]}
              component={CHECKBOX}
            />
          ))}
        </div>
      );

    default:
      return null;
  }
}
/// MyForm.jsx
const weekdayOptions = [
  { value: 1, label: 'Monday' },
  { value: 2, label: 'Tuesday' },
  { value: 3, label: 'Wednesday' },
  { value: 4, label: 'Thursday' },
  { value: 5, label: 'Friday' },
  { value: 6, label: 'Saturday' },
  { value: 7, label: 'Sunday' },
];

<Field
  name="weekdays"
  label="Weekdays"
  component={CHECKBOXES}
  options={weekdayOptions}
/>

@hardchor I'm trying to get a multiselect checkbox working and I'm not sure I follow everything from your example above - can you explain the workflow in a bit more detail? I'm still very new to redux and redux-form and am trying to get some sort of multi-select option working one of my forms but I've spent the better part of 2 days and don't have it functional yet...

I would avoid this method, it create far more connected Field components then is needed. Each state update will re-render all the fields for each checkbox AND run your redux reducers.

better to use a single Field component and manage the state of child <input type="checkbox" /> components within that custom Field component.

@andrewmclagan can you provide example please? :)

import React from 'react';
import { FormGroup, ControlLabel, HelpBlock, Label } from 'react-bootstrap';

const Error = ({ meta : {touched, error} }) =>  (touched && error ? <HelpBlock>{error}</HelpBlock> : null);

const CheckboxGroup = ({ label, required, name, options,  input, meta}) => (
  <FormGroup controlId={name}>
    <ControlLabel>{label} { required && <Label bsStyle="info">required</Label> }</ControlLabel>
      { options.map((option, index) => (
        <div className="checkbox" key={index}>
          <label>
            <input type="checkbox"
                   name={`${name}[${index}]`}
                   value={option.name}
                   checked={input.value.indexOf(option.name) !== -1}
                   onChange={event => {
                     const newValue = [...input.value];
                     if(event.target.checked) {
                       newValue.push(option.name);
                     } else {
                       newValue.splice(newValue.indexOf(option.name), 1);
                     }

                     return input.onChange(newValue);
                   }}/>
            {option.name}
          </label>
        </div>))
      }
    <Error meta={meta} />
  </FormGroup>
);

export default CheckboxGroup;`

@Crazy-Ivan may I ask for an example on how would you implement this if I will put this in a separate component so I can reuse this

I think @Crazy-Ivan comment should become part of the documentation.

@Crazy-Ivan 's answer works this means onChange mutates the state before it dispatches the action?

me thinks that would be VERY beneficial to have in the docs, along with a modified version of Ivan's answer

@goldylucks @aftabnaveed @Crazy-Ivan
If any of you would like to contribute to this project, that's a chance 馃槈 send us your docs PR!

@gustavohenke
I might take a swing at it with some guidance.

I'm using a simplified version of @Crazy-Ivan code.
Seeing how many people waste hours on a simple thing as checkboxes, how about I make a component for that?

with documentations, tests and everything of course :)

@goldylucks Sounds like a plan, I'm currently wasting hours, too :)

Ah, code changes aren't exactly what I was "hired" for, @goldylucks 馃榿
@erikras is the best person to ask some suggestion, but he might take ages to look at this issue, so perhaps you should ping him on Twitter!

Anyway, I support your idea! 馃憤
If you could make sure that your component is not opinionated in terms of what the UI should look like, then IMO it's definitely an addition to the next release.

@gustavohenke
I'm not on twitter :o

The component will be fully customizable, and will support passing className and style for every part, so no worries about that.

For those who spent hours struggling with this,
here comes the rescue which modified from @Crazy-Ivan's

import React, {Component} from 'react';
import {Field} from "redux-form";
import PropTypes from 'prop-types';

export default class CheckboxGroup extends Component {

  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  };

  field = ({input, meta, options}) => {

    const {name, onChange} = input;
    const {touched, error} = meta;
    const inputValue = input.value;

    const checkboxes = options.map(({label, value}, index) => {

      const handleChange = (event) => {
        const arr = [...inputValue];
        if (event.target.checked) {
          arr.push(value);
        }
        else {
          arr.splice(arr.indexOf(value), 1);
        }
        return onChange(arr);
      };
      const checked = inputValue.includes(value);
      return (
        <label key={`checkbox-${index}`}>
          <input type="checkbox" name={`${name}[${index}]`} value={value} checked={checked} onChange={handleChange} />
          <span>{label}</span>
        </label>
      );
    });

    return (
      <div>
        <div>{checkboxes}</div>
        {touched && error && <p className="error">{error}</p>}
      </div>
    );
  };

  render() {
    return <Field {...this.props} type="checkbox" component={this.field} />;
  }
}

And this is how you use it

const options = [
  {label: 'English', value: 'en'},
  {label: '绻侀珨涓枃', value: 'zh-TW'},
  {label: 'Tibetan', value: 'bo'}
];

<CheckboxGroup name="langs" options={options} />

@kmsheng : I tried your solution and it works great, only touched is never updated because input.onBlur is not connected. I found it works best when putting it right before return onChange(arr);, like this:

import React, {Component} from 'react';
import {Field} from "redux-form";
import PropTypes from 'prop-types';

export default class CheckboxGroup extends Component {

  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  };

  field = ({input, meta, options}) => {

    const {name, onChange, onBlur, onFocus} = input;
    const {touched, error} = meta;
    const inputValue = input.value;

    const checkboxes = options.map(({label, value}, index) => {

      const handleChange = (event) => {
        const arr = [...inputValue];
        if (event.target.checked) {
          arr.push(value);
        }
        else {
          arr.splice(arr.indexOf(value), 1);
        }
        onBlur(arr);
        return onChange(arr);
      };
      const checked = inputValue.includes(value);
      return (
        <label key={`checkbox-${index}`}>
          <input type="checkbox" name={`${name}[${index}]`} value={value} checked={checked} onChange={handleChange} onFocus={onFocus} />
          <span>{label}</span>
        </label>
      );
    });

    return (
      <div>
        <div>{checkboxes}</div>
        {touched && error && <p className="error">{error}</p>}
      </div>
    );
  };

  render() {
    return <Field {...this.props} type="checkbox" component={this.field} />;
  }
}

Hello, how a can return this in checkbox group

value: {
  values: ['one', 'two'],
  labels: ['One apple', 'Two apples'],
}

@jorrit : I tried your code, the checkboxes don't get unticked or unticked when they are infocus. The first time it works, but if we try again without removing focus, the checkbox doesn't get unticked and the value is added to the array the second time. Would you know why?
Edit: Removing the call to OnBlur from handleChange makes it work well.

@Jaikant which browser is this? Are you using keyboard navigation to check/uncheck?

@jorrit I am using chrome on Mac with a mouse.

I don't know how I can help you. If the first example works for you, I guess you should use it.

I needed the same, but needed to extend the visualisation with custom components. In my use-case the choices also need to display images. And another one had to display images in another way.

So I created a package for this, based on @kmsheng 's example, located here: https://github.com/leroy0211/form-redux-checkbox-group

It's far from ready yet, and also not available on NPM. So please contribute!

@Crazy-Ivan

Happened to see your answer after Googling for a bit for Group checkbox. Thanks a ton! Got what I needed to work exactly. :)

I was hoping to achieve this without interfering with the onChange handler, here's how I did it if it helps anyone:

I am using redux-form-material-ui too

[
 { id: 'watermelon', label: 'I like watermelon.' },
 { id: 'pineapple', label: 'I like pineapple.' },
 { id: 'banana', label: 'I like bananas.' },
].map(food => (
 <FormControlLabel
   label={food.label}
   control={
     <Field
       name="favouriteFoods"
       component={Checkbox}
       normalize={(value, previousValue) => {
         if (value) {
           if (previousValue.includes(food.id)) return previousValue;
           return [...previousValue, food.id];
         }
         const i = previousValue.indexOf(food.id);
         if (i === -1)  return previousValue;
         return [
           ...previousValue.slice(0, i),
           ...previousValue.slice(i + 1),
         ];
       }}
       format={value => value.includes(food.id)}
     />}
 />
))

@kmsheng how can I handle the function to show preselected checkboxes ?

const checkboxes = options.map(({ label, value, check }, index) => {
      var execution = false
      var checked = check
      const handleChange = (event) => {
        const arr = [...inputValue];

        if (event.target.checked) {
          arr.push(value);
        }
        else {
          arr.splice(arr.indexOf(value), 1);
        }

        checked = !checked

        return onChange(arr);

      };      

      return (
        <FormControlLabel
          control={<Checkbox name={`${name}[${index}]`} value={value} checked={checked} onChange={handleChange} />}
          label={label}
        />
      );
    });

but I am getting this error

TypeError: Invalid attempt to spread non-iterable instance

How can I get this to work with semantic-ui's Checkbox? I'm using redux-form, and to my understanding I need to hook up the component so I can read the value, using onChange and value like so:

onChange={(param, data) => props.input.onChange(data.value)}
value={props.input.value}

I've tried but I don't really understand how to combine this with the group code...

How about "check all/uncheck all" input, help me pls ?

@jorrit Hi! How i can set checked for existing checkbox from outside?

@Crazy-Ivan 's answer works this means onChange mutates the state before it dispatches the action?

me thinks that would be VERY beneficial to have in the docs, along with a modified version of Ivan's answer

Yes the user deployed some underground tech. I am not sure how to feel about it. There was also some intricate destructuring involved with the function params as well. Some serious cheese here,

@kmsheng

For those who spent hours struggling with this,
here comes the rescue which modified from @Crazy-Ivan's

import React, {Component} from 'react';
import {Field} from "redux-form";
import PropTypes from 'prop-types';

export default class CheckboxGroup extends Component {

  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  };

  field = ({input, meta, options}) => {

    const {name, onChange} = input;
    const {touched, error} = meta;
    const inputValue = input.value;

    const checkboxes = options.map(({label, value}, index) => {

      const handleChange = (event) => {
        const arr = [...inputValue];
        if (event.target.checked) {
          arr.push(value);
        }
        else {
          arr.splice(arr.indexOf(value), 1);
        }
        return onChange(arr);
      };
      const checked = inputValue.includes(value);
      return (
        <label key={`checkbox-${index}`}>
          <input type="checkbox" name={`${name}[${index}]`} value={value} checked={checked} onChange={handleChange} />
          <span>{label}</span>
        </label>
      );
    });

    return (
      <div>
        <div>{checkboxes}</div>
        {touched && error && <p className="error">{error}</p>}
      </div>
    );
  };

  render() {
    return <Field {...this.props} type="checkbox" component={this.field} />;
  }
}

And this is how you use it

const options = [
  {label: 'English', value: 'en'},
  {label: '绻侀珨涓枃', value: 'zh-TW'},
  {label: 'Tibetan', value: 'bo'}
];

<CheckboxGroup name="langs" options={options} />

This worked like champ. Thank you.

@Crazy-Ivan, the CheckBoxGroup component is good but in my case I need it in Multi-select-box form. can you or any help in this?
I am struggling for hours with it to work in redux-form!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tejans24 picture tejans24  路  3Comments

srajchevski picture srajchevski  路  3Comments

nygardk picture nygardk  路  3Comments

chienvuhoang picture chienvuhoang  路  3Comments

jaraquistain picture jaraquistain  路  3Comments