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

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!!

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...
Method "bind('railsAutocomplete.select', function(event, data)" from rail3-jquery-autocomplete gem.
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 :)

Related

How to avoid sending params of fields deleted using JQuery remove()

In this code I was trying to remove fileds for nested_attributes using ajax :remote => ture to avoid reloading
the whole page in browser. Although fileds in fields_for was removed from DOM and association was removed from database, the fields of the nested attributes
still exist in page source and raise ActiveRecord::RecordNotFound error when trying to send params to update action of parent model
consider the following code:
_artist_form.html.erb
<%= form_for #artist do |f| %>
<%= f.label :name %>
<%= f.text_field :name %><br/>
<%= f.label :style %>
<%= f.text_field :style %><br/>
<%= f.fields_for :songs do |song_builder|%>
<div id = 'song_<%= song_builder.object.id %>_div'>
<%= song_builder.label :title %>
<%= song_builder.text_field :title %><br/>
<%= song_builder.label :lyrics %>
<%= song_builder.text_area :lyrics %><br/>
<%= link_to 'Remove song', delete_song_path(:a_id => #artist.id, :s_id => song_builder.object.id),
:method => :delete , :remote => true %>
</div>
<% end %>
<%= f.submit 'Save' %>
<% end %>
routes.rb
Rails.application.routes.draw do
...
delete '/artists/remove_song', :to => 'artists#delete_song', :as => :delete_song
end
application_controller.rb
class ArtistsController < ApplicationController
def edit
...
end
def update
#artist = Artist.find(params[:id])
if #artist.update(artist_params) #=> error Couldn't find Song with ID=2 for Artist with ID=2
redirect_to artist_path(#artist)
else
flash[:errors] = #artist.errors.full_messages
render :edit
end
end
...
def delete_song
#song_id = params[s_id]
aritst = Artist.find(:params[a_id])
song = artist.songs.find(#song_id)
song.delete
respond_to do |format|
format.js {render 'delete_song.js.erb'}
end
end
end
delete_song.js.erb
$('#song_<%= #song_id %>_div').remove() ;
Error
Couldn't find Song with ID=2 for Artist with ID=2
how to prevent sending params of removed fields by $(...).remove() to update action?
I tried to find a solution for this error. So according to charlietfl comment, I tried to store delete status somewhere locally, then rails can delete association later. So I modified the code as following:
deleting all remote script code including delete_song.js.erb file and delete_song action and delete route. then I allowed marking nested attribute for delete in Artist model file:
accepts_nested_attributes_for :songs, :allow_destroy => true
then adding delete button in _artist_form.html.erb file as following:
<%= button_tag 'x' , :class => 'close_sign', :type => 'button', :onclick => "$('#song_#{song_builder.object.id}_destroy').val('true'); $('#song_#{song_builder.object.id}_div').hide()" %><br/>
and a hidden flied to fields_for as below:
<%= song_builder.hidden_field :_destroy, :id => "song_#{song_builder.object.id}_destroy" %>
and allowing :songs_nested_attributes => [:title, :lyrics, :_destroy] in song_params
once user remove the song field, it will be hidden and marked for destroy later

Allowing users to volunteer for a task via ajax

My app is setup where a user owns a task and other user's can volunteer to complete these tasks. My models are setup as so:
User
class User < ActiveRecord::Base
has_many :participations, foreign_key: :participant_id
has_many :owned_tasks, class_name: "Task", foreign_key: :owner_id
end
Participation (Join Table)
class Participation < ActiveRecord::Base
enum status: [:interested, :selected]
belongs_to :task
belongs_to :participant, class_name: "User"
end
Task
class Task < ActiveRecord::Base
enum status: [:open, :in_progress, :complete]
has_many :participations
has_many :participants, through: :participations, source: :participant
# Dynamically generates relations such as 'selected_participants'
Participation.statuses.keys.each do |status|
has_many "#{status}_participants".to_sym,
-> { where(participations: { status: status.to_sym }) },
through: :participations,
source: :participant
end
belongs_to :owner, class_name: "User"
end
What I would like do is simply allow users to click a button to volunteer for a task within that particular task's show view.
I can accomplish this with ease inside my rails console:
user = User.first
task = Task.first
user.owned_tasks << task
user_2 = User.find(2)
task.participants << user_2
Where I get stuck is trying to figure out how to setup the necessary controller code to get this to work. I'm also not sure how/where to create the conditional that checks if a user is already participating in a task is_participating?. Does it go in the join mode Participation or the Task table?
I think I have a vague idea on what the view should look like:
Task - Show View
<% unless current_user == #task.owner %>
<div class="volunteer-form">
<% if current_user.is_participating? %>
<%= render 'cancel' %>
<% else %>
<%= render 'volunteer' %>
<% end %>
</div>
<% end %>
_volunteer.html.erb:
<%= form_for(current_user.participations.build(participant_id: current_user), remote: true) do |f| %>
<div><%= f.hidden_field :participant_id %></div>
<%= f.submit "Volunteer" %>
<% end %>
_cancel.html.erb:
<%= form_for(current_user.participations.find_by(participant_id: current_user), html: { method: :delete }, remote: true) do |f| %>
<%= f.submit "Cancel" %>
<% end %>
JS
// create.js.erb
$(".volunteer-form").html("<%= escape_javascript(render('tasks/volunteer')) %>");
// destroy.js.erb
$(".volunteer-form").html("<%= escape_javascript(render('tasks/cancel')) %>");
From your view looks like is_participating? belongs in the User model, it should probably be something along these lines:
def is_participating?(task_id)
participations.where(task_id: task_id).exists?
end
As for the controller code you probably want something along these lines:
class ParticipationsController
# Note I assume you have access to current_user here
def create
participation = current_user.participations.create(participation_params)
respond_to do |format|
format.js
end
end
def destroy
participation = current_user.participations.find(params[:id])
participation.destroy
respond_to do |format|
format.js
end
end
protected
def participation_params
params.require(:participation).permit :task_id
end
end
On _volunteer.html.erb (Note that you now have to pass the task into the partial):
<%= form_for(current_user.participations.build, remote: true) do |f| %>
<%= f.hidden_field :task_id, value: task.id %>
<%= f.submit "Volunteer" %>
<% end %>

Passing Form object when using Ajax in rails

I have a complex form for a survey. A survey
belongs_to :template
has_many :questions, :through => :template
has_many :answers, :through => :questions
I.e. User creates a new survey, and he must select a survey template. The survey template will create some default questions and answer fields.
<%= form_for #survey do |f| %>
<p>Select a template:</p>
<%= render #templates, :locals => {:patient => #patient }, :f => f %>
<% end %>
Then I render the _templates partial, and I still have access to the form object. From here, the idea is that a user can click on a template name, and the template questions and answers will be rendered via Ajax.
<% #templates.each do |template| %>
<div class="thumbnail">
<%= link_to template.name,
{ :controller => "surveys",
:action => "new",
:template_id => template.id,
:patient_id => nil,
:f => f,
:remote=> true },
:class=> "template", :id=> template.id %>
</div>
<% end %>
surveys#new:
respond_to :js, :html
def new
#template = Template.find(params[:template_id])
#patient = Patient.find(params[:patient_id])
end
new.js.erb:
$('#assessment').append("<%= j (render new_survey_path, :locals => {:f => f} ) %>");
new_survey_path:
<%= f.fields_for :questions do |builder| %>
<% #template.questions.where(category:"S").each do |question| %>
<p><%= question.content %></p>
<%= render 'answer_fields', :question=> question %>
<% end %>
<% end %>
But I'm having trouble passing the original |f| object from #survey to new_survey_path. The last place I can access the form object is in the templates partial.
How can I fix this?

Nested devise form not linking models

I'm trying to create a new location and a devise user in the same form and link them. The user and the location are created, but the location_id is not saving to the user. There is a location_id column in the user table.
My form
<% resource.build_location %>
<%= simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => {:class => 'form-vertical' }) do |f| %>
<%= f.error_notification %>
<!-- capture location details hidden values -->
<%= f.fields_for :location do |location_form| %>
<%= location_form.text_field :name, :name => "name", :type => "hidden" %>
<%= location_form.text_field :street_address, :name => "formatted_address", :type => "hidden" %>
<%= location_form.text_field :lat, :name => "lat", :type => "hidden" %>
<%= location_form.text_field :long, :name => "lng", :type => "hidden" %>
<% end %>
<!-- devise user authenticate -->
<%= f.input :name, :autofocus => true %>
<%= f.input :email, :required => true %>
<%= f.input :password, :required => true %>
<%= f.input :password_confirmation, :required => true %>
<%= f.button :submit, 'Sign up', :class => 'btn-primary' %>
<% end %>
<%= render "devise/shared/links" %>
Location model
class Location < ActiveRecord::Base
has_many :users, :dependent => :destroy
accepts_nested_attributes_for :users, :allow_destroy => true
attr_accessible :lat, :long, :name, :street_address
attr_accessible :user_attributes
end
Location controller
def new
#location = Location.new
#location.user.build
respond_to do |format|
format.html # new.html.erb
format.json { render json: #location }
end
end
# GET /locations/1/edit
def edit
#location = Location.find(params[:id])
end
# POST /locations
# POST /locations.json
def create
#location = #user.location.build(params[:location])
respond_to do |format|
if #location.save
format.html { redirect_to #location, notice: 'Location was successfully created.' }
format.json { render json: #location, status: :created, location: #location }
else
format.html { render action: "new" }
format.json { render json: #location.errors, status: :unprocessable_entity }
end
end
end
User Model
class User < ActiveRecord::Base
belongs_to :location
accepts_nested_attributes_for :location
rolify
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :role_ids, :as => :admin
attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :location, :location_id, :location_attributes
end
User controller
class UsersController < ApplicationController
before_filter :authenticate_user!
def index
authorize! :index, #user, :message => 'Not authorized as an administrator.'
#users = User.all
end
def show
#user = User.find(params[:id])
end
def update
authorize! :update, #user, :message => 'Not authorized as an administrator.'
#user = User.find(params[:id])
if #user.update_attributes(params[:user], :as => :admin)
redirect_to users_path, :notice => "User updated."
else
redirect_to users_path, :alert => "Unable to update user."
end
end
def destroy
authorize! :destroy, #user, :message => 'Not authorized as an administrator.'
user = User.find(params[:id])
unless user == current_user
user.destroy
redirect_to users_path, :notice => "User deleted."
else
redirect_to users_path, :notice => "Can't delete yourself."
end
end
end
No errors when creating the user, just getting a location_id="nil" when creating the user. I can access the location and a location_id is created but not linked to the user. Any ideas on how to save the location_id to the user?
I am populating the location info with json returned from a google location api autocomplete and assigning to an element with name="". It seems that everything works fine when I manually enter location info, but fails when the fields are populated from the autocomplete.
The first thing to note is that you do not need accepts_nested_attributes_for on both models, only the one that has the has_many association. Also, it looks like the pluralization is wrong in the Location model for users attributes
class Location < ActiveRecord::Base
has_many :users, :dependent => :destroy
accepts_nested_attributes_for :users, :allow_destroy => true
attr_accessible :lat, :long, :name, :street_address
attr_accessible :users_attributes # <- This should be plural
end
Remove the accepts_nested_attributes_for in the user model
class User < ActiveRecord::Base
belongs_to :location
accepts_nested_attributes_for :location # <- This should be removed
rolify
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :role_ids, :as => :admin
attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :location, :location_id # Also remove location_atributes
end
Also, this line
#location = #user.location.build(params[:location])
Should be
#location = Location.new(params[:location)
since the way your models are setup now a location has a user so you do not need to build a location from a user. That being said, I would recommend you create the association in the opposite direction where a user has_many locations, but this of course may be contrary to your function so take it with a grain of salt :).
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Javascript code just produces HTML printout, fails to render it

I'm a newbie to RoR and am following this railscast that explains how to remove and add elements within nested forms. In my case, I'm trying to allow users to add/remove "items" from a shopping "list." Unfortunately, however, when I click "add item" in my browser, HTML is printed out, instead of rendered. Here's what it outputs:
<div class ="fields"> Quantity: <input id="list_items_attributes_1315942810959_quantity" name="list[items_attributes][1315942810959][quantity]" size="7" type="text" /> Name: <input id="list_items_attributes_1315942810959_name" name="list[items_attributes][1315942810959][name]" size="30" type="text" /> Category: <select id="list_items_attributes_1315942810959_category" name="list[items_attributes][1315942810959][category]"><option value="Produce">Produce</option> <option value="Dairy">Dairy</option></select> <p><input id="list_items_attributes_1315942810959__destroy" name="list[items_attributes][1315942810959][_destroy]" type="hidden" value="false" />remove</p> </div>
Add Item
I feel like I'm missing something simple. Is there some switch that I can turn on to get this to render? Thanks!
In case it's helpful, here's my list model:
class List < ActiveRecord::Base
has_many :items, :dependent => :destroy
accepts_nested_attributes_for :items, :reject_if => lambda { |a| a[:name].blank?}, :allow_destroy => true
belongs_to :user
end
Here's my short form for adding items to a list:
<%= form_for #list do |f| %>
<p>
<%= f.label :date %>
<%= f.date_select :date %>
</p>
<%= f.fields_for :items do |builder|%>
<%= render "item_fields", :f => builder %>
<% end %>
<p><%= link_to_add_fields "Add Item", f, :items %></p>
<%= submit_tag "Create List" %>
<% end %>
Here's my link_to_add_fields method in my application helper (it's a verbatim copy from the railscast):
def link_to_add_fields(name, f, association)
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
render(association.to_s.singularize + "_fields", :f => builder)
end
link_to_function(name, h("add_fields(this, '#{association}', '#{escape_javascript(fields)}')"))
end
And finally, here's my add_fields function in my application.js file:
function add_fields(link, association, content) {
var new_id = new Date().getTime();
var regexp = new RegExp("new_" + association, "g")
$(link).up().insert({
before: content.replace(regexp, new_id)
});
}

Categories

Resources