In the project I’m working on I had to create a script that would allow users to update their information directly from their (show) views.
From a general perspective, I think it is useful to give users the option to edit-in-place contents by just clicking them, leaving forms for when there is no other alternative (such as creating new objects). To me, an in-place editor feels more natural and usable and helps notably the user experience. The obvious fact is that most real world applications, such as Flickr or Facebox have implemented this feature in most of their interfaces.
To do that, I wanted to think of a general solution, one that could be applied to my other projects. Even a framework agnostic solution. I decided I would write it completely in Javascript and communicate with the server in a conventional RESTful way. Doing a bit of research before starting out, I ran across the project of Jan Varwig called Rest In Place which did precisely the same thing. So I examined carefully his code, fixed some stuff and extended it to support all usage cases I wanted to cover, this is how BestInPlace is born.
Basically, BestInPlace makes possible to tag (via HTML classes and HTML5 data* attributes) any field that is going to be user-editable so that the script automatically converts it to an form input when the user clicks on it, with no further muss or fuss for the developer. Of course, this field can take the form of a one-line text input, a textarea for longer texts, a select dropdown that will populate with your custom collection of options or boolean sort of data that works the same way a checkbox would, and allows value customization as well. Additionally, the script will trim and sanitize all user input, display server errors in case the format is not proper, it will also allow you to provide an external handler to activate the input.
Before getting into details.
Simply copy and load the files from the folder the following JS files in your application (in the same order):
Add the following line to your onLoad block:
|
1 2 3 4 |
$(document).ready(function() { /* Activating Best In Place */ jQuery(".best_in_place").best_in_place() }); |
Only add the folloging line to your application’s Gemfile and run bundle install:
|
1 |
gem "best_in_place" |
You still need to load the onLoad block, jQuery.js and jquery.purr.js in your application, but you can use the generator to copy (and update when necessary) the best_in_place.js script:
|
1 |
rails g best_in_place:setup |
First of all, you should write your controllers in a RESTful way and make sure they respond properly to a json request. Here you can see an example of how a standard update action should look like in a Rails app:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def update @user = User.find(params[:id]) respond_to do |format| if @user.update_attributes(params[:user]) format.html { redirect_to(@user, :notice => 'User was successfully updated.') } format.json { head :ok } else format.html { render :action => "edit" } format.json { respond_with_bip(@user) } end end end |
At the same time, if you want to perform server-side validations so that errors are displayed when users introduce invalid data, then the models should look something like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class User < ActiveRecord::Base validates :name, :length => { :minimum => 2, :maximum => 24, :message => "has invalid length"}, :presence => {:message => "can't be blank"} validates :last_name, :length => { :minimum => 2, :maximum => 24, :message => "has invalid length"}, :presence => {:message => "can't be blank"} validates :address, :length => { :minimum => 5, :message => "too short length"}, :presence => {:message => "can't be blank"} validates :email, :presence => {:message => "can't be blank"}, :format => {:with => /^([^@s]+)@((?:[-a-z0-9]+.)+[a-z]{2,})$/i, :message => "has wrong email format"} validates :zip, :numericality => true, :length => { :minimum => 5 } end |
Now all defined messages will be shown to the user if he introduces invalid data and the field will remain unchanged. Notice that messages will be displayed in a jquery.purr pop up. Take time to style it the way you prefer.
Best in Place will be wrapped into an html object (div/span preferred) with the following attributes:
Beware: All attributes are assigned underscore strings.
If you are using the Rails 3 Gem, you only need to tag user in-place editable fields like this:
Let’s see now how to create user-editable fields by some examples. We’ll use a generic User show action, as I did in the Test Application.
In regular HTML:
|
1 2 |
<span class='best_in_place' id='best_in_place_user_name' data-url='/users/4' data-object='user' data-attribute='name' data-type='input'>Dominic</span> |
Using the Rails 3 gem:
|
1 2 3 |
<%= best_in_place @user, :name, :type => :input %> <%= best_in_place @user, :name, :type => :input, :nil => "Click me to add content!" %> <%= best_in_place @user, :name, :activator => "#activator" %> |
In regular HTML:
|
1 2 3 4 5 6 |
<span class='best_in_place' id='best_in_place_user_description' data-url='/users/4' data-object='user'data-attribute='description' data-type='textarea'> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. </span> |
Using the Rails 3 gem:
|
1 |
<%= best_in_place @user, :description, :type => :textarea %> |
Allowing html tags:
|
1 |
<%= best_in_place @user, :description, :type => :textarea, :sanitize => false %> |
In regular HTML:
|
1 2 3 |
<span class='best_in_place' id='best_in_place_user_country' data-url='/users/4' data-object='user' data-collection='[[1,"Spain"],[2,"Italy"],[3,"Germany"],[4,"France"]]' data-attribute='country' data-type='select'>Germany</span> |
Using the Rails 3 gem:
|
1 2 |
<%= best_in_place @user, :country, :type => :select, :collection => [[1, "Spain"], [2, "Italy"], [3, "Germany"], [4, "France"]] %> |
In regular HTML:
|
1 2 3 4 |
<span class='best_in_place' id='best_in_place_user_receive_email' data-url='/users/4' data-object='user' data-collection='["No thanks","Yes of course"]' data-attribute='receive_email' data-type='checkbox' >Yes of course</span> |
Using the Rails 3 gem:
|
1 |
<%= best_in_place @user, :receive_emails, :type => :checkbox, :collection => ["No, thanks", "Yes, of course!"] %> |
Best In Place will make easier your application interfaces by letting users modify in-line every content. At the same time, it will control user input and display errors. Let me know if there’s something you would do different or don’t hesitate to give me a pull request if you contribute anyhow.
Jürgen Brüder
832 days ago
Hi!
I really like your work here. I was using Jan’s solution but yours is a very great improvement.
A feature I would love to see is adding relations to the :collection. Let’s say a @user can have a language. Then it would be great to just do something like:
:select, :collection => Language.all %>
It could show the name of the language but save the id.
Any thoughts on that?
Nevertheless, a great gem!
Best
Jay
bernat
832 days ago
Thanks, but this feature is there already. The only thing is you should pass the collection doing [[id, value], [id, value], [id, value],…]
Paul Strong
829 days ago
Have you tried running this on Rails 2? Should I just use REST in Place or do you think it would be easy enough for me to port your library to Rails 2?
Thanks
bernat
829 days ago
It should work in Rails 2 as well. The gem is just a helper though, you could create it yourself if you needed. The important thing is the script, and it isn’t framework-specific, all it needs is for you to have RESTful controllers, the way I explain in this tutorial.
Brad
810 days ago
Thank you so much for posting this! This is exactly what I was looking for. Nice work.
Pol
807 days ago
Buscant informació de Rails em trobo amb aquesta notícia i al cap d’una estona, coi si aquest de la foto de la dreta em sembla que l’he vist a algun lloc… ah vale és de la FIB. Felicitats pel blog, he trobat coses molt útils!
Steve
790 days ago
Hi Bernat,
This sounds like an nice implementation of inline editing. However, still being fairly new to rails, I’m getting the following error when I try and run my server after installing your gem. I can uninstall it and everything goes back to working fine. Its essentially a photo gallery with lots of javascript and other packages. I’m running Rails 3.0.5 on Ruby 1.9.2, with jquery 1.5.1. I’ll keep digging to see if I can figure it out, but thought I’d ping you to see if you had any ideas. Thanks for your contribution and efforts on this.
Here’s the error:
/opt/dev/rails/3/pappy/config/initializers/setup_jquery.rb:1:in `’: You have a nil object when you didn’t expect it! (NoMethodError)
My setup_jquery.rb has:
Pappy::Application.config.action_view.javascript_expansions[:defaults] = ['jquery.min', 'rails']
which works fine normally. I suspect there is a require missing somewhere, but I’m not sure where. Any ideas?
Thanks,
Steve
Steve
789 days ago
A quick update that I got things working by removing my jquery initializer. I thought I needed it to replace prototype as the default javascript lib. I found and installed the ‘jquery-rails’ gem and that seems to have installed jquery the correct way. Thanks again for creating and sharing your code!
- Steve
Ynes
779 days ago
Can I have the id value from the “select” to replace a div?
durr
777 days ago
if one wanted to override one of the callbacks, what is the “right” way? Presumably, i should be able to get a handle to the BestInPlaceEditorObject, and set the loadErrorCallback function appropriately, but what scope does that editor object exist?
Luis
774 days ago
Man, this was a great tutorial. You rock!
Is there any way to implement this in a “real” checkbox, rather than using “Yes, No” collection?
Ryan
773 days ago
Being new at this stuff, I apologize, but can you clarify how to pass in relations to the :collection part. For example, I have a table of clients with name as an attribute. How would I pass in the list of client names that have been entered into the system so far? Great gem by the way!
Alberto
763 days ago
I am trying to add the jquery datepicker to the input field and combine that with your awesome best in place. Any suggestions on how to do this would be appreciated.
aqabawe
749 days ago
thanks man, awesome plugin.
for those who want to use this on some other page like the INDEX page just add
or whatever your model name to the partial that populates it.
sharath
745 days ago
hello Bernat,
thanks a lot for posting these kind of tutorials ,using these idea in the project has helped me lot and any newbie can also learn easily from u r tutorials
you rock dude!!!!!!!!!!
ejangi
737 days ago
This is awesome man. But, is there a way I can define an “oncomplete” callback? I need to do some post processing to the updated html…
bernat
736 days ago
I appologize for not keeping the project updated, i’ve been craizingly busy lately. I recommend you to look at the Github README instead of the instructions above, I’ll keep there the latest version of the specifications. And also, I’ll devote some time eventually to look at the pull requests and issues people opened, thanks everyone for posting!
Tommie Jones
708 days ago
I can get it working by embedding the html. But I cannot get the best_in_place method working. I keep getting a MoMethod error. Any suggestions? Also I want to trigger another javascript event after the value is entered and accepted. Any suggestions.
hey dear
700 days ago
dude you done a great job. its so simple and having magical effects…
imohan
698 days ago
I see the message when there is an error in the data due to validations but no message if the data is saved correctly
I see these codes where I need to add the message any example or can i extend and put something in application.js exampple will be great if possible.
loadSuccessCallback : function(data) {
this.element.html(data[this.objectName]);
// Binding back after being clicked
$(this.activator).bind(‘click’, {editor: this}, this.clickHandler);
},
thank you
-imohan
jeromeyers
689 days ago
The edit in place demo doesn’t really work in Chrome for the multiline “User Description” text area. Before the first time you edit-in-place the label moves up and down as you move your mouse over the text, and then, after you leave the field and let it revert to simple text, and you move your mouse over the field, the content itself jumps up and down maddeningly, seeming to squirm away from all attempts to click it, like fish through castaways’ fingers.
jeromeyers
689 days ago
To add to that, it is only on one of the examples that this is happening, it is an example with a … tag in it. This creates a break and it is when the mouse moves over this break that the squirming starts.
Bryan Wang
676 days ago
Amazingly simple and elegant! Thank you for sharing it with the public
Bryan Wang
675 days ago
Is there a plan to add advanced input types such as tinymce or markdown or textile?
Dmitry
622 days ago
Please, say how to scroll red pen over editable field?
Pol Miro
537 days ago
Felicitats pel railcast nano! quin crack estas fet
habib
518 days ago
really thanx but look at this ……. irtci.com
rpruiz
472 days ago
Felicidades Bernat. Muy útil. Tienes alguna idea de cómo poder implementar el uso de tabs para moverse entre campos a editar? He tratado usando :html_attrs => {:tabindex => 1} sin éxito. Ojalá puedas comentar algo al respecto.
Gracias.
jim
422 days ago
Hey I really like your gem. I just have one question: how can I get it to work with jQuery autocomplete? Thanks!
Rashila Noushad
82 days ago
Hi,
I like your gem. But when trying to test with rspec and capybara the bip_area test helper is simulating the filling procedure, but it is not updating the field. The field is not shown updated after entering the new value in the test. But the whole thing working very beautifully in the application.
Can you please help in testing using rspec.
Thanks.