Search code examples
javascriptjqueryruby-on-rails-3simple-formcocoon-gem

Dynamically adding prepopulated nested forms using cocoon and rails3-jquery-autocomplete


Thanks for your time.

I am working on rails 3.2 and I am using gems simple_form, cocoon, and rails3-jquery-autocomplete.

I have following models Machine, Part, PartUsage, MachineCosting and PartCosting

Machine and Part models with many-to-many association.

    class Machine < ActiveRecord::Base
      attr_accessible :name

      has_many :part_usages
      has_many :parts, :through => :part_usage
    end

    class Part < ActiveRecord::Base
      attr_accessible :name
    end

    class PartUsage < ActiveRecord::Base
      attr_accessible :machine_id, 
                      :part_id
      belongs_to :part
      belongs_to :machine
    end

MachineCosting and PartCosting models with one-to-many association.

    class MachineCosting < ActiveRecord::Base

       attr_accessible :machine_id,
                       :total_cost,
                       :machine_name,
                       :part_costings_attributes

       attr_accessor :machine_name                

       has_many :part_costings, :dependent => :destroy

       accepts_nested_attributes_for :part_costings, :allow_destroy => true    

       def machine_name
         self.machine.try(:name)
       end

    end

    class PartCosting < ActiveRecord::Base
      attr_accessible :part_id,
                      :part_name,
                      :cost

      attr_accessor :part_name

      belongs_to :machine_costing
      belongs_to :part

      def part_name
        self.part.try(:name)
      end
    end

Form for MachineCosting

views/machine_costings/_form.html.erb
   <%= simple_form_for(@machine_costing, :html => {:multipart => true}) do |f| %>

     <%= f.input :machine_id, :url => autocomplete_machine_id_machines_path, 
            :as => :autocomplete %>

        <!-- HERE, WHENEVER I SELECT A MACHINE, I WANT TO ADD NESTED-FORMS FOR 
     PARTCOSTINGS CORRESPONDING TO ALL THE PARTS OF THE MACHINE I JUST SELECTED.-->

        <%= f.simple_fields_for :part_costings do |part_costing|%>   
          <%= render 'part_costings/part_costing_fields', :f => part_costing %>
        <% end %>

   <% end %>

Please suggest how can I populate and dynamically add fixed no of fields for PartCostings using javascript after machine_id is selected in the field.

I would be happy to provide any more information.

Thanks again!!


Solution

  • First, summary of my approach.

    • In MachineCosting form, use 'rails3-jquery-autopopulate' gem to search for Machines by their name.
    • Hack MachineController to also send 'Parts info' together with matching machines in json form.
    • When user selects a machine, use javascript to add nested_forms for PartCostings and partially populate them using data recieved in json form.

    Now here is how I have done it (code part).

    views/machine_costings/_form.html.erb

        <%= simple_form_for(@machine_costing, :html => {:multipart => true}) do |f| %>
            <%= f.error_notification %>
    
            <%= f.input :machine_name, :url => autocomplete_machine_name_machines_path, 
                      :as => :autocomplete, :input_html => { :id_element   =>  "#machine_costing_machine_id"} %>
    
            <%= f.association :machine, :as => :hidden %>
    
            <div id='part_costings'>
              <%= f.simple_fields_for(:part_costings) do |part_costing|%>   
                <%= render 'part_costings/part_costing_fields', :f => part_costing %>
              <% end %> 
    
              <div class="links hidden">
                <%= link_to_add_association 'Add part costings', f, :part_costings, 
                  :partial => 'part_costings/part_costing_fields' %>
              </div>
            </div>
    
            <%= f.button :submit%>
        <% end %>
    

    views/part_costings/_part_costing_fields.html.erb

        <div class= 'nested-fields'>    
                <!-- DO NOT CHANGE THE ORDER OF ATTRIBUTES ELSE machine_costing.js WILL FAIL -->
               <%= f.input :part_id, :as=> :hidden %>
               <%= f.text_field :part_name, :disabled => 'disabled' %>           
               <%= f.input :cost %>
               <%= link_to_remove_association '<i class="icon-remove"></i>'.html_safe, f ,:class =>"part_costing_remove" %>
        </div> 
    

    controllers/machines_controller.rb

        class MachinesController < ApplicationController
    
          # OVER RIDING THIS BY OWN METHOD
          # autocomplete :machine, :name, :full=> true, :extra_data => [:machine_id]
    
          def autocomplete_machine_name
            respond_to do |format|
              format.json {
                @machines = Machine.where("name ilike ?", "%#{params[:term]}%")
    
                render json: json_for_autocomplete_machines(@machines)
              }
            end
          end
    
          def json_for_autocomplete_machines(machines)
            machines.collect do |machine|
    
              parts = Hash.new
              unless machine.part_usages.empty?
                machine.part_usages.each do |part_usage|
                  parts[part_usage.part.id] = part_usage.part.name
                end
              end
    
              hash = {"id" => machine.id.to_s, "value" => machine.name,
                                "parts" => parts} 
    
              hash
            end
          end 
    
          #
          #
          #
    
        end
    

    And here is the javascript part.

    assest/javascripts/machine_costings.js

        $(function() {
    
          // Part Costings
          var part_id;
          var part_name;
    
          $('#machine_costing_machine_name').bind('railsAutocomplete.select', function(event, data){
            console.debug("Machine selected...");
            parts = data.item.parts;
            console.debug("Removing existing part_costings...");
            $('.part_costing_remove').click();
            console.debug("Adding part_costings...");
            for(var id in parts) {
             part_id = id;
             part_name = parts[id];
             $('.add_fields').click();
            }
            console.debug("Done adding all part_costings...");
          });
    
          $('#part_costings').bind('cocoon:after-insert', function(e, part_costing) {
            part_costing[0].getElementsByTagName('input')[0].value = part_id;
            part_costing[0].getElementsByTagName('input')[1].value = part_name;
            console.debug("Added part_costing...");
          });
    
        });
    

    routes.rb

      resources :machines  do
          get :autocomplete_machine_name, :on => :collection
      end
    

    The critical points in javascript solution are...

    1. Method "bind('railsAutocomplete.select', function(event, data)" from rail3-jquery-autocomplete gem.

    2. Method "bind('cocoon:after-insert', function(e, part_costing)" from cocoon gem.

    That's all. Please let me know if you have some suggestions. Thanks :)