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.
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!
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
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With