0

I want a list of text inputs that are in sync with a list of values I have in the backend. So typing into them updates the values, and updates to the values reflect in the text inputs.

This is a typical 2-way binding situation with something like this:

class BoundInput extends Component {

    constructor(props) {
        super(props);
        this.state = {textVal: ''};
    }

    handleChange = evt => this.setState({textVal: evt.target.value});

    render() {
        return <input type="text"
                      value={this.state.textVal}
                      onChange={this.handleChange} />
    }

}

But I would like to store the value not as exactly what the user typed but, say, a conversion from cm. to in.

Unfortunately, the conversion and then conversion back is not quite perfect.

E.g. typing '2.' immediately gets converted back to '2', so you get stuck if you want to type a decimal and anything beyond.

I'd like to be able to have the stored value update the input display unless the input is active, in which case it should only be updating the stored value from the input.

So I have a Boolean that lets me conditionally pass in value to the text input, so the typer can type unhindered, and the display may update to an equivalent representation only once they leave focus (e.g. '1.' suddenly becomes '1' or '1.0'), which is fine.

I tried this but I get the error that an input component must be either controlled, or uncontrolled, for its whole lifetime. No switching it up like I did.

Any suggestions on overall strategy, or getting around this particular warning about controlled vs. uncontrolled?

Perhaps a way to kill and re-mount the input component before switching from passing value to not passing value?

Something I did which is ugly as all heck is just duplicate my input component (which I call NiceInput), and when it is not active I swap in the duplicate NiceInput2. This works, but there has to be a less ridiculous way than duplicating components :D

tscizzle
  • 11,191
  • 15
  • 54
  • 88

2 Answers2

1

You could store a 2nd variable in the state where you constantly "makeNice" the text value, and on deselect, assign the nice value to the visible text value.

You still use the "onDeselect", but you save making a 2nd component

class BoundInput extends Component {

    constructor(props) {
        super(props);
        this.state = {textVal: '', niceVal: ''};
    }

    handleChange = evt => {
        this.setState({
            textVal: evt.target.value,
            niceVal: makeNice(evt.target.value),
        });    
    }

    handleDeselect = () => {this.setState({textVal: this.state.niceVal});}

    render() {
        return <input type="text"
                      value={this.state.textVal}
                      onChange={this.handleChange} 
                      onDeselect={this.handleDeselect}
                />
    }

}
Teo
  • 558
  • 1
  • 4
  • 10
0

What I ended up doing is creating an UncontrolledInput component which removes the value from props before forwarding the rest of the props to the input.

const UncontrolledInput = ({ value, ...otherProps }) => {
  return (
    <input {...otherProps} />
  )
}

and I rendered an UncontrolledComponent instead of my normal input component when the user was typing into it.

So the stored value was being updated by their typing, but while they typed there was nothing writing that input element's value besides the user's keystrokes.

But my final version was this:

const UncontrolledInput = ({ value, focusOnMount, ...otherProps }) => {
  otherProps.defaultValue = value;
  if (focusOnMount) {
    otherProps.ref = input => input && input.focus();
  }
  return (
    <input {...otherProps} />
  )
}

The 2 important additions:

1) at least starting the input's value with what the parent tried to pass in

2) an option to focus the input once it mounts (since replacing an input with this uncontrolled equivalent is likely to happen when you want the input to be focused, but this component replacement makes it not focused initially)

I achieved these with:

1) defaultValue

2) ref see this question/answer about focusing on mount

tscizzle
  • 11,191
  • 15
  • 54
  • 88