Topic: Multiple child models in one form - Marsvin?

Hi all,
This is particularly aimed at Marsvin as I want to build on his totally excellent multi model forms tutorial http://railsforum.com/viewtopic.php?id=28447 and hopefully marsvin can extend his tutorial somewhat if I can get this sorted but feel free to join in :-)

The problem.
A parent has multiple children
One of the child maintenance forms needs a lot of space for editing and there is not enough screen space to place this in an inline form as described in your tutorial and every other tutorial I can find.
There will be more children to add in over time and displaying a complete list of all children all of the time will become impractical.

The theory for the solution.

The parent form is a standard form_for that renders a partial for editing the actual data in the standard way "_form.html.erb" (Also described in marsvins tutorial).

In addition to the parent fields The _form.html.erb (Used by both the edit and the new.html.erb forms) needs the following.

Hidden lists for all children with links to show/hide the lists.

Lastly an editing area for editing the child data.

A submit button for this form will have the job of submitting the all the data for all it's children in one go to the contoller.

The child lists are just rows of data similar to a standard crud index.html.erb with just edit and delete links next to each row and a new record link.

The editing area is an empty div that will have the relevant child maintenance form "inserted" into it.

The child maintenance form has a link to add the data to the bottom of it's list if creating a new child record or replaceing the data in the relevant row if editing child data

Sounds simple enough but how to plumb it all together?

This is what I have so far

My particular requirement is for a user that has many roles which in turn has many addresses, eMail_addresses, social_network_links. The User also has a has_many addresses :thorough => :roles and there is more to come. but to keep this generalised I want to talk about parents and children and to look at a simple example that should work for whatever relationships I need for other models as well as this one.

The models

Parent and child models

class User < ActiveRecord::Base

  has_many :children, :dependent => :destroy

  accepts_nested_attributes_for :children, :allow_destroy => true
...
end

class Child < ActiveRecord::Base

  belongs_to :parent
...
end


Simple enough although my particulat requirement deals with STI and much mofre complex relationships but this should not matter.

The parent controller is just a standard generated controller with the normal 7 unmodified restfull CRUD actions.

class Admin::ParentsController < ApplicationController
  # GET /parents
  # GET /parents.xml
  def index
    @parents = Parent.all

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @parents }
    end
  end

  # GET /parents/1
  # GET /parents/1.xml
  def show
    @parent = Parent.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @parent }
    end
  end

  # GET /parents/new
  # GET /parents/new.xml
  def new
    @parent = Parent.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @parent }
    end
  end

  # GET /parents/1/edit
  def edit
    @parent = Parent.find(params[:id])
  end

  # POST /parents
  # POST /parents.xml
  def create
    @parent = Parent.new(params[:parent])

    respond_to do |format|
      if @parent.save
        flash[:notice] = 'Parent was successfully created.'
        format.html { redirect_to([:admin, @parent]) }
        format.xml  { render :xml => @parent, :status => :created, :location => @parent }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @parent.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /parents/1
  # PUT /parents/1.xml
  def update
    @parent = Parent.find(params[:id])

    respond_to do |format|
      if @parent.update_attributes(params[:parent])
        flash[:notice] = 'SiteOwner was successfully updated.'
        format.html { redirect_to([:admin, @parent]) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @parent.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /parents/1
  # DELETE /parents/1.xml
  def destroy
    @parent = Parent.find(params[:id])
    @parent.destroy

    respond_to do |format|
      format.html { redirect_to(admin_parents_url) }
      format.xml  { head :ok }
    end
  end
end


The views.
edit.html.erb
<h3>Editing Parent</h3>

<% form_for([:admin, @parent]) do |f| %>

  <%= render :partial => "form", :locals => {:f => f}%>
  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<%= link_to 'Show', [:admin, @parent] %> |
<%= link_to 'Back', admin_parents_path %>


_form.html.erb

<%= f.error_messages -%>
<div class="admin-form">
  <fieldset>
    <legend>Parent</legend>
    <p>
      <%= f.label :name -%>:
      <%= f.text_field :name -%>
    </p>
  </fieldset>
</div>
<%= link_to_function "Show/Hide addresses", {:id => "toggle_child_list"}%><!-- Controlled by jQuery code in application.js-->
<div id = "child_list">
  <h4>Children</h4>
  <%= link_to_function "New child" -%>
  <%= render :partial => "child_list", :collection => @parent.children -%>
</div>
<div id ="maintenance_form">
  <%= render :partial => 'child_form', :locals => {:f => f} %>
</div>

_child_list.html.erb
  <%- child = child_list -%>
  <p>
    <%= child.name %> - <%= link_to_function "edit", {:class => "edit"}} %>|<%= link_to_function "Remove", {:class => "remove"} %>
  </p>

_child_form.html.erb
<fieldset>
  <legend>Details...</legend>
  <% f.fields_for :children do |child| %>
    <p>
      Name<br />
      <%= child.text_field :name %>
    </p>
    <p>
      Description<br />
      <%= child.text_field :description %>
    </p>
  <% end %>
  <!-- This link needs to append this record to the child_list and cause this form to hide itself -->
  <%= link_to_function 'Done' %>

</fieldset>


application.js
jQuery(documant).ready(function($){
  $('#toggle_child_list').click(function() {
     $("#child_list").toggle("slow");
  });
});

All of the above works as far as it goes and the child list shows and hides really nicely but the obvious thing here is that there is no functionality attached to any of the links that need to edit or create child records.

This was deliberate as I didn't want to cloud the issue with my attempts which could be and probably are very wrong and I'm hoping to cover each function and what is required to make it all hang together.

I'd like to start with the new_child functionality first and move onto the edititing of the child record later. With those two issues sorted the remove should be a breeze.

There are also a couple of gotchas based around the fact that we only have one parent child relationship at the moment but again the introduction of further children can wait for the minuite until the main functionality can be sorted out.

I though I'd make a start by turning the new child link into a helper as per the tutorial and this is what I did.
In the _form.html.erb I replaced the

  <%= link_to_function "New child" -%>

With a
  <%= link_to_new_child(f) -%>

then I made the helper function very similar to the tutorial with the exception that I'm using jQuery rather than javascript plus rather than adding the form to the bottom of the child list I'm using the maintenance_form div

helper code (currently application_helper.rb)

  def link_to_new_child(form_builder)
    link_to_function 'add child' do |page|
      form_builder.fields_for :children, Child.new, :child_index => 'NEW_RECORD' do |f|
        html = render(:partial => 'child_form', :locals => { :f => f })
        page << "jQuery('#maintenance_form').append('#{escape_javascript(html)}'.replace(/NEW_RECORD/g, new Date().getTime()))"
      end
    end
  end

This kinda works as I get an editable form but the "Done" link (Maybe should be a button) in the _child_html.erb needs hooking up and rather than appending to the maintenance_form div I need to replace the maintenance_form div otherwise I'll end up with lots of forms rather than just one editable form but that's easily changed

  def add_address_link(form_builder)
    link_to_function 'add address' do |page|
      form_builder.fields_for :administrator_addresses, Address.new, :child_index => 'NEW_RECORD' do |f|
        html = render(:partial => 'address_form', :locals => { :f => f })
        page << "jQuery('#maintenance_form').replaceWith('#{escape_javascript(html)}'.replace(/NEW_RECORD/g, new Date().getTime()))"
      end
    end
  end

and I removed the render of the child form so that _form.html.erb now looks like this
[code=]<%= f.error_messages -%>
<div class="admin-form">
  <fieldset>
    <legend>Parent</legend>
    <p>
      <%= f.label :name -%>:
      <%= f.text_field :name -%>
    </p>
  </fieldset>
</div>
<%= link_to_function "Show/Hide addresses", {:id => "toggle_child_list"}%><!-- Controlled by jQuery code in application.js-->
<div id = "child_list">
  <h4>Children</h4>
  <%= link_to_new_child(f) -%>
  <%= render :partial => "child_list", :collection => @parent.children -%>
</div>
<div id ="maintenance_form">
</div>

So the link_to_new_child link will put a form in place that will give me editable fields for the child model.

Now for what I thought I should do.

The new child link should really just show/hide an editable child form and it should be the job of the "Done" link or button that makes use of the form builder and appends this data to the child_list.

That's as far as I have got.

Any contributions towards the theory and how to hook up those links properly would be massively appreciated.

James

Last edited by jamesw (2009-08-05 14:58:07)

What you want and what you need are too often not the same thing!
When your head is hurting from trying to solve a problem, stop standing on it. When you are the right way up you will see the problem differently and you just might find the solution.
(Quote by me 15th July 2009)

Re: Multiple child models in one form - Marsvin?

I now have a working version of what I wanted to do.
I'm tidying up the code and will post it here when I have it working.

This is a description of the problem I was facing.
Multi model form where the input data can't be dealt with all on one line.
Plus more than one child needs to be handled.

The problem.
User has many addresses.
user has_many eMails and so on

The addresses form is far too large to have as fields in one line so I wanted to have a list of addresses and eMail addresses that could be shown and hidden.
Each line needs an edit and a delete link.
The edit link needs to "grow" the address form underneath the address line and a link in the address form is needed in order to hide the address form.
I'm also using jQuery so the JS code is slightly different and I'm sure can be improved upon.

I'll post the code over the next couple of days after I have finished refactoring.

What you want and what you need are too often not the same thing!
When your head is hurting from trying to solve a problem, stop standing on it. When you are the right way up you will see the problem differently and you just might find the solution.
(Quote by me 15th July 2009)

Re: Multiple child models in one form - Marsvin?

jamesw, have you implemented a remove_address_link button?

I'm interested in writing marsvin tutorial for jQuery.