Non-dynamic dynamic form fields in Rails with jQuery

I am trying to overcome the obstacles of dynamic form fields in Rails - it looks like the structure does not handle very elegantly. I also use jQuery in my project. I have JRails installed, but I would rather write AJAX code unobtrusively where possible.

My forms are quite complex, two or three levels of nesting are not unusual. The problem I am having is causing the correct form identifiers, as they are so dependent on the context of the form builder. I need to be able to dynamically add new fields or delete existing records in a has_many relationship, and I'm completely at a loss.

Every example I've seen so far has been so ugly. Ryan Bates' tutorial requires RJS, which leads to some pretty ugly obsessive javascript in markup and seems to be written before nested attributes. I saw a fork of this example with unobtrusive jQuery, but I just don’t understand what it is doing, and could not get it to work in my project.

Can someone provide a simple example of how this is done? Is this even possible while respecting the RESTful controller agreement?




Andy posted a great example of deleting an existing record, can anyone provide an example of creating new fields with the correct attributes? I could not figure out how to do this with nested forms.

+41
jquery ajax ruby-on-rails forms
Nov 09 '09 at 21:54
source share
6 answers

Since no one offered an answer to this question, even after generosity, I finally managed to get it to work independently. It shouldn't have been stunning! Hopefully this will be easier to do in Rails 3.0.

Andy's example is a good way to delete records directly, without submitting the form to the server. In this particular case, what I'm really looking for is a way to dynamically add / remove fields before updating the sub-form. This is a slightly different case, because as the fields are deleted, they are not actually deleted until the form is submitted. I will probably end up using both options depending on the situation.

I based my implementation on the Riley dash of complex fork gitub form examples .

First configure the models and make sure they support nested attributes:

 class Person < ActiveRecord::Base has_many :phone_numbers, :dependent => :destroy accepts_nested_attributes_for :phone_numbers, :reject_if => lambda { |p| p.values.all?(&:blank?) }, :allow_destroy => true end class PhoneNumber < ActiveRecord::Base belongs_to :person end 

Create a partial view of the fields of the PhoneNumber form:

 <div class="fields"> <%= f.text_field :description %> <%= f.text_field :number %> </div> 

Then write the base view for the Person model:

 <% form_for @person, :builder => LabeledFormBuilder do |f| -%> <%= f.text_field :name %> <%= f.text_field :email %> <% f.fields_for :phone_numbers do |ph| -%> <%= render :partial => 'phone_number', :locals => { :f => ph } %> <% end -%> <%= f.submit "Save" %> <% end -%> 

This will work by creating a set of template fields for the PhoneNumber model, which we can duplicate using javascript. We will create helper methods in app/helpers/application_helper.rb for this:

 def new_child_fields_template(form_builder, association, options = {}) options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new options[:partial] ||= association.to_s.singularize options[:form_builder_local] ||= :f content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f| render(:partial => options[:partial], :locals => { options[:form_builder_local] => f }) end end end def add_child_link(name, association) link_to(name, "javascript:void(0)", :class => "add_child", :"data-association" => association) end def remove_child_link(name, f) f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child") end 

Now add these helper methods to partial editing:

 <% form_for @person, :builder => LabeledFormBuilder do |f| -%> <%= f.text_field :name %> <%= f.text_field :email %> <% f.fields_for :phone_numbers do |ph| -%> <%= render :partial => 'phone_number', :locals => { :f => ph } %> <% end -%> <p><%= add_child_link "New Phone Number", :phone_numbers %></p> <%= new_child_fields_template f, :phone_numbers %> <%= f.submit "Save" %> <% end -%> 

You now have a js templating template. It will send an empty template for each association, but the sentence :reject_if in the model will drop them, leaving only user-created fields. Update: I rethought this project, see below.

This is not true AJAX, since there is no connection on the server that goes beyond loading the page and submitting the form, but I honestly could not find a way to do this after the fact.

In fact, this can provide a better user interface than AJAX, since you do not need to wait for a server response for each additional field until you are done.

Finally, we need to link this with javascript. Add the following to your `public / javascripts / application.js' file:

 $(function() { $('form a.add_child').click(function() { var association = $(this).attr('data-association'); var template = $('#' + association + '_fields_template').html(); var regexp = new RegExp('new_' + association, 'g'); var new_id = new Date().getTime(); $(this).parent().before(template.replace(regexp, new_id)); return false; }); $('form a.remove_child').live('click', function() { var hidden_field = $(this).prev('input[type=hidden]')[0]; if(hidden_field) { hidden_field.value = '1'; } $(this).parents('.fields').hide(); return false; }); }); 

By this time you should have a dynamic form of barebones! Javascript here is very simple and can be easily done with other frameworks. You can easily replace my application.js code with the prototype + lowpro, for example. The basic idea is that you don't embed giant javascript functions in your markup, and you don't need to write the tedious phone_numbers=() functions in your models. Everything just works. Hurrah!




After some additional testing, I came to the conclusion that the templates must be removed from the <form> fields. Keeping them there means that they are sent back to the server with the rest of the form, and this only creates headaches later.

I added this to the bottom of my layout:

 <div id="jstemplates"> <%= yield :jstemplates %> </div 

And the new_child_fields_template helper new_child_fields_template :

 def new_child_fields_template(form_builder, association, options = {}) options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new options[:partial] ||= association.to_s.singularize options[:form_builder_local] ||= :f content_for :jstemplates do content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f| render(:partial => options[:partial], :locals => { options[:form_builder_local] => f }) end end end end 

Now you can remove the proposals :reject_if from your models and stop worrying about sending templates.

+53
Nov 17 '09 at 21:43
source share

Based on your response to my comment, I think that uninstall processing is unobtrusive - this is a good place to start. As an example, I will use Product with scaffolding, but the code will be general, so it will need to be easy to use in your application.

First add a new option to your route:

 map.resources :products, :member => { :delete => :get } 

And now add a view view to your product views:

 <% title "Delete Product" %> <% form_for @product, :html => { :method => :delete } do |f| %> <h2>Are you sure you want to delete this Product?</h2> <p> <%= submit_tag "Delete" %> or <%= link_to "cancel", products_path %> </p> <% end %> 

This view will only be visible to users with JavaScript disabled.

In the Products controller, you need to add the delete action.

 def delete Product.find(params[:id]) end 

Now go to your index and change the Destroy link to this:

 <td><%= link_to "Delete", delete_product_path(product), :class => 'delete' %></td> 

If you run the application at this moment and view the list of products, you can remove the product, but we can do better for users with JavaScript support. The class added to the delete link will be used in our JavaScript.

This will be a fairly large piece of JavaScript, but it’s important to focus on the code that concerns the creation of ajax code β€” the code in the ajaxSend handler and the clicker β€œa.delete”.

 (function() { var originalRemoveMethod = jQuery.fn.remove; jQuery.fn.remove = function() { if(this.hasClass("infomenu") || this.hasClass("pop")) { $(".selected").removeClass("selected"); } originalRemoveMethod.apply(this, arguments); } })(); function isPost(requestType) { return requestType.toLowerCase() == 'post'; } $(document).ajaxSend(function(event, xhr, settings) { if (isPost(settings.type)) { settings.data = (settings.data ? settings.data + "&" : "") + "authenticity_token=" + encodeURIComponent( AUTH_TOKEN ); } xhr.setRequestHeader("Accept", "text/javascript, application/javascript"); }); function closePop(fn) { var arglength = arguments.length; if($(".pop").length == 0) { return false; } $(".pop").slideFadeToggle(function() { if(arglength) { fn.call(); } $(this).remove(); }); return true; } $('a.delete').live('click', function(event) { if(event.button != 0) { return true; } var link = $(this); link.addClass("selected").parent().append("<div class='pop delpop'><p>Are you sure?</p><p><input type='button' value='Yes' /> or <a href='#' class='close'>Cancel</a></div>"); $(".delpop").slideFadeToggle(); $(".delpop input").click(function() { $(".pop").slideFadeToggle(function() { $.post(link.attr('href').substring(0, link.attr('href').indexOf('/delete')), { _method: "delete" }, function(response) { link.parents("tr").fadeOut(function() { $(this).remove(); }); }); $(this).remove(); }); }); return false; }); $(".close").live('click', function() { return !closePop(); }); $.fn.slideFadeToggle = function(easing, callback) { return this.animate({opacity: 'toggle', height: 'toggle'}, "fast", easing, callback); }; 

Here you will need CSS:

 .pop { background-color:#FFFFFF; border:1px solid #999999; cursor:default; display: none; position:absolute; text-align:left; z-index:500; padding: 25px 25px 20px; margin: 0; -webkit-border-radius: 8px; -moz-border-radius: 8px; } a.selected { background-color:#1F75CC; color:white; z-index:100; } 

We need to send the auth token when we do POST, PUT or DELETE. Add this line under the existing JS tag (possibly in your layout):

 <%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? -%> 

Almost done. Open your application controller and add the following filters:

 before_filter :correct_safari_and_ie_accept_headers after_filter :set_xhr_flash 

And related methods:

 protected def set_xhr_flash flash.discard if request.xhr? end def correct_safari_and_ie_accept_headers ajax_request_types = ['text/javascript', 'application/json', 'text/xml'] request.accepts.sort!{ |x, y| ajax_request_types.include?(y.to_s) ? 1 : -1 } if request.xhr? end 

We need to discard flash messages if this is an ajax call, otherwise you will see flash messages from the "past" in the next HTTP request. A second filter is also required for webkit and IE browsers. I am adding these 2 filters to all my Rails projects.

All that remains is the destroy action:

 def destroy @product.destroy flash[:notice] = "Successfully destroyed product." respond_to do |format| format.html { redirect_to redirect_to products_url } format.js { render :nothing => true } end end 

And you have it. Unobtrusive removal using Rails. It seems that most of the work has been printed, but it really is not so bad as soon as you start. You may be interested in this Railscast .

+3
Nov 10 '09 at 1:41
source share

By the way. the rails have changed a bit, so you can no longer use _delete, now use _destroy.

 def remove_child_link(name, f) f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child") end 

It was also easier for me to just remove the html that is for new entries ... so I do this

 $(function() { $('form a.add_child').click(function() { var association = $(this).attr('data-association'); var template = $('#' + association + '_fields_template').html(); var regexp = new RegExp('new_' + association, 'g'); var new_id = new Date().getTime(); $(this).parent().before(template.replace(regexp, new_id)); return false; }); $('form a.remove_child').live('click', function() { var hidden_field = $(this).prev('input[type=hidden]')[0]; if(hidden_field) { hidden_field.value = '1'; } $(this).parents('.new_fields').remove(); $(this).parents('.fields').hide(); return false; }); }); 
+2
Aug 16 '10 at 1:33
source share

FYI, Ryan Bates now has a gem that works great: nested_form

+2
Jan 17 '12 at 3:25
source share

I created an unobtrusive jQuery plugin to dynamically add fields_for objects to Rails 3. It is very easy to use, just download the js file. Almost no configuration. Just follow the conventions and you're good to go.

https://github.com/kbparagua/numerous.js

It is not super flexible, but it will do the job.

+1
Aug 25 '12 at 18:55
source share

I have successfully (and quite painlessly) used https://github.com/nathanvda/cocoon to dynamically create my forms. Handles associations conveniently and the documentation is very simple. You can use it with simple_form too, which was especially useful to me.

+1
Jan 21 '13 at 15:55
source share



All Articles