Can't get dynamic form to work in React Meteor application

Hi everyone,

So I have been trying to make the form in my application dynamic but am failing to retrieve the data from the input. Eventhough the code is messy (it’s my first meteor react project), I would like to learn how to get it working.

Any feedback is highly appreciated.

Before Adding the dynamic part it was all working as requested. The only problem I had is that the form was not returning all the values of the dynamic input. I now try to map over it but am getting a :
“Uncaught TypeError: Cannot set property ‘0’ of undefined” error which refers to this line:
"{this.state.inputs.map((input, idx) => <input".

How can I fix this?

This is my code:

import React, { Component } from 'react';
import { Row, Col, Checkbox, Radio, ControlLabel, HelpBlock, FormGroup, FormControl, Button, Tabs, Tab } from 'react-bootstrap';
import { Bert } from 'meteor/themeteorchef:bert';
import { insertComment } from '../../../api/comments/methods';
import ReactQuill from 'react-quill'; 

var s3Url = null;

export default class AddSparkShanghai extends Component {
  constructor(props) {
    super(props);

    this.createSpark = this.createSpark.bind(this);
    this.onChange = this.onChange.bind(this);

    this.state ={
      inputs: ['input-0'],
      city: '',
      person: '',
      location: '',
      title: '',
      content: [],
      mediaUrls: [], 
    };
  }

componentWillMount(){
    // we create this rule both on client and server
    Slingshot.fileRestrictions("myFileUploads", {
      allowedFileTypes: ["image/png", "image/jpeg", "image/gif"],
      maxSize: 10 * 1024 * 1024 // 10 MB (use null for unlimited)
    });
  }

  upload(file){
    var uploader = new Slingshot.Upload("myFileUploads");

    uploader.send(document.getElementById('input').files[0], function (error, downloadUrl) {
      if (error) {
        // Log service detailed response
        alert (error);
      }
      else {
        s3Url = encodeURI(downloadUrl);
        Bert.alert('File uploaded!', 'success');
        Meteor.users.update(Meteor.userId(), {$push: {"profile.files": downloadUrl}});
      }
    });
  }

  createSpark(event) {
  event.preventDefault();
  var mediaArray = [];
  if (this.mediaUrls.value == 0) {
    mediaArray = [];
  } else {
    mediaArray.push(encodeURI(this.mediaUrls.value));
    console.log(this.mediaUrl.value);
    console.log(mediaArray);
  }

  const city = 'Shanghai';
  const person = this.person.value;
  const location = this.location.value;
  const title = this.title.value;
  const content = this.state.content;
  const fileLink = s3Url;
  const timestamp = parseInt(this.props.timestamp);
  const mediaUrls = mediaArray;
  const approved = true;
  const adminSpark = true;
  const createdBy = Meteor.userId();


  insertComment.call({
    city, person, location, title, content, fileLink, timestamp, approved, adminSpark, createdBy, mediaUrl,
    }, (error) => {
      if (error) {
            Bert.alert(error.reason, 'danger');
        } else {
            Bert.alert('Spark added!', 'success');
        }
    });
  }

  onChange(html) {
    this.setState ({ content: html });
  }

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

  render() {
     const events = {
      'text-change': delta => {
      }
    }    

    return (
      <div className="background-container">
        <form ref={(input) => this.sparkForm = input} onSubmit={(e) => this.createSpark(e)}>
       
            <ControlLabel>Select your person (optional)</ControlLabel>
            <select id="formControlsPerson" placeholder="Choose your person" className="form-control" ref={(input) => this.person = input}>
              <option value='select'>Select your person</option>
              <option value='jane'>Jane Siesta</option>
              <option value='ben'>Ben Huang</option>
              <option value='han'>Han Han</option>
              <option value='mau'>Mau Mau</option>
              <option value='void'>VOID</option>
              <option value='tommy'>Tommy Hendriks</option>
              <option value='gareth'>Gareth Williams</option>
              <option value='gigi'>Gigi Lee</option>
            </select>
    
            <ControlLabel>Select your location (optional)</ControlLabel>
            <select id="formControlsLocation" placeholder="Choose your location" className="form-control" ref={(input) => this.location = input}>
              <option value='select'>Select your location</option>
              <option value='shelter'>Shelter</option>
              <option value='mansion'>The Mansion</option>
            </select>

            <ControlLabel>Title</ControlLabel>
            <input type="text" label="Title" placeholder="Enter your title" className="form-control" ref={(input) => this.title = input}/>
          
            <ControlLabel>Add Image</ControlLabel>
            <div className="upload-area">
              <p className="alert alert-success text-center">
                <span>Click or Drag an Image Here to Upload</span>
                <input type="file" id="input" className="file_bag" onChange={this.upload} />
              </p>
            </div>

            <ControlLabel>Content</ControlLabel>
              <div className='_quill'>
                <ReactQuill
                  toolbar={false} 
                  theme="snow"
                  ref='editor'
                  onChange={this.onChange}
                  events={events} />
               </div>
              <br />

          <ControlLabel>Media (optional)</ControlLabel>
          <div id="dynamicInput">
            {this.state.inputs.map((input, idx) => <input 
                key={ input } 
                type="text" 
                label="Media" 
                placeholder="Add your media url" 
                className="form-control" 
                ref={(input) => this.mediaUrls[idx] = input}/> )}       
          </div>
          <Button onClick={ () => this.appendInput() }>
            Add media field
          </Button>



       <ControlLabel>Media (optional)</ControlLabel>
          <div id="dynamicInput">
            {this.state.inputs.map(input => <input key={input} type="text" label="Media" placeholder="Add your media url" className="form-control" ref={(input) => this.mediaUrl = input}/> )}
          </div>
          <Button onClick={ () => this.appendInput() }>
            Add media field
          </Button>


          <Button type="submit" data-dismiss="modal">Submit</Button>
        </form>
      </div>
  )}
}

No time at the moment to help out properly, but a couple pro tips:

  1. Don’t use componentWillMount, there’s not much reason to. If you need to initialize something, use componentDidMount if it depends on the DOM being rendered; if it doesn’t depend on the DOM, use constructor.
  2. Use PureComponent instead of Component where it makes sense (in nearly all cases, probably).
1 Like

Thanks, I will look into this.

Still no luck trying to get my dynamic form to work sadly :frowning:

Is there really nobody that can push me in the right direction…?

this.mediaUrls is your problem. Did you mean this.state.mediaUrls?

Ow my… Yes indeed! That was the problem! Thanks a lot!

If you could still help me figure out the last two points I’d be eternally gratefull!

I am able to read the different values now in the array by calling them like this.state.mediaUrls[0].value = …

But now I need to map over them I still don’t understand the map function fully, so I’m having trouble writting it out.

I understand I need to map over the values of mediaUrls but how do I do that?

Also I don’t know how I can remove the added field.

I use this function to add a new field but how do I remove it again?

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

Rather than just fix the code, I’ll explain some of these concepts which will be much more useful to you in the long run.

So, a few important JavaScript array methods: map, filter, forEach. I’ll go through each and explain the differences and how they work.

map basically takes an array, and transforms/remaps it, returning a new array of equal size (i.e. no items are added or removed). For instance, let’s say you had this object:

const people = [
  {
    name: 'Bob',
    job: 'Data Analyst',
    age: 38,
  },
  {
    name: 'Ahmed',
    job: 'Underwriter',
    age: 21,
  },
  {
    name: 'Ofelia',
    job: 'Doctor',
    age: 42,
  },
];

If you wanted to make a new array where their ages were doubled, you could use map:

const peopleAged = people.map(person => ({
  ...person,
  age: person.age * 2,
}));

The result is:

[ { name: 'Bob', job: 'Data Analyst', age: 76 },
  { name: 'Ahmed', job: 'Underwriter', age: 42 },
  { name: 'Ofelia', job: 'Doctor', age: 84 } ]

The person => ({ }) syntax just returns an object, without needing a return statement. The above code could’ve been written the long way:

const peopleAged = people.map(person => {
  return {
    ...person,
    age: person.age * 2,
  }
});

If you’re not familiar with the ...person syntax, it’s an object spread - you should definitely read up on that. It essentially “flattens” the person object out. The long way would’ve looked like this:

const peopleAged = people.map(person => {
  return {
    name: person.name,
    job: person.job,
    age: person.age * 2,
  }
});

The big advantage of using the object spread is that if you add new properties to people, you don’t have to go back to the code above and add address: person.address. The spread will take care of that for you.

Now onto filter.

filter will allow you to filter out or remove items from an array, but non-destructively, by returning a new array. Let’s say you wanted to filter out the people who were older than 30:

const youngPeople = people.filter(person => person.age <= 30);

Whenever the function passed to filter returns a truthy value, that element is added to the resulting array.

Last up, forEach, which simply loops through an array but returns nothing.

people.forEach(person => {
  // do something
});

It’s the same as:

for (let person of people) {
  // do something
}

Some people argue there’s a speed difference. I doubt it matters all that much. :slight_smile: I use for (x of y) in nearly all cases, but forEach is really handy if you’re chaining methods:

people.filter(person => person.age <= 30).forEach(person => {
  // do something
});

Now as far as your concat of arrays, this is a place where you could use array spread (...) as a shorthand. So this statement of yours above:

this.setState({ inputs: this.state.inputs.concat([newInput]) });

Could be rewritten as:

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

Hopefully that all makes sense!

3 Likes

Man this is more than I could have asked for. I like it a lot more than just receiving the code. Now I can build my own code. I will read up on all you have posted! Thanks again for taking the time to write this out.

1 Like

const mediaArray = this.state.mediaContents.map(url => ({ url: url.value }));

Should this not create a new array where the url equals the value of the previous array?

It would be helpful if I had an idea of what mediaContents looked like. But assuming it looks like this:

[
  { title: 'Someplace', value: 'http://whatever.net' },
  { title: 'Google', value: 'http://google.com' },
]

Then yes, mediaArray should look like this:

[
  { url: 'http://whatever.net' },
  { url: 'http://google.com' },
]

I already understand why it isn’t working. Instead of just creating an array of data. I actually create an array that hold arrays. And the data I am after is located in the second array.

Sounds like you might be overcomplicating your data structures, possibly.

I’ve come back to revise this statement. In many cases it doesn’t make sense to use PureComponent, because there will always be inequalities in props changes, hence executing a shallow compare with the same false result every time.

Every time you make a new React component, you’ll have to make a careful decision whether it should be a stateless function, a regular Component class, or a PureComponent.