3

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

<div class="rating_questions">
    <% @rating_questions.each do |rating_question| %>
        <%= render partial: "rating_questions/rating_form", :locals => {:rating_question => rating_question} %>
    <% end %>
</div>

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| %>
   <div class="rq"><%= rating_question.title %></div>
   <%= 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.

Zedrian
  • 909
  • 9
  • 29

1 Answers1

3

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: $('#<random_id_of_your_form>').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.

Community
  • 1
  • 1
pdu
  • 10,295
  • 4
  • 58
  • 95
  • Hello thank you for your prompt answer, I guess my question is not very clear and it is in 2 parts. If I understand correctly, the random id is to help define the specific form that I wish to update? This will definitely help since I have 5 similar forms. After digging deeper with Ajax, when it reloads the form, it doesn't use the helper method `rating_ballot`, which defines the route of the form `rating_questions/1/ratings # create` or `rating_questions/1/ratings/1 # update`. I will try your solution and see if it solves half of my problem :) – Zedrian Nov 06 '13 at 10:27
  • Yes, that is the intent of the random id. I saw your helper, and that should work since you pass an array with both objects to build the path. You just need to put your form into a partial that can handle both `create` and `update`, which should be the case right now. – pdu Nov 06 '13 at 11:56
  • So I followed your suggestions but I encountered further problems. 1) When I submit one rating (whether create or update) it reloads the partial for them. 2) The jQuery in Coffescript file is no longer used after the partial is reloaded. See my edits in my original post. – Zedrian Nov 07 '13 at 04:52
  • Updated my answer to solve your javascript issues. But I don't get your first problem; You want to update the partial, don't you? Since this updates the form tag and everything inside it, which is required for a properly working update-form. – pdu Nov 07 '13 at 08:12
  • Ok well this was a bit crazy, but I ended adding the .on() function everywhere. And now everything works! Thank you so much pduersteler! (for some reason I have to wait 1 hour before giving the bounty -.-) – Zedrian Nov 07 '13 at 08:32