Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactJS - managed checkbox groups

Tags:

reactjs

As a way of helping me to learn ReactJS, I'm setting up something which really ought to be easy, but has proved to be somewhat tricky for me.

I want to set up some managed checkbox groups in ReactJS. In HTML, a checkbox "field" actually consists of a number of input type="checkbox" elements that share a common NAME property. As I understand it, this is just the kind of UI element that ought to fit the compositional nature of ReactJS.

I have two ReactJS components:

First, CheckboxField is for each individual entry in the checkbox group - i.e, each input type="checkbox" HTML element.

Second, CheckboxFieldGroup is for each group of checkbox entries - i.e each bunch of HTML elements that share a common NAME property. The CheckboxFieldGroup component creates a number of CheckboxField components, based on the initial props that are passed into it.

State is managed in the CheckboxFieldGroup component rather than at the individual CheckboxField level. From what I've read, you should manage state as the highest level that makes sense. And to me, it makes more sense to have it at the CheckboxFieldGroup level.

When CheckboxFieldGroup first runs, its initial state is created, as an array, from its initial props, also an array. The render method (actually the renderChoices method) loops through its state array, and passes each state member's properties down to a CheckboxField component as the latter's props. When the user ticks/unticks one of the checkboxes, that event is passed via a callback to the handleChange method of CheckboxFieldGroup, its owner. This method determines which of the checkboxes has been changed by interrogating its id property, and then makes the corresponding change to the correct member of CheckboxFieldGroup's state array via a setState() call. This causes CheckboxFieldGroup to automatically re-render, with the new state array being passed down to the individual CheckboxField components, so that's everything in sync.

/** @jsx React.DOM */

var CheckboxField = React.createClass({
    propTypes: {
        values: React.PropTypes.object.isRequired
    },
    getDefaultProps: function () {
        return {
            values: {
                label: "Place holder text"                
            }
        };
    },
    render: function() {
        return (
            <label htlmFor={this.props.values.id}>
                <input type="checkbox"
                    name={this.props.values.name}
                    id={this.props.values.id}
                    value={this.props.values.value}
                    checked={this.props.values.checked}
                    onChange={this.handleChange} />
                {this.props.values.label} <br />
            </label>
        );
    },
    handleChange: function(event) {
        // Should use this to set parent's state via a callback func.  Then the
        // change to the parent's state will generate new props to be passed down
        // to the children in the render().
        this.props.callBackOnChange(this, event.target.checked);
    }
});


var CheckboxFieldGroup = React.createClass({
    propTypes: {
        defaultValues: React.PropTypes.object.isRequired
    },
    getInitialState: function () {
        // default props passed in to CheckboxFieldGroup (this componenent) will be used to set up the state.  State
        // is stored in this component, and *not* in the child CheckboxField components.  The state store in this
        // component will, in turn, generate the props for the child CheckboxField components.  When the latter
        // are updated (i.e. clicked) by the user, then the event will call the handleChange() function in
        // this component.  That will generate update this component's state, which in turn will generate
        // new props for the child CheckboxField components, which will cause those components to re-render!
        var that = this;
        var initStateArray = this.props.defaultValues.valuesArray.map(function(choice, i) {
            var tempObj = {
                name: that.props.defaultValues.name,
                value: choice.value,
                label: choice.label,
                id: _.uniqueId("choice"),
                checked: choice.checked
            };
            return tempObj;
        });
        return {valuesArray: initStateArray};
    },
    renderChoices: function() {
        var that = this; // Could also use .bind(this) on our map() function but that requires IE9+.
        return this.state.valuesArray.map(function(choice, i) {
            return CheckboxField({
                values: {
                    name: that.props.defaultValues.name,
                    value: choice.label,
                    label: choice.label,
                    id: choice.id,
                    checked: choice.checked
                },
                callBackOnChange: that.handleChange
            });
        });
    },
    render: function () {
        return (
            <form>
                {this.renderChoices()}
            </form>
        );
    },
    handleChange: function(componentChanged, newState) {
        // Callback function passed from CheckboxFieldGroup (this component) to each of the
        // CheckboxField child components.  (See renderChoices func).
        var idx = -1;
        var stateMemberToChange = _.find(this.state.valuesArray, function(obj, num) {
            idx = num;
            return obj.id === componentChanged.props.values.id;
        });

        // Threw an error when I tried to update and indiviudal member of the state array/object.  So, take a copy
        // of the state, update the copy and do a setState() on the whole thing.  Using setState() rather than
        // replaceState() should be more efficient here.
        var newStateValuesArray = this.state.valuesArray;
        newStateValuesArray[idx].checked = newState;
        this.setState({valuesArray: newStateValuesArray});  // Automatically triggers render() !!
    },
    getCheckedValues: function() {
        // Get an array of state objects that are checked
        var checkedObjArray = [];
        checkedObjArray = _.filter(this.state.valuesArray, function(obj){
            return obj.checked;
        });

        // Get an array of value properties for the checked objects
        var checkedArray = _.map(checkedObjArray, function(obj){
            return obj.value;
        });
        console.log("CheckboxFieldGroup.getCheckedValues() = " + checkedArray);
    },
    componentDidMount: function() {
        this.getCheckedValues();
    },
    componentDidUpdate: function() {
        this.getCheckedValues();
    }
});


var defaults = {
    name : "mikeyCheck",
    valuesArray : [{
        label : "My Checkbox Field",
        value: "MyCheckboxField",
        checked : false
    }, {
        label : "My Other Checkbox Field",
        value : "MyOtherCheckboxField",
        checked : false
    }, {
        label : "Yet Another Checkbox Field",
        value : "YetAnotherCheckboxField",
        checked : true
    },{
        label : "Yes, it's a fourth checkbox field",
        value : "YesItsAFourthCheckboxField",
        checked : false
    }]
}; 

React.renderComponent(<CheckboxFieldGroup defaultValues={defaults} />, document.getElementById("main"));

This all works fine, and here's a JSFiddle of it in operation.

Yet I feel that I've done a number of things wrong here.

  1. It seems an awful lot of code to achieve something so simple. Is my whole approach misguided?
  2. The state of my CheckboxFieldGroup seems to contain a lot of stuff that maybe shouldn't be there, e.g. it contains name, value, label, id and checked, when in fact it's only last named that's ever going to be changed (by the user). So should that be the only one that's in state and the others in props somehow? Yet I need the id property to be in state so that the CheckboxFieldGroup.handleChange() method can determine which checkbox has actually changed. Or is there a better/easier way of doing this?
  3. When I update the CheckboxFieldGroup component's state, in the handleChange() method again, I couldn't find a way to directly update the one part of the state that I needed - i.e. the checked property of the state array element that corresponds to the checkbox that was just ticked/unticked. What I ended up doing is taking an entire copy of the state array to another variable, updating my one property there, and then replacing the entire state with the new array. Is this not a wasteful way of doing that, even though I'm using setState() rather than replaceState()?

Many thanks, in advance, for your help. And yes, I have Google, and pored though documentation. I've also bought and read the Developing a React Edge book, which appears to be number one in a field of one at the moment!

like image 978
ChillyPenguin Avatar asked Jan 09 '15 04:01

ChillyPenguin


1 Answers

for question number 1, i have the same feeling when i first using the react to build my first component, guess that is the way? haha

for question number 2 and 3, i will only save the checked in the state, and the rest of information remains in the props. Then when handle the update, i only set the certain checkbox to true/false.

http://jsfiddle.net/p0s58exh/4/

getInitialState: function () {   
  var that = this;
  var states = {};
  _.map(this.props.defaultValues.checkboxes, function (choice, key) {
    states[key] = choice.checked;
  });
  return states;
},

remember to add the key to the child array elements too so the react know precisely which element to be update.

return _.map(this.props.defaultValues.checkboxes, function (choice, key) {
  return CheckboxField({
    key: key,
    values: {
      name: that.props.defaultValues.name,
      value: key,
      label: choice.label,
      id: choice.id,
      checked: that.state[key]
    },
    callBackOnChange: that.handleChange
  });
});
like image 63
ChinKang Avatar answered Oct 25 '22 08:10

ChinKang