learningtech
learningtech

Reputation: 33695

Make react components less verbose

I'll show the entire code at the end of my question. But the thing I want to focus on is this render function in my App.jsx react component

render() {
    return (
      <div className="container">
          <ul>
            {this.renderListApplications()}
          </ul>
          <div>{JSON.stringify(this.data.currentApplication)}</div>
          <form className="new-task" onSubmit={this.handleSubmit} >
            <input ref="input_36" id="input_36" type="text" value={this.state.input_36} onChange={this.handleChange} />
            <input ref="input_37" id="input_37" type="text" value={this.state.input_37} onChange={this.handleChange} />
            <textarea ref="input_38" id="input_38" onChange={this.handleChange} value={this.state.input_38} />
            <textarea ref="input_39" id="input_39" onChange={this.handleChange} value={this.state.input_39} />
            <input ref="input_40" id="input_40" type="text" value={this.state.input_40} onChange={this.handleChange} />
            <button type="submit">Submit</button>
          </form>
      </div>
    );
}

I find ref="input_36" id="input_36" type="text" value={this.state.input_36} onChange={this.handleChange} to be too verbose. If I had about 300 other input, textarea, select fields interspersed between a whole bunch of other html elements, I'd be looking to reduce the amount of typing wherever possible. Is there a more convenient way to programmatically set some of these attributes and event handlers of the input, textarea and select fields? Or a react setting somewhere to default some of these things? Or just some way to make things less verbose?

If this were PHP, I'd put all my html code into a form.html, then I'd programmatically set all those attributes with something like:

/* I could simplify this code even more, but I'm making it verbose so you understand what I'm trying to achieve. */
$htmlForm = new DOMDocument();
$htmlForm->loadHTMLFile('form.html');
foreach ($htmlForm->getElementsByTagName('input') as $i)
{
    $i->setAttribute('value',$arrState[$i->getAttribute('name')]);
    $i->setAttribute('onChange','javascript:somefunction()');
    etc...
}
foreach ($htmlForm->getElementsByTagName('textarea') as $i)
{
    $i->nodeValue = $arrState[$i->getAttribute('name')];
    $i->setAttribute('onChange','javascript:somefunction()');
    etc...
}   

When I tried various approaches to programmatically set these attributes in React and JS, I kept running into errors about whatever DOM or element or whatever doesn't exist, or my app would crash,or whatever. If someone can show me an example of how to make this less verbose, that would be great.

Now here's my full App.jsx file.

App = React.createClass({
  mixins: [ReactMeteorData],

  getMeteorData() {
    return {
      applications: Applications.find({}, {sort: {createdAt: -1}}).fetch(),
      currentApplication: Applications.findOne({_id:this.props.router.params.appid}, {sort: {createdAt: -1}}),
    }
  },
  getInitialState: function() {
    return this.loadForm(this.props.router.params.appid);
  },
  loadForm(appId) {
    var currentApp = Applications.findOne({_id:appId});
    if(!currentApp) currentApp = {};
    return currentApp;
  },
  clickLoadForm(appId)
  {
    var currentApp = this.loadForm(appId);
    var state = new Object();
    var refs = this.refs;
    Object.keys(refs).map(function(prop,index){
      state[prop] = typeof currentApp[prop] == 'undefined' ? "" : currentApp[prop];
    });
    this.setState(state);
  },
  renderListApplications() {
    var _this = this;
    return this.data.applications.map(function(applicationform,i) {
      return <li key={"li"+i}><a onClick={_this.clickLoadForm.bind(_this,applicationform._id)} href={Meteor.absoluteUrl()+'application/' +applicationform._id} key={"a"+i}>Version {applicationform._id}</a></li>;
    });
  },
  handleSubmit(event) {
    event.preventDefault();
    var refs = this.refs;
    var formVals = new Object();
    Object.keys(refs).map(function(prop, index){
      if(refs[prop].nodeName.match(/(INPUT|SELECT|TEXTAREA)/).length > 0)
        formVals[prop] = refs[prop].value;
    });

    Meteor.call("saveApplication", formVals);

  },
  handleChange: function(e) {
    if(!e.target.id) return;
    if(typeof e.target.id == 'undefined') return;
    var state = new Object();
    state[e.target.id] = e.target.value;

    this.setState(state);
  },
  render() {
    return (
      <div className="container">
          <ul>
            {this.renderListApplications()}
          </ul>
          <div>{JSON.stringify(this.data.currentApplication)}</div>
          <form className="new-task" onSubmit={this.handleSubmit} >
            <input ref="input_36" id="input_36" type="text" value={this.state.input_36} onChange={this.handleChange} />
            <input ref="input_37" id="input_37" type="text" value={this.state.input_37} onChange={this.handleChange} />
            <textarea ref="input_38" id="input_38" onChange={this.handleChange} value={this.state.input_38} />
            <textarea ref="input_39" id="input_39" onChange={this.handleChange} value={this.state.input_39} />
            <input ref="input_40" id="input_40" type="text" value={this.state.input_40} onChange={this.handleChange} />
            <button type="submit">Submit</button>
          </form>
      </div>
    );
  }
});

Notes

Upvotes: 1

Views: 2031

Answers (2)

fabien
fabien

Reputation: 2258

The kadrian answer don't solve the problem if there is 300 fields. You still have 300 lines you cannot build somewhere else. I would suggest a map :

render() {

const inputs = [
    [ 12, 'text', this.handle1 ],
    [ 13, 'password', this.handle1 ],
    [ 14, 'textarea', this.handle1 ]
];

return <div>
    <form> {
        inputs.map( ( input ) => <ReactComponents.input
            key={ index }
            ref={ input[ 0 ] }
            data={ input }
        />; )
    } </form>
</div>;

And a component to manage all the use case :

ReactComponents.input = ...
...
    render() {
        ...
        if( this.props.data[ 1 ] === 'textarea' )
            return <textarea
                ...
            />
        return <input
            type={ this.props.data[ 1 ] }
            ref={ this.props.data[ 0 ] }
            key={ this.props.data[ 0 ] }
            onChange={ this.props.data[ 2 ] }
            ...
        />
}

So you can build your inputs array somewhere else (in a x-config file ?) and just pass it to build everything in your view.

Upvotes: 4

kadrian
kadrian

Reputation: 5009

You could simply create functions in your component that build parts of your UI (like a factory). Here is an example:

getInputField(id){
    const inputId = `input_${id}`;

    return(
        <input ref={inputId}
               id={inputId}
               type="text"
               value={this.state[`input${id}`]}
               onChange={this.handleChange} />
    );
}

render(){
    const input36 = this.getInputField(36);
    const input37 = this.getInputField(37);

    return (
        <div className="container">
          <ul>
            {this.renderListApplications()}
          </ul>
          <div>{JSON.stringify(this.data.currentApplication)}</div>
          <form className="new-task" onSubmit={this.handleSubmit} >
            {input36}
            {input37}
            ...
          </form>
        </div>
    );
}

Upvotes: 2

Related Questions