×

Real-Time In Place Editing For Rails

Juggernaut

Published: April 07, 2012

In an effort to make Hospitium's interface easier to use, I removed the extra step of editing data in a standard form. Instead opting to use an in place editing solution.

Best in Place is the gem I used and it's pretty simple to set up. Instead of covering the details here, I'll let this railscast explain it. That episode is a little out of date so make sure to check out the gem's README, the main change being Controller response with respond_with_bip.

Best in Place is a jQuery based AJAX Inplace-Editor that takes profit of RESTful server-side controllers to allow users to edit stuff with no need of forms.

Getting in place editing working is easy, but one downside is multiple users can edit the same record without seeing each others changes. This could lead to confusion after a page reload and a users changes are completely wrong (at least different from what they expected). So the obvious solution is to push the changes to all users making the edits real-time.

We could roll our own complete push state, but instead I'm going to offload the work to Juggernaut.

Juggernaut gives you a realtime connection between your servers and client browsers.

You can run Juggernaut locally for development, but I recommend just throwing it up on Heroku to make your life easier.

Clone repo:

git clone git://github.com/maccman/juggernaut.git
cd juggernaut

Create Heroku app:

heroku create myapp --stack cedar
heroku addons:add redistogo:nano
git push heroku master
heroku ps:scale web=1
heroku open

Find RedisToGo url

heroku run node
process.env.REDISTOGO_URL

And then publish from Juggernaut's Ruby gem:

irb
require "juggernaut"
Juggernaut.url = "REDISTOGO_URL"
Juggernaut.publish(&quot;channel1&quot;, &quot;woo! it&#39;s working&quot;)</code></pre></noscript>

Once you have Juggernaut up and running it's time to start publishing events from rails to juggernaut. The easiest way to do this is with model observers. For the rest of this post we'll assume we are working with a pretty typical User model.

First install the Juggernaut gem by including it in your gemfile.

Create a new file /app/models/user_observer.rb

require "juggernaut"
class UserObserver < ActiveRecord::Observer

def after_update(user) publish(:update, user) end

def publish(type, user) Juggernaut.url = ENV['JUGG_URL'] Juggernaut.publish("/observer/user/#{user.id}", { :id => user.id, :type => type, :klass => user.class.name, :record => user.changes }) end

end

All this does is listen for update actions to the user model. Which conveniently is the action best in place uses to update the model. The changed data is then published to the Juggernaut instance.

The ENV['JUGG_URL'] should be set to the REDISTOGO_URL from the Heroku setup steps above.

You will also need to initialize the observer by adding the following to /config/application.rb

config.active_record.observers = :user_observer

Now that the changes are being published, we need need to make the view listen for the changes.

In your application layout add

<script src="http://your-juggernaut-app.herokuapp.com/application.js" type="text/javascript" charset="utf-8"></script>

Then in your user view add

<script type="text/javascript">
    //setup juggernaut to handle real time updating page changes
    var jug = new Juggernaut({
        secure: false,
        host: 'your-juggernaut-app.herokuapp.com',
        port: 80,
        transports: ['xhr-polling','jsonp-polling']
    });

//subscribe to the url for the specific user, this is the same url we published to from the model observer
jug.subscribe(&quot;/observer/user/&lt;%= @user.id %&gt;&quot;, function(data){
    //set the updated_text with an error message first - will override if a better result
    var updated_text = &quot;There was an update, but a problem displaying. Please refresh.&quot;;
    jQuery.each(data.record, function(i, val) {
        //the updated_at of the record is always returned, we just skip it here since it&#39;s not important
        if (i != &quot;updated_at&quot;){
            //set the updated_text to the update vaule - this is for simple text areas and fields
            updated_text = val[1];
            //if we updated a collection via dropdown, we need to do some more work
            if ($(&quot;#best_in_place_user_&lt;%= @user.id %&gt;_&quot;+i).attr(&quot;data-collection&quot;) !== undefined) {
                //grab the data values from the best in place dropdown
                var brand = $(&#39;#best_in_place_user_&lt;%= @user.id %&gt;_&#39;+i).attr(&quot;data-collection&quot;);
                var test = $.parseJSON(brand);
                $.each(test, function(index, value) {
                    //loop on the json looking for a match
                    if(value[0] == val[1]){
                        //update the text from the dropdown value
                        updated_text = value[1];
                    }
                });
            }
            //highlight the changed field and update the text with the value from juggernaut - all users viewing the page will see this
            $(&#39;#best_in_place_user_&lt;%= @user.id %&gt;_&#39;+i).css(&quot;background-color&quot;,&quot;#c7f464&quot;).html(updated_text).delay(1500).animate({backgroundColor: &quot;#f5f5f5&quot;}, 1000 );
        }
    });
});

</script>

Now anytime a user changes a field with best in place, other users viewing the same page will see the updates real-time.

The real tricky part here is knowing the field ID to update with the changed value. Best In Place generates field IDs dynamically with no way to override them. Luckily they are fairly predictable and we can infer what field was updated by the field name and the model name/ID. That's what $('#best_in_place_user_<%= @user.id %>_'+i) is doing. i contains the model field name such as username or last_login.

There are also a few problems with this. One being any special formatting done to things like dates won't be formatted by the javascript. This can be worked around by formatting the value as needed though. The other problem is the way I passed the user info into the javascript is ugly. Here is a railscast with some ideas on how you could clean that up.

To see real-time editing in action, sign up for an account at Hospitium.