简体   繁体   中英

React: dynamically add input fields to form

I am using formsy-react for the form, I want to render more options when an event is fired, code looks something like this:

class MultipleChoice extends Component {
constructor(props) {
    super(props);

}

render() {

    return(
        <div>
           <Form>
               <div id="dynamicInput">
                   <FormInput />
               </div>
           </Form>
        </div>
    );

}
}

I have a button and onClick event I want to fire a function that appends another into div id "dynamicInput", is it possible?

Yes, we can update our component's underlying data (ie state or props ). One of the reasons React is so great is that it allows us to focus on our data instead of the DOM.

Let's pretend we have a list of inputs (stored as an array of strings in state ) to display, and when a button is clicked we add a new input item to this list:

class MultipleChoice extends Component {
    constructor(props) {
        super(props);
        this.state = { inputs: ['input-0'] };
    }

    render() {
        return(
            <div>
               <Form>
                   <div id="dynamicInput">
                       {this.state.inputs.map(input => <FormInput key={input} />)}
                   </div>
               </Form>
               <button onClick={ () => this.appendInput() }>
                   CLICK ME TO ADD AN INPUT
               </button>
            </div>
        );
    }

    appendInput() {
        var newInput = `input-${this.state.inputs.length}`;
        this.setState(prevState => ({ inputs: prevState.inputs.concat([newInput]) }));
    }
}

Obviously this example isn't very useful, but hopefully it will show you how to accomplish what you need.

I didn't use formsy-react but I solved the same problem, posting here in case it helps someone trying to do the same without formsy.

class ListOfQuestions extends Component {
  state = {
    questions: ['hello']
  }

  handleText = i => e => {
    let questions = [...this.state.questions]
    questions[i] = e.target.value
    this.setState({
      questions
    })
  }

  handleDelete = i => e => {
    e.preventDefault()
    let questions = [
      ...this.state.questions.slice(0, i),
      ...this.state.questions.slice(i + 1)
    ]
    this.setState({
      questions
    })
  }

  addQuestion = e => {
    e.preventDefault()
    let questions = this.state.questions.concat([''])
    this.setState({
      questions
    })
  }

  render() {
    return (
      <Fragment>
        {this.state.questions.map((question, index) => (
          <span key={index}>
            <input
              type="text"
              onChange={this.handleText(index)}
              value={question}
            />
            <button onClick={this.handleDelete(index)}>X</button>
          </span>
        ))}
        <button onClick={this.addQuestion}>Add New Question</button>
      </Fragment>
    )
  }
}

Below is the complete solution for this

    var OnlineEstimate = React.createClass({
    getInitialState: function() {
        return {inputs:[0,1]};
    },
    handleSubmit: function(e) {
        e.preventDefault();
        console.log( this.refs );
        return false;

    },
    appendInput: function(e) {
        e.preventDefault();
        var newInput = this.state.inputs.length;

        this.setState({ inputs: this.state.inputs.concat(newInput)},function(){
            return;
        });

        $('.online-est').next('.room-form').remove()

    },
    render: function() {
        var style = {
            color: 'green'
        };
        return(
                <div className="room-main">
                    <div className="online-est">
                        <h2 className="room-head">Room Details
                            <button onClick={this.handleSubmit} className="rednew-btn"><i className="fa fa-plus-circle"></i> Save All</button>&nbsp;
                            <a href="javascript:void(0);" onClick={this.appendInput} className="rednew-btn"><i className="fa fa-plus-circle"></i> Add Room</a>
                        </h2>

                       {this.state.inputs.map(function(item){
                            return (
                                    <div className="room-form" key={item} id={item}>
                                        {item}
                                        <a href="" className="remove"><i className="fa fa-remove"></i></a>
                                        <ul>
                                            <li>
                                                <label>Name <span className="red">*</span></label>
                                                <input type="text" ref={'name'+item} defaultValue={item} />
                                            </li>

                                        </ul>
                                    </div>
                            )

                       })}
                    </div>
                </div>

        );
    }
   });

Here is modern dynamic solution works by reusing Input component with React Hooks depending on json file. Here is how it looks:

在此处输入图片说明

The benefits of using such paradigm: the input component (having its own hook state) may be reused in any other app part without changing any line of the code.

The drawback it's much more complicate. here is simplified json (to build Components basing on):

{
    "fields": [
        {
            "id": "titleDescription",
            "label": "Description",
            "template": [
                {
                    "input": {
                        "required": "true",
                        "type": "text",
                        "disabled": "false",
                        "name": "Item Description",
                        "value": "",
                        "defaultValue": "a default description",
                        "placeholder": "write your initail description",
                        "pattern": "[A-Za-z]{3}"
                    }
                }
            ]
        },
        {
            "id": "requestedDate",
            "label": "Requested Date",
            "template": [
                {
                    "input": {
                        "type": "date",
                        "name": "Item Description",
                        "value": "10-14-2007"
                    }
                }
            ]
        },
        {
            "id": "tieLine",
            "label": "Tie Line #",
            "template": [
                {
                    "select": {
                        "required": true,
                        "styles": ""
                    },
                    "options": [
                        "TL625B",
                        "TL626B-$selected",
                        "TL627B",
                        "TL628B"
                    ]
                }
            ]
        }
    ]
}

stateless Input component with Hooks, which may read different input types such as: text, number, date, password and some others.

import React, { forwardRef } from 'react';

import useInputState from '../Hooks/InputStateHolder';

const Input = ({ parsedConfig, className }, ref) => {
  const inputState = useInputState(parsedConfig);
  return (
    <input
      //the reference to return to parent
      ref={ref}
      //we pass through the input attributes and rewrite the boolean attrs
      {...inputState.config.attrs}
      required={inputState.parseAttributeValue(inputState.config, 'required')}
      disabled={inputState.parseAttributeValue(inputState.config, 'disabled')}
      className={`m-1 p-1 border bd-light rounded custom-height ${className}`}
      onChange={inputState.onChange}
    />
  )
};
//we connect this separated component to passing ref
export default forwardRef(Input)

Hook holder InputStateHolder.js file

import { useState } from 'react';

const useInputState = (initialValue) => {
  //it stores read the json, proccess it, 
  //applies modifies and stores input values
  const [config, setInputConfig] = useState({
    isLoaded: false,
    attrs: { ...initialValue }
  });

  //mutating and storing input values
  function changeValue(e) {
    const updatedConfig = { ...config };
    updatedConfig.attrs.value = e.target.value;
    setInputConfig({ ...config })
  }
  // to apply form configs to input element 
  //only one time at the first load
  function checkTheFirstLoad() {
    const updatedConfig = { ...config };
    if (config.attrs.value.length === 0) {
      updatedConfig.attrs.value = config.attrs.defaultValue;
      //defaultValue is not allowed to pass as attribute in React
      //so we apply its value depending on the conditions and remove it
      delete updatedConfig.attrs.defaultValue;
      updatedConfig.isLoaded = true;
      setInputConfig(updatedConfig);
    }
  }
  //parsing boolean input attributs such as required or disabled
  function parseAttributeValue(newState, attribute) {
    return typeof newState.attrs[attribute] === 'string' && newState.attrs[attribute] === 'true'
      ? true : false
  }

  !config.isLoaded && checkTheFirstLoad();

  //returning the hook storage 
  return {
    config,
    onChange: changeValue,
    parseAttributeValue
  }
}

export default useInputState;

And the parent FormFields component (containing form and submit tags):

import React, { createElement } from "react";

import Input from '../UI/Input';

const FormField = ({ setConfig }) => {
  //it receives the parsed json and check to not be empty
  if (!!Object.keys(setConfig).length) {
    const fieldsConfig = setConfig.fields;
    //the array to get created elements in
    const fieldsToGetBuilt = [];
    // the array to store input refs for created elements
    const inputRefs = [];
    // the function to store new ref
    const setRef = (ref) => inputRefs.push(ref);
    fieldsConfig.map(field => {
      switch (true) {
        //here is we create children depending on the form configs
        case (!!field.template[0].input): {
          let classes = 'someStyle';
          fieldsToGetBuilt.push(
            createElement(Input, {
              ref: setRef,
              parsedConfig: field.template[0].input,
              key: field.id,
              className: classes
            })
          );
          break
        }
        //default case needed to build warning div notifying the missed tag
        default: {
          let classes = 'someOther danger style';
          let child = `<${Object.keys(field.template[0])[0]}/> not built`;
          fieldsToGetBuilt.push(
            createElement('div', {
              key: field.id,
              className: classes
            }, child)
          );
        }
      }
    })

    const onSubmitHandler = (e) => {
      //every time we click on submit button 
      //we receive the inputs`es values in console
      e.preventDefault();
      inputRefs.map(e =>
        console.log(e.value)
      )
    }

    return (
      <div className='m-2 d-flex flex-column'>
        <form onSubmit={onSubmitHandler}>
          <h5 className='text-center'>{setConfig.title}</h5>
          <div className='d-flex flex-row justify-content-center align-items-center'>
            {fieldsToGetBuilt.map(e => e)}
          </div>
          <input type="submit" onClick={onSubmitHandler} className='btn-info' />
        </form>
      </div >
    )
  } 
  // if in json there are no any fields to get built
  else return <div>no Page has been built</div>
};

export default FormField;

The result is here

在此处输入图片说明 and what we see in the console after input fields are changed and submit button is clicked

在此处输入图片说明

PS in my another answer i implemented dymanic module upload basing on json

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM