Search code examples
jsonreactjsspring-bootfetch-apireact-bootstrap

JSON.stringify() function is replacing the file object with an empty one


ISSUE

Hello guys, can anyone please help me to solve this complicated issue? : I'm creating an application using Spring boot v2.0.5 and React.js v15.6.2, ReactDom v15.6.2, React Bootstrap v0.32.4, and the linking between my frontend and serverside parts is made of restful web services using Spring annotations on the back and fetch API on the front. My React components are made by following the Parent-Children design pattern concept, that means: some of my components can be children of some others and vice versa.

How it works?

I have a table with columns and rows, each row inside the table has a unique id, 2 drop-downs, 1 text input, 2 datepickers and 1 file upload input which is causing the main issue; The user can add more rows that has same components as the previous ones, by clicking on the "+ Document" button; Each row has a unique incremental Id of type number (integer); the drop-downs and the inputs events are handled by one method inside the parent component based on their tag names; I'm storing all data entered by the user inside a list ([]) of objects({}).

Example: if the user fill only the first row; the object stored inside the list state will be like this:

[{id:0,type:"forms",lang:"all",libelle:"some strings",dateBegin:"11-12-2018",dateEnd:"12-12-2018",document:{File(154845)}]

if the user adds one other row and then filled it like the first one, the list will be like this:

  [{id:0,type:"forms",lang:"all",libelle:"some strings",dateBegin:"11-12-2018",dateEnd:"12-12-2018",document:{File(154845)},{id:1,type:"howTo",lang:"en",libelle:"some strings",dateBegin:"11-12-2018",dateEnd:"01-01-2019",document:{File(742015)}]

Check this image to see how the table look like: Table Demo

The table as code in Presentational component Class (child of the main component)

class Presentational extends React.Component {

  constructor(props) {
   super(props);

   this.state = {
    docObjList: [],
    element: (
      <FormDocRowItem // this contains the table tbody tds elements..
        id={1}
        handleChanges={this.props.handleChanges}/>)
   };

     this.handleAddDocumentRow = this.handleAddDocumentRow.bind(this);
 }

  // handleAddDocumentRow method
  handleAddDocumentRow(e) {

   const value = e.target.value;
   const name = e.target.name;

   if (name === 'add') {
    let arr = this.state.docObjList; // get the list state

    // assign the new row component
    arr = [...arr, Object.assign({}, this.state.element)]; 

    // set the new list state
    this.setState({docObjList: arr});
   }

   // if name === 'delete' logic..
 }

   // render method
   render() {
          const {handleReset} = this.props;
      return(
       <FormGroup>
              <Form encType="multipart/form-data">
                <Table striped bordered condensed hover>
                  <thead>
                  <tr>
                    <th>id</th>
                    <th>Type</th>
                    <th>Lang</th>
                    <th>Title</th>
                    <th>Date begin</th>
                    <th>Date end</th>
                    <th>+ Document</th>
                    <th>Options</th>
                  </tr>
                  </thead>
                  <tbody>

                  {this.state.element} // this row is required as initialization
                  {
                    this.state.docObjList.map((doc, index) => {
                      // as index in map() starts from 0 and there is an   
                      // already row component above => The index inside the 
                      // table should start from 1 except The key property 
                      // which should know the right index of the function 
                      const id = index+1; 

                      return (
                        <tr key={index}> 
                          <td>
                            {id}
                          </td>
                          <td>
                            <DocumentTypes id={id} handleChange={this.props.handleChanges}/>
                          </td>
                          <td>
                            <DocumentLanguage id={id} handleChange={this.props.handleChanges}/>
                          </td>
                          <td>
                            <DocumentLibelle id={id} handleChange={this.props.handleChanges}/>
                          </td>
                          <td>
                            <FormControl  id={''+id} name="dateBegin" componentClass="input" type="date"
                                         onChange={this.props.handleChanges}/>
                          </td>
                          <td>
                            <FormControl  id={''+id} name="dateEnd" componentClass="input" type="date"
                                         onChange={this.props.handleChanges}/>
                          </td>
                          <td>
                            <Document  id={id} handleChange={this.props.handleChanges}/>
                          </td>
                          {
                            this.state.docObjList.length == index + 1 &&
                            <td>

                              <button type="button" style={{verticalAlign: 'middle', textAlign: 'center'}} id={index + 1}
                                      name="delete"
                                      onClick={this.handleAddDocumentRow}>
                                Delete
                              </button>
                            </td>
                          }
                        </tr>
                      );
                    })
                  }
                  </tbody>
                </Table>
                <button type="button" name="add" onClick={this.handleAddDocumentRow}>+ Document</button>

                <FormGroup>
                  <Button type="reset"
                          style={{marginRight: '20%'}}
                          className="btn-primary"
                          onClick={this.props.handleClickSubmit}>Submit</Button>
                  <Button name="back" onClick={this.props.handleClickSubmit}>Annuler</Button>
                </FormGroup>
              </Form>
       </FormGroup>
        )
  }
}

The row component class (Child component of Presentational)

const FormDocRowItem = (props) => {

  const {id} = props; // the ID here is refering the column that is going to be 
                      // show inside the table not the index of the map function
  return(
    return (
      <tr>
        <td>
          {id}
        </td>
        <td>
          <DocumentTypes id={id} handleChange={this.props.handleChanges}/>
        </td>
        <td>
          <DocumentLanguage id={id} handleChange={this.props.handleChanges}/>
        </td>
        <td>
          <DocumentLibelle id={id} handleChange={this.props.handleChanges}/>
        </td>
        <td>
          <FormControl id={''+id} name="dateBegin" componentClass="input" type="date" onChange={this.props.handleChanges}/>
        </td>
        <td>
          <FormControl id={''+id} name="dateEnd" componentClass="input" type="date" onChange={this.props.handleChanges}/>
        </td>
        <td>
          <Document id={id} handleChange={this.props.handleChanges}/>
        </td>
      </tr>
    );
  }

}

Parent Component Class (The main component)

   constructor(props) {
     this.state ={
       docDataList: [],
       formIsReadyToSubmit: false
     } 

     this.handleSubmit = this.handleSubmit.bind(this); // button submit click
     this.handleReset = this.handleReset.bind(this); // button reset click
     this.fillWithData = this.fillWithData.bind(this); // handle changes

   }

   // handleReset method..

   fillWithData(e) {

    const name = e.target.name; // get the name of the target
    const id = parseInt(e.target.id); // get the id of the target
    let value = e.target.value; // get the value of the target
    let arr = this.state.docDataList; // get the list state

    // if the target is a file upload
    if (name === 'selectDocument') 
      value = e.target.files[0];

    // create properties with null values starting from the first onchange 
    // event handling, to not get a misplaced properties inside the
   // objects of the list state
    arr.map((x) => {
      x.type = x.type ? x.type : null;
      x.lang = x.lang ? x.lang : null;
      x.libelle = x.libelle ? x.libelle : null;
      x.dateBegin = x.dateBegin ? x.dateBegin : null;
      x.dateEnd = x.dateEnd ? x.dateEnd : null;
      x.document = x.document ? x.document : null;
    });

    // if the event target name is not delete
    if (name != 'delete') {
      // check if the object id already exist in the table
      // if it exists, the new value should replace the previous one
      // and not allowed to add a new object to the list state
      if ((arr.find((x) => x.id == id))) {
        // loop through the list state to find the id of the object
        arr.map((x) => {
          if (x.id == id) {
            // helper variable to prevent empty strings
            const val = value != '' ? value : null;

            switch (name) {
              case 'selectType':
                x.type = val;
                break;
              case 'selectLang':
                x.lang = val;
                break;
              case 'libelle':
                x.libelle = val;
                break;
              case 'dateBegin':
                x.dateBegin = val;
                break;
              case 'dateEnd':
                x.dateEnd = val;
                break;
              case 'selectDocument':
                x.document = val;
                break;
            }
          }
        });
        // assign the new list to my docDataList state
        // mentioning that the id of the element already exist
        this.setState({docDataList: arr}, () => {
          console.log(' ID exist; new dataList :', this.state.docDataList);
        });
      }
      // if the id doesn't exist (means that the button +document is clicked)
      else {
        // again, a helper variable as the previous statement 
        const val = value != '' ? value : null;

        this.setState({
          docDataList: [...arr, Object.assign({
            id: id,
            type: name === 'selectType' ? val : null,
            lang: name === 'selectLang' ? val : null,
            libelle: name === 'libelle' ? val : null,
            dateBegin: name === 'dataBegin' ? val : null,
            dateEnd: name === 'dateEnd' ? val : null//,
            //document: name==='selectDocument'? val:null
          })]
        }, () => {
          console.log('ID doesnt exist; new dataList :', this.state.docDataList);
        });
      }
    }
  }

HandleSubmit() method (Inside the Parent component class)

// Submit button click handler
  handleSubmit(e) {

      let docDataList = this.state.docDataList;

      // if the user didn't touch any thing on the table rows
      // that means the list is empty and its length = 0
      if (docDataList.length === 0) {
        this.setState({
          alerts: {
            message: 'Please enter your document information ',
            show: true
          }
        });
      }
      // if the user has entered a data on the table row
      else if (docDataList.length > 0) {

        let data = new FormData(); // object which will be sent 

        // check the docDataList before request
        console.log('DocDataList before request:', docDataList); 
        data.append('docDataList', JSON.stringify(docDataList)); 

        fetch('http://localhost:8080/api/files/uploadFile', {
          method: 'POST',
          body: data
        }).then(response => {
          console.log('success document upload', response);
        }).catch(error => {
          console.log('error', error);
        });

        this.setState({
          formIsReadyToSubmit: true, 
          docDataList: [], // reset the list
          alerts: {updateAlert: true} // make an alert
        });
      }

    }

To see what the console show when I fill the row with data: CLICK HERE PLEASE

To see the response of the request: CLICK HERE PLEASE

NOTE: You may notice after watching those screenshots, that there is an extra list of data called "arrContrats" which which I didn't mention it in my issue because it doesn't have any problem; the problem is with the "docDataList" list. Thanks in advance


Solution

  • If your problem is that you're getting a File object from the browser, and then later using JSON.stringify on it (or something containing it) and getting {} for it in the JSON, that's correct. The browser's File object has no own, enumerable properties. JSON.stringify only includes own, enumerable properties.

    If you want the various properties that File objects have (inherited accessor properties), you'll need to copy them to a new object.

    If you want the file data, it's not accessible as a property on the object, you have to use one of the methods it inherits from Blob to read the file data, such as stream, text, or arrayBuffer (or alternatively, you could use a FileReader, but there's no need to except in obsolete environments that don't have the modern methods).