Wednesday, 20 July 2016

javascript - Rails/Ajax change form from create to update after submission




So I wanted to play with a star ratings page, where I have 5 rating questions and you can rate once (create) and update your rating at any given time. The rating form is ajaxified but the problem is the form element stays as a Create method after submission instead of an update method.



I followed a pretty cool tutorial by eighty-b but there is one part that is missing about the form being updated to the proper method.



Here is the code



I have a form in a partial rating_questions/_quick_form.html.erb with a nested form for each rating form. Note that the form route is using a helper to define if it should be a create or and update action




<% @rating_questions.each do |rating_question| %>

<%= render partial: "rating_questions/rating_form", :locals => {:rating_question => rating_question} %>
<% end %>



The rating_form.html.erb partial



# EDIT: I added the temporary random id in an instance variable to make sure the form id and hidden_field have the same reference

<% @rand_id = SecureRandom.hex %>

<%= form_for(rating_ballot(rating_question), :remote => true, :html => { :class => 'rating_ballot', :id => @rand_id}) do |f| %>
<%= rating_question.title %>

<%= hidden_field_tag :temp_id, @rand_id %>
<%= f.label("value_#{rating_question.id}_1", content_tag(:span, '1'), {:class => "rating", :id => "1"}) %>
<%= radio_button_tag("rating[value]", 1, current_user_rating(rating_question) == 1, :class => 'radio_button', :id => "rating_value_#{rating_question.id}_1") %>
... (the other rating radio buttons) ...
<%= f.hidden_field("rating_question_id", :value => rating_question.id) %>
<%= f.submit :Submit, :id => "rating_submit" %>
<% end %>



Then in my create.js.erb I added the line to replace the form with the partial



$('#<%= params[:temp_id] %>').replaceWith("<%= j render(partial: 'rating_questions/rating_form', locals: {:rating_question => @rating_question}) %>");


The helper methods for defining if the form should be a create or an update if there is an existing record



def rating_ballot(rating_question)
if @rating = current_user.ratings.find_by_rating_question_id(rating_question.id)

[rating_question, @rating]
else
[rating_question, current_user.ratings.new]
end
end

def current_user_rating(rating_question)
if @rating = current_user.ratings.find_by_rating_question_id(rating_question.id)
@rating.value
else

"N/A"
end
end


and my ratings_controller.rb that calls for create.js.erb and update.js.erb



def create
@rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
@rating = Rating.new(params[:rating])

@rating.user_id = current_user.id
if @rating.save
respond_to do |format|
format.html { redirect_to rating_questions_path, :notice => "Your rating has been saved"}
format.js
end
end
end

def update

@rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
@rating = current_user.ratings.find_by_rating_question_id(@rating_question.id)
if @rating.update_attributes(params[:rating])
respond_to do |format|
format.html { redirect_to rating_questions_path, :notice => "Your rating has been updated"}
format.js
end
end
end



Obviously I only want to update the form that was just submitted and not the other ones.
Any idea on how to reload the form with the proper method using Javascript in the create.js.erb and the update.js.erb?



Thank you very much!



EDIT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(See the edits above)



I went ahead with your suggestions and I added the random id to the form and changed the create.js.erb, but I encountered problems:




1) After the ratings forms are replaced, the Javascript no longer works on that partial.



Here is also the coffee script for dynamic interactions of the stars (actually the form radio labels)



$ ->
# Add the "Bright class" to the stars for each checked radio button before the document is ready.
$("form.rating_ballot").each ->
checkedId = undefined
checkedId = $(this).find("input:checked").attr("id")

$(this).find("label[for=" + checkedId + "]").prevAll().andSelf().addClass "bright"

$(document).ready ->
# makes stars glow on hover.
$("form.rating_ballot > label").hover (-> # mouseover
$(this).prevAll().andSelf().addClass "glow"
), -> # mouseout
$(this).siblings().andSelf().removeClass "glow"

# makes stars stay glowing after click.

$("form.rating_ballot > label").click ->
$(this).siblings().removeClass "bright"
$(this).prevAll().andSelf().addClass "bright"

# Submits the form (saves data) after user makes a change.
$(".rating_ballot").change ->
$(this).submit()


EDIT 2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The fact that the update action was submitting all the form was corrected.


Answer



If I got you right...



1. Add a generated ID as a hidden field to your form
This ID does not really have to be a virtual attribute for your model but can be. Another option is to put it outside of your mass-assignment array, like e.g params[:controller] also is.



Example: <%= hidden_field_tag :temporary_id, SecureRandom.hex %>. For other possibilities on creating a random string, have a look at how-best-to-generate-a-random-string-in-ruby .



Also make sure that your form (or the first element which gets rendered in your form partial) has that generated random string as id or data- attribute, so you can access it without using a complicated selector in the next step.




2. Update your create.js.erb
I assume your format.js in your create action passes over to create.js.erb. So in there, you replace the whole form with a new one, by searching for that temporary id which got passed on submit.



Example: $('#').replaceWith('<%= j render(partial: 'form', locals: {...}) %>');



Make sure to not omit the j helper as it encodes your rendered form for save use in javascript. As you are rendering your usual form, the create action does its usual business, and now also replaces the form with a new rendered one, where automatically, the "update" action is called (the usual rails "magic" stuff in routing, that is. like you'd render another usual edit form).



Javascript




After the ratings forms are replaced, the Javascript no longer works on that partial.





Of course it isn't. You're binding events to specific elements on document load. The elements you insert afterwards (via create/update) are newly added, and thus do not have that event binding because they were not available at the time of the event-binding taking place.



Since events are "bubbling", you need to bind your code to a container which is persistent across your js-modifications. Lets say your create.js only updates/replaces elements in #rating_questions_container, you'd need to rewrite the js to e.g.



$('#rating_questions_container').on('click', 'form.rating_ballot > label', function(e) {
# code
});




  • $('#rating_questions_container') is the "persistent" container on your page, that can capture the events

  • .on('click', 'form.rating_ballot > label' watches for click events on elements matching the selector in the second argument, here form.rating_ballot > label.



This allows to dynamically append/remove DOM nodes inside of #rating_questions_container without losing event listeners.


No comments:

Post a Comment

c++ - Does curly brackets matter for empty constructor?

Those brackets declare an empty, inline constructor. In that case, with them, the constructor does exist, it merely does nothing more than t...