Update form in React.js


#1

Turns out there are only .insert() form examples for React.js + Meteor. How do I get a multi-user reactive update form?

Specifically how do I get the data from getMeteorData() to render()?

    OrderForm = React.createClass({

        mixins: [ReactMeteorData],
        getMeteorData() {
            var subscription = Meteor.subscribe("orders");
            return {order: Orders.findOne(this.props.orderId)}
        },

        handleSubmit(event, template) {
            event.preventDefault();
            Orders.update(this.props.orderId, {$set: this.state})
        },

        handleChange(e) {
            this.setState({[e.target.name]: e.target.value});
        },

        render() {

            return (
                <form onSubmit={this.handleSubmit}>
                    <input name="name" onChange={this.handleChange}></input>
                    <input name="age" onChange={this.handleChange}></input>
                    <input name="location" onChange={this.handleChange}></input>
                </form>
            )
        }
    })

#2

I’m not exactly sure where you want to put the data but for example you could use this.data.order to access the record. Does this example answer your question? Also you may want to checkout my social feed React example app: React-ive Meteor

render() {
  var order = this.data.order;
  return (
    <form onSubmit={this.handleSubmit}>
      <label>Name: {order.name} Age: {order.age}</label>
      <input name="name" onChange={this.handleChange} />
      <input name="age" onChange={this.handleChange} />
      <input name="location" onChange={this.handleChange} />
    </form>
  )
}

#3

hey there @SkinnyGeek1010, i’ve actually read a lot of your posts and gone through react-ive many times. Your react-ive repo is a great resource.

However in your repo there are no update style fields. All <input type="text"> are insert type. Do you see what I mean? I know the like button is a step towards what I need, but really that doesn’t require the complexity that I have

Your last snippet is faulty. I don’t want a label with the values and then an update field for the values. I want a single <input type="text"> to first display the initlai value and then allow the user to change the value.

What makes this extra compicated is that I need the form fields to be reactive to itself. For example if checkbox “A” is selected input “B” will appear. So I understand I need to use this.state. But how to make this.state sync with mongo so that:

  1. Initial state is correct
  2. Changes go to database
  3. One users change on field “A” does not disturb second user changing field “B”.

But of course this is Meteor so syncing to database can’t be all that difficult. Just can’t figure this out.


#4

Okay I think I figured it out.

Solution

Basically I link every change (letter-by-letter) straight to the database and get the reactive data straight back. Because of Meteors local database there is no lag when typing. But honestly to save on network transactions I would prefer to update state onChange and update database onBlur, currently this is not possible thus it feels like a hack :slight_smile:

    OrderForm = React.createClass({

        mixins: [ReactMeteorData],
        getMeteorData() {
            var subscription = Meteor.subscribe("orders");
            this.order = Orders.findOne(this.props.orderId)
        },

        handleChange(e) {
            this.order.updateField(e.target.name, e.target.value)
        },

        render() {
            var order = this.order
            return (
                <form onSubmit={this.handleSubmit}>
                    <input name="name" onChange={this.handleChange} value={order.name}/>
                    <input name="age" onChange={this.handleChange} value={order.age}/>
                    <input name="location" onChange={this.handleChange} value={order.location}/>
                </form>
            )
        }
    })

The reason for why I need letter-by-letter DB saving is because React shows the value straight from database. So when I type a letter the letter needs to go to the Database and back for React to show it. This is why this.state should be used, but I just can’t figure out how. This is covered in the official Facebook documentation.


#5

I’d highly advise against saving “letter-by-letter”, I suggest you look at underscore.js debounce - http://underscorejs.org/#debounce


#6

Ah yea good point. I’ll add an edit post to the app this weekend!

Here’s an example of updating the forms on blur while still keeping track of the state on change. This also has a div that shows data being read from the meteor mixin (this.data). It also compensates for an order that is already filled out, or if not it will use an empty string for age, name, location.

Orders.jsx (assumes there is a div of #app available)

Orders = new Mongo.Collection('orders');

var orderId = Orders.insert({name: "Adam"});
var docForProps = Orders.findOne(orderId);

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

  getInitialState() {
    return {
      name: this.props.name || '',
      age: this.props.age || '',
      location: this.props.location || ''
    };
  },

  getMeteorData() {
    var subscription = Meteor.subscribe("orders");
    return {
      order: Orders.findOne({_id: orderId})
    };
  },

  handleChange(e) {
    this.setState({
      name: this.refs.name.getDOMNode().value,
      age: this.refs.age.getDOMNode().value,
      location: this.refs.location.getDOMNode().value,
    });
  },

  handleBlur() {
    this.updateOrderWithNewData({
      name: this.refs.name.getDOMNode().value,
      age: this.refs.age.getDOMNode().value,
      location: this.refs.location.getDOMNode().value,
    });
  },

  updateOrderWithNewData(newData) {
    Orders.update({_id: orderId}, {$set: newData });
    console.log(Orders.findOne(orderId));
  },

  render() {
    return (
      <div>
        <div className="db record">
          Name: {this.data.order.name} <br/>
          Age: {this.data.order.age} <br/>
          Location: {this.data.order.location} <br/><br/>
        </div>

        <form onSubmit={this.handleSubmit}>
          <input
            name="name"
            ref="name"
            onChange={this.handleChange}
            onBlur={this.handleBlur}
            value={this.state.name}/> <br/>
          <input
            name="age"
            ref="age"
            onChange={this.handleChange}
            onBlur={this.handleBlur}
            value={this.state.age}/> <br/>
          <input
            name="location"
            ref="location"
            onChange={this.handleChange}
            onBlur={this.handleBlur}
            value={this.state.location}/>
        </form>
      </div>
    )
  }
})

if (Meteor.isClient) {
  Meteor.startup(function() {
    React.render(React.createElement(OrderForm, docForProps), $('#app')[0]);
  })
} else {
  Orders.remove({}); // clear out old docs on startup
}

#7

great take, @SkinnyGeek1010, but there are multiple multi-user problems with your approach.

  1. <input> value is not exactly reflected from database, thus when user 1 changes a field, user 2 does not see the changes.
  2. One user changing any one field will overwrite whole document with old info.

I recorded a video for clarity sake: http://youtu.be/J6Hqj9Q4rB4 (never mind the refresh at the end)

Bonus: You are now referencing the data in so many different ways that it just doesn’t feel like the Meteor way anymore. I mean react’s inherit separation of concern is great, but now Blaze still seems like the clear winner with code clarity and brevity.


#8

unfortunately this issue is much more complex than this method can fix. But thanks for the input.


#9

Ahh thanks for the video, that helps. I didn’t realize this is a requirement. I’ll think about how the best way is to do this in react. Basically you would need to update state every time the subscription updates with new data. You can move updateOrderWithNewData on the onChange and drop the onBlur so it updates on every keystroke.


[quote="KristerV, post:7, topic:6941"] One user changing any one field will overwrite whole document with old info. [/quote] You can have separate handlers for each input and then call set on just that input instead.

You don’t have to use refs and getDOMNode but if you access the virtual DOM it will be faster. this.data is just like the this context in Blaze. It just points to the reactive data (Order Collection in this case) from getMeteorData. Getting data from the server to the client is still the Meteor way but the front end will work best if it’s the React way (assuming you’re using React).

this.state is something that Blaze lacks by default and is one of the main features of React and it’s components. Basically a ‘component’ will have it’s own state that isn’t shared by the world. It uses this to keep track of what it should look like at any given time. In this example it’s very confusing because state is what is keeping the values up to date but we also want to pull in the data from the database and sync it.


[quote="KristerV, post:7, topic:6941"] I mean react's inherit separation of concern is great, but now Blaze still seems like the clear winner with code clarity and brevity. [/quote]

You’re right, Blaze is definitely more simple and brief. It also has the tightest integration with Meteor. However it’s achilles heel is managing state (or it’s representation of the dom) with large apps. It’s also hard to share templates across apps and other parent templates. IMHO React has way more clarity once you understand the concept of components. This is not really seen until you’re writing > 1000 lines of code and many components.


#11

Okay I guess i’ll settle with “update every keystroke to DB”. I still want to use React and this seems to be the best approach at the moment.

This sounds like something worthwhile. I could not figure out how to implement it though.

Thank you for the help, @SkinnyGeek1010. I look forward to your future code examples and posts.


#12

Instead of a letter by letter update or even on change you should do a timeout function that checks if the user has finished typing and if they have then send the update that way if the user says focued on the field it will send the new data but only if they quit typing

example

var delay = (function(){
  var timer = 0;
  return function(callback, ms){
    clearTimeout (timer);
    timer = setTimeout(callback, ms);
  };
})();

Template.liveField.events({
  'keyup .live-field' : function (e, t) {
    var instance = this;
    //Create a $ objects
    var elm = $(e.currentTarget);
    if (this.permission && !Roles.userHasPermission(Meteor.userId(), this.permission) ) {
      toastr.error('You do not have the permissions to update '+ instance.collection, 'Unable to Update')
    } else {
      //set a new time out for the end of typing
      delay(function () {
        var setConfig = {};
        //Set out config based on the field config
        setConfig[ instance.key ] = elm.val();

        //Since Meteor.users is not accessable via window[] we need a special case for it
        if ( instance.collection === 'Users' ) {
          //Update the collection
          Meteor.users.update({ _id: instance.documentId }, { $set: setConfig });
        } else {
          //Find the collection via the window object
          window[ instance.collection ].update({ _id: instance.documentId }, { $set: setConfig });
        }

        //Notify the user of the success
        toastr.success('Has Been Updated!', instance.key);
      }, instance.delay || 1000);
    }
  }
});

#13

If you want to update the database after a delay then I can strongly recommend the Debounce Input module.

All users will see the changes when they’re saved so this isn’t exactly what @KristerV was asking about.

But if you want all users to see the changes… but you want to be more efficient than saving every letter in the DB as you go… this library is great.

In my case I found saving every letter caused critical UX issues as well as performance issues. It felt like the cursor was jumping around and letters got missed out.


#14

This is an old thread but I thought I’d answer anyway because I looked around and never found one until figuring out a way.

Have two input boxes per field, one visible whenever the other isn’t. Both are Controlled fields, but Instance A contains the data, making it uneditable, while Instance B contains a mutable state version, which is editable. Instance A has an onFocus function that hides it and shows the identical Instance B. Then Instance B has an onChange that changes the state letter-by-letter and an onBlur that updates the source of truth, hides Instance B and displays Instance A.

Other users will get their Instance A updated but not their Instance B. Since Instance B is invisible until they focus on instance A, that doesn’t matter until they make it visible by focusing on Instance A. To solve that issue, Instance A’s onFocus function also sets the value of Instance B to that of Instance A, thereby updating it too.


#15

The solution is relatively simple, just use defaultValue instead of value…

<input className="form-control input-lg" defaultValue={userName} ref="userName" placeholder="Nombre de usuario" />