Topic: Passing objects to partials and RJS in dynamic multi-model form

I'm having trouble getting objects to persist in order to pass them to partials when requested by an AJAX event (link_to_remote).  The objects are present when the page is generated, but no longer available when requested by a user event.

In my app, I've built a fairly complex form that allows users to add Collections to a music database.  Songs are associated with Collections through a tracklisting which uses the techniques outlined in ryanb's awesome Creating Variable Number of Models in One Form tutorial.  I've extended the functionality by allowing the user to associate multiple Artists with each Song that they add to the Collection.  For example: "I Left My Heart in San Francisco" by Frank Sinatra, Dean Martin, and Sammy Davis Jr.

I've got each element of the form separated out into partials.  For example, when creating a new Collection, the view iterates over 30 initial Song objects and builds each with a call to _song_fields.rhtml:

# in views/collections/_form.rhtml    
<ul id="..."> 
    # ...
    <!-- SONG FIELDS -->
    <% @collection.songs.each_with_index do |song, index| %>
        <%= render :partial => 'song_fields', :locals => { :song => song, :index => index, :disc_num => 1 } %>
    <% end %>   
    # ...

The _song_fields.rhtml partial then outputs a list item with the fields for the Song (position, songName, etc), and calls a partial to generate a drop-down select list containing Artists in the database that can be associated with that song:
# in views/collections/_song_fields.rhtml
<% fields_for "disc_#{disc_num}_songs[#{index}]", song do |s| %>         
    <li id="...">
        # ...
        <div class="artist_column">
            <!-- ADD ARTIST LINK -->
            <div id="<%= "disc_#{disc_num}_song_#{index}_ajax_loader" %>" class="song_ajax_loader" style="display: none; float: left; padding: 4px 3px 0px 0px;">
                <%= image_tag("/icons/ajax_loader_small.gif") %>
            <%= render :partial => 'add_song_artist_link', :locals => { :s => s, :song => song, :song_index => index, :artist_index => 1, :disc_index => disc_num } %>                   
            <!-- SONG ARTIST FIELDS -->
            <div id="<%= "disc_#{disc_num}_song_#{index}_artists" %>" style="float: left;">
                <%= render :partial => 'song_artist_fields', :locals => { :s => s, :song => song, :song_index => index, :artist_index => 1, :disc_index => disc_num } %>
        # ...
<% end %>

Here is the _song_artist_fields.rhtml partial which outputs the Artist select list:
# in views/collections/_song_artist_fields.rhtml
<div id="<%= "disc_#{disc_index}_song_#{song_index}_artist_#{artist_index}" %>" style="clear: both;">
    <%    # initial artist (song[:artist_id])
            if artist_index == 1 %>
        <%= :artist_id, Artist.alphabetical_list.collect {|a| [a.fullName,]}, { :include_blank => true }, :style => "width: 210px;", :class => "song_artist_field", :selected => song["artist_id"] %><br />
    <%    # additional artists (song[:artist_id_2], etc.)
            else %>
        <%= "artist_id_#{artist_index}", Artist.alphabetical_list.collect {|a| [a.fullName,]}, { :include_blank => true }, :style => "width: 210px;", :class => "song_artist_field", :selected => song["artist_id_#{artist_index}"] %>
    <% end %>

And the add_song_artist_link partial:
# in views/collections/_add_song_artist_link.rhtml
<div id="<%= "disc_#{disc_index}_song_#{song_index}_add_song_artist_link" %>" style="float: left; padding: 4px 5px 0px 0px;">
    <% if artist_index.to_i < 5 %>
        <%= link_to_remote(image_tag("/icons/add_artist.png", :class => "image", :alt => "Add an additional Artist to this Song", :title => "Add an additional Artist to this Song"),
            :url => { :action => 'add_song_artist', :song => song, :s => s, :song_index => song_index, :disc_index => disc_index, :artist_index => artist_index },
            :loading     => "Element.hide('disc_#{disc_index}_song_#{song_index}_add_song_artist_link');'disc_#{disc_index}_song_#{song_index}_ajax_loader');",
            :complete     => "Element.hide('disc_#{disc_index}_song_#{song_index}_ajax_loader');'disc_#{disc_index}_song_#{song_index}_add_song_artist_link');")
    <% else %>
        <%= image_tag("/icons/add_artist_max.png", :class => "image", :alt => "You may only associate up to 5 Artists with a Song", :title => "You may only associate up to 5 Artists with a Song") %>
    <% end %>

And the final piece of the puzzle is the RJS file which generates each new Artist field as it is requested by the user:
# in view/collections/add_song_artist.rjs
# assign parameters to variables
s = params[:s]
#logger.debug "s: " + params[:s].inspect
song = params[:song]
#logger.debug "song: " + params[:song].inspect
song_index = params[:song_index]
#logger.debug "song_index: " + params[:song_index].to_s
disc_index = params[:disc_index]
#logger.debug "disc_index: " + params[:disc_index].to_s
artist_index = params[:artist_index]
#logger.debug "artist_index: " + params[:artist_index].to_s

#cap additional artists at 5
if artist_index.to_i < 5

    # add a new song artist drop-down menu to the selected song list item
    page.insert_html :bottom, "disc_#{disc_index}_song_#{song_index}_artists", :partial => 'song_artist_fields',
                                     :locals => { :s => s, :song => song, :song_index => song_index, :disc_index => disc_index, :artist_index => artist_index }
    # reset the index to be assigned to the next new artist
    page.replace "disc_#{disc_index}_song_#{song_index}_add_song_artist_link", :partial => 'add_song_artist_link', :locals => { :song => song, :s => s, :song_index => song_index, :disc_index => disc_index, :artist_index => artist_index.to_i + 1 }
    # display visual effect
    page["disc_#{disc_index}_song_#{song_index}_artist_#{artist_index}"].visual_effect(:highlight, :startcolor => "#FFFF99", :endcolor => "#F7FFF2")

    # replace add_song_artist_link with blank
    page.replace "disc_#{disc_index}_song_#{song_index}_add_song_artist_link", :partial => 'add_song_artist_link', :locals => { :song => song, :s => s, :song_index => song_index, :disc_index => disc_index, :artist_index => artist_index.to_i + 1 }

I realize that's a lot of code to look at, but if you're still with me I greatly appreciate it.  Now the point: when the form is generated (by calling /collections/new), the 30 song objects are created correctly, with artist fields associated.  If I simply add songs to the Collection using the form, and restrict myself to single-Artist Song entries, everything works as expected. 

However, when I attempt to add an additional Artist to a Song, I run into problems.  The AJAX part of things is working fine (if I replace the Artist select list in _song_artist_fields.rhtml with generic HTML, it is added to the div as expected). The issue is that the s (ActionView::Helpers::FormBuilder object) and song (ActiveRecord::Song object) objects are no longer recognized/available as objects.  If I output their classes to the log after the AJAX call (using logger.debug or logger.debug, the first comes up as a String ("#<ActionView::Helpers::FormBuilder:0x3516668>") and the second as nil.  So, after building the form, these objects are no longer available to me for AJAX/RJS calls. 

I don't understand why they are available at runtime, but not afterward.  I expect this has to do with how each partial is initially called within a loop, and so the variable is being overwritten each time (and thus no longer present for subsequent requests)?  If I'm missing something obvious, please let me know. 

Here's the error message I get, indicating that the song object I expected to have access to is nil:

ActionView::TemplateError (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]) on line #13 of app/views/collections/_song_artist_fields.rhtml:
10:                     Artist.alphabetical_list.collect {|a| [a.fullName,]}, { :include_blank => true },
11:                     :style => "width: 210px;",
12:                     :class => "song_artist_field",
13:                     :selected => song["artist_id_#{artist_index}"]
14:             %>
15:     <% end %>
16: </div>

Alternatively, if I comment out the :selected parameter to the select list, I get this error:

ActionView::TemplateError (wrong number of arguments (4 for 0)) on line #9 of app/views/collections/_song_artist_fields.rhtml:
6:      <%      # additional artists (song[:artist_id_2], etc.)
7:                      else %>
8:              <%=
9:             "artist_id_#{artist_index}",
10:                     Artist.alphabetical_list.collect {|a| [a.fullName,]}, { :include_blank => true },
11:                     :style => "width: 210px;",
12:                     :class => "song_artist_field"

This is harder to understand (arguments for what?), but I believe still stems from trying to access s as a FormBuilder object when it is in fact a String object at this point.

After reading up on passing variables and objects to partials here, I'm still not closer to a solution.  Any help is much appreciated!

Last edited by johnny.rodgers (2007-10-05 15:36:18)

Re: Passing objects to partials and RJS in dynamic multi-model form

Objects can't be passed through link_to_remote. Instead these are converted to strings so they can be passed through HTTP to the server. This means you'll have to reconstruct these objects in the resulting controller action/rjs template.

However, there's a much better way to do all of this. See the latest Railscasts episode on complex forms. In the next part (released this Monday) I'll show how to make it dynamic which will basically duplicate what I did in the tutorial, only much simpler. I'll try to get around to rewriting the tutorial as well.

Railscasts - Free Ruby on Rails Screencasts

Re: Passing objects to partials and RJS in dynamic multi-model form

Thanks Ryan!  That helps explains things.  I should have looked into the link_to_remote API more thoroughly.  I'll check out the pair of Railscasts you mention once I return to a fast connection after Thanksgiving weekend (here in Canada).  Cheers.

Re: Passing objects to partials and RJS in dynamic multi-model form

After watching the Railscasts mentioned above, I see that there is a cleaner way of accomplishing  the multi-model form I've developed with RJS and partials.  However, being this close to a solution and having to do some significant reconfiguring wasn't very appealing to me, so I took another look at the problem.  As Ryan pointed out in his comments for Episode 73, what is important in all this is the HTML that is generated by the code (regardless of how you go about it).  With that in mind, I abandoned the FormBuilder syntax for the additional Song Artist fields, and simply wrote an HTML select list, dynamically named with the passed variables, and then populated it with the Artists in the DB.  The result works, and is relatively simple:

# in views/collections/_song_artist_fields.rhtml
<div id="<%= "disc_#{disc_index}_song_#{song_index}_artist_#{artist_index}" %>">
    <%    # initial artist (song[:artist_id])
            if artist_index == 1 %>
        <%= :artist_id, Artist.alphabetical_list.collect {|a| [a.fullName,]}, { :include_blank => true }, :selected => song["artist_id"] %><br />
    <%    # additional artists (song[:artist_id_2], etc.)
            else %>
            <select id="<%= "disc_#{disc_index}_songs[#{song_index}]_artist_id_#{artist_index.to_i + 1}" %>" name="<%= "disc_#{disc_index}_songs[#{song_index}][artist_id_#{artist_index.to_i + 1}]" %>">
                <option value="" selected="selected"></option>
                <% # populate select list with Artists
                    for artist in Artist.alphabetical_list %>
                        <option value="<%= artist["id"].to_s %>"><%= artist.fullName %></option>
                    <% end %>                           
    <% end %>