16

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?

jayasth
  • 163
  • 1
  • 2
  • 6

4 Answers4

46

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.

Calvin Belden
  • 3,114
  • 1
  • 19
  • 21
9

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>
    )
  }
}
Josh Pittman
  • 7,024
  • 7
  • 38
  • 66
  • 1
    thanks @josh. for future users don't use same object for setting up the default value. here Josh used 'hello' and blank string for setting the values. Don't try to define an object and assign . – Junaid Atique Jul 03 '19 at 08:42
1

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>

        );
    }
   });
Naveen Kumar
  • 987
  • 11
  • 11
  • 2
    Does anyone used it? – awzx Jul 06 '17 at 11:24
  • 1
    Good to know, I've not been able to implement my own solution of it. By the way, for a cleaner React solution, I'd remove any JQuery call : $('.online-est').next('.room-form').remove() – awzx Jul 07 '17 at 16:05
-1

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

enter image description here

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

enter image description here and what we see in the console after input fields are changed and submit button is clicked

enter image description here

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

Alexey Nikonov
  • 4,958
  • 5
  • 39
  • 66