1

I have a component that renders three custom radio buttons. The user can either submit the selected or clear (unselect) them, leaving with no selected radio buttons.

I tried some options with comparing the filterResult to the data.value, but without success. Here's a simplified code:

// imports
...

type Props = {
  filterConfig: PropTypes.object,
  filterValue: Proptypes.string,
  onFilterChange: PropTypes.func
}

class Filter extends React.Component {
  this.props = Props
  this.state = {
    filterValue: this.props.filterValue,
  }

  handleChange = (e) => {
    this.setState({ filterValue: e.target.value })
  }

  handleSubmit = () => {
    this.props.onFilterChange(this.state.filterValue)
    this.refs.filterContainer.close()
  }

  handleClear = () => {
    this.setState({ filterValue: '' })
  }

  renderOptions = () => {
    const { data, name } = this.props.filterConfig
    const options = data.map(
      (o, i) => (
        <div className='custom-radio' key={i}>
          <input
            id={`${name}-${i}`}
            name={name}
            onChange={this.handleChange}
            type='radio'
            value={o.value}
          />
          <label htmlFor={`${name}-${i}`}>
            <span />
            {o.label}
          </label>
        </div>
      )
    )

    return (
      <div>
        {options}
      </div>
    )
  }

  renderPickerNavigation = () => {
    return (
      <div>
        <a
          href='javascript:void(0)'
          onClick={this.handleClear}
        >
          Clear
        </a>
        <a
          href='javascript:void(0)'
          onClick={this.handleSubmit}
        >
          Done
        </a>
      </div>
    )
  }

  render = () => {
    return (
      <FilterWrapper
        ref='filterWrapper'
      >
        {this.renderOptions()}
        {this.renderPickerNavigation()}
      </FilterWrapper>
    )
  }
}

The data I'm passing in is:

const filters = [
  {
    data: [{
      label: 'Label 1',
      value: 1
    }, {
      label: 'Label 2',
      value: 2
    }, {
      label: 'Label 3',
      value: 3
    }],
    name: 'userFilter'
  }
]

EDIT: The click event on the native radio input works fine, so no need to change that to be on the custom radio (the span element) or the label.

2
  • What behavior are you expecting? Note that your event handlers are not defined correctly. (they need to be written like this.change = () => {...} or explicitly bound) Commented Aug 15, 2017 at 11:05
  • I will change that to be correct, we use our own private library that doesn't require that syntax. But what I need to know is how to make the selected radio button unselected via clear method called on button click. Commented Aug 15, 2017 at 11:07

2 Answers 2

3

You should begin with having a state variable that stores which radio is currently selected. The initial value for this should be null (or some other falsy value) if you want none to be pre-selected.

The reset button should trigger a function which resets this state variable back to the initial value.


Take a look at this simple demo, using custom css radio buttons as you requested:

class MyApp extends React.Component {
  constructor() {
    super();
    this.state = {
      selectedRadio: null,
      products: [{id: 1, name: "foo"}, {id: 2, name: "bar"}, {id: 3, name: "baz"}]
    }
  }
  
  select = (id) => {
    this.setState({selectedRadio: id});
  }
  
  reset = () => {
    this.setState({selectedRadio: null});
  }
  
  render() {
    return (
      <div>
        {this.state.products.map(
          (item) => {
            return (
              <div key={item.id}>
                <input type="radio" name="myRadio" checked={this.state.selectedRadio === item.id} />
                <label onClick={this.select.bind(this, item.id)}>{item.name}<span /></label>
              </div>
            );
          }
        )}
        <button onClick={this.reset}>Reset</button>
      </div>
    );
  }
}

ReactDOM.render(<MyApp />, document.getElementById("app"));
div {
  margin: 10px 0;
}

input[type="radio"] {
  display: none;
}

input[type="radio"]+label span {
  display: inline-block;
  width: 14px;
  height: 14px;
  margin-left: 4px;
  vertical-align: middle;
  cursor: pointer;
  border-radius: 34%;
}

input[type="radio"]+label span {
  background-color: #333;
}

input[type="radio"]:checked+label span {
  background-color: orange;
}
<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>
<div id="app"></div>

Note: Since you are hiding the input element with css, you cannot have any listeners on it (e.g onChange or onClick). Instead, you should have onClick on the span that replaces it (see code below).


For a solution of how to reset all "traditional", non-css-only radio buttons, see the snippet below:

class MyApp extends React.Component {
  constructor() {
    super();
    this.state = {
      selectedRadio: null,
      products: [{id: 1, name: "foo"}, {id: 2, name: "bar"}, {id: 3, name: "baz"}]
    }
  }
  
  select = (id) => {
    this.setState({selectedRadio: id});
  }
  
  reset = () => {
    this.setState({selectedRadio: null});
  }
  
  render() {
    return (
      <div>
        {this.state.products.map(
          (item) => {
            return (
              <div key={item.id}>
                <label>{item.name}</label>
                <input type="radio" name="myRadio" onChange={this.select.bind(this, item.id)} checked={this.state.selectedRadio === item.id} />
              </div>
            );
          }
        )}
        <button onClick={this.reset}>Reset</button>
      </div>
    );
  }
}

ReactDOM.render(<MyApp />, document.getElementById("app"));
<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>
<div id="app"></div>

Sign up to request clarification or add additional context in comments.

7 Comments

That's a good idea, the only problem is my object doesn't have an ID. So I tried using the value instead and add checked={this.state.filterResult === e.value}. For some reason though, the this.state.filterResult is always empty within the map function, but outside of it is populated with the new value and the result of that is the radio is not selected at all.
@abpetkov, can you add the full code for the component? I can see you have cut certain things out, but right now it looks like your state declaration isn't inside the constructor, which I can't tell if that's intentional or because you cut code out.
Also, you should avoid using the letter e as the variable name for the current item in a loop, since that's typically used for events. While it will work, it might be confusing to others. Also, there is no reason to have a value here, your state variable already knows which item is selected, so you could just look it up in your data. Also, you could use label as an id. It seems unique, and so that could work as an identifier.
updated the original post with more detailed code. Please LMK if you have a different suggestion.
@abpetkov, change onChange={this.handleChange} to onChange={this.handleChange.bind(this, i)}. In handleChange do instead: handleChange = (id) => { this.setState({ filterValue: id }) } and then for checked do: checked={this.state.filterValue === i} and that should work. To reset all, just set filterValue to null.
|
0

This example can help you https://codepen.io/evoyan/pen/vxGBOw

Code:

class Radio extends React.Component {
  constructor(props) {
    super(props);
    this.state = {selected: false};
  }

  toggle() {
    const {onChange} = this.context.radioGroup;
    const selected = !this.state.selected;
    this.setState({selected});
    onChange(selected, this);
  }

  setSelected(selected) {
    this.setState({selected});
  }

  render() {
    let classname = this.state.selected ? 'active' : ''
    return (
      <button type="button" className={classname} onClick={this.toggle.bind(this)}>
        {this.state.selected ? 'yes' : 'no'}
      </button>
    );
  }
}

Radio.contextTypes = {
  radioGroup: React.PropTypes.object
};

class RadioGroup extends React.Component {
  constructor(props) {
    super(props);
    this.options = [];
  }

  getChildContext() {
    const {name} = this.props;
    return {radioGroup: {
      name,
      onChange: this.onChange.bind(this)
    }};
  }

  onChange(selected, child) {
    this.options.forEach(option => {
      if (option !== child) {
        option.setSelected(!selected);
      }
    });
  }

  render() {
    let children = React.Children.map(this.props.children, child => {
      return React.cloneElement(child, {
        ref: (component => {this.options.push(component);}) 
      });
    });
    return <div className="radio-group">{children}</div>;
  }
}

RadioGroup.childContextTypes = {
  radioGroup: React.PropTypes.object
};

class Application extends React.Component {
  render() {
    return (
      <RadioGroup name="test">
        <Radio value="1" />
        <Radio value="2" />
        <Radio value="3" />
      </RadioGroup>
    );
  }
}

/*
 * Render the above component into the div#app
 */
ReactDOM.render(<Application />, document.getElementById('app'));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.