Re: Multiple child models in a dynamic form

Hi, thanks for the tutorial!

I do have a question though, How would you do it to, for example, sum fields in an estimate, I'm guessing with observe_field, but I not sure how to implement it because of the ids of each child.
Example, you have an estimate and this has many products (product name, unit cost, quantity, etc), that you can add dynamically to the estimate, just like your tutorial.
So, my question is how would you do it to show the total(rjs?) of each product ( product_unit_cost x product_quantity = product_total) in the same row(just view)
and also how to show the estimate's total ( product_1_total + product_2_total + ...).

Other question, when the user save the estimate, sometimes they left empty products(child) but they are saved in the db anyway, how can I do it to not save the empty childs?.

Thanks in advance.

Last edited by johnT (2009-05-12 20:01:45)

Re: Multiple child models in a dynamic form

solved the problem with saving empty records.

add this

 :reject_if => proc { |a| a['name_of_column_in_child_model'].blank?

at the end of "accepts_nested_attributes_for" for the child model.

Last edited by johnT (2009-05-12 21:21:17)

Re: Multiple child models in a dynamic form

johnT wrote:

So, my question is how would you do it to show the total(rjs?) of each product ( product_unit_cost x product_quantity = product_total) in the same row(just view)
and also how to show the estimate's total ( product_1_total + product_2_total + ...).

This is actually a little complicated, and not too different from the post before yours. The problem is that, when adding an item the server doesn't get called, it is all done locally with Javascript. You could set up a complex javascript which counts all displayed records every time you add/remove a product but that also means you have to put the individual product price somewhere in the DOM, which you might not know if you're giving the option to add blank lines.

When it comes to "shopping cart" type solutions like this I prefer using Ajax. Simply call the server for each product that you add and use rails to calculate the items in your cart. Then you can use a simple rjs script to update the list. In the end, if your display uses calculations or other complicated stuff, this is probably a better idea.

johnT wrote:

Other question, when the user save the estimate, sometimes they left empty products(child) but they are saved in the db anyway, how can I do it to not save the empty childs?.

Good to see you solved this one already! wink

Re: Multiple child models in a dynamic form

Because I don't know much of rjs I ended up doing this(probably the worst thing to do) but it works for now while I learn ajax:

-first, I created a fake column in the items table, this because I didn't know how to save a record that is in a disabled text_field into the db. (If someone knows how to do it please tell me)
-second, I set the real unit_total field as a hidden_field.

<%= form.text_field :fakeunittotal, :disabled => 'disabled', :size => 14 %>
<%= form.hidden_field :unittotal, :size => 10 -%>

-third, at the end of the child partial I scanned the prefix for each field, because each item has a different ID, in my case 'estimate_items_attributes_0_unitcost', 'estimate_items_attributes_1_unitcost'....

<% prefix = form.object_name.gsub(/(\]\[)/, ']').gsub(/\[/,'_').gsub(/]/,'_') -%>

- Then I set a field_observer for each of the fields that need to be calculated.

<%= observe_field "#{prefix}unitcost", :frequency => 0.50, :function => "report_total('#{prefix}unitcost', '#{prefix}qty', '#{prefix}unittotal', '#{prefix}fakeunittotal');" %>

<%= observe_field "#{prefix}qty", :frequency => 0.50, :function => "report_total('#{prefix}unitcost', '#{prefix}qty', '#{prefix}unittotal', '#{prefix}fakeunittotal');" %>


- Then I create the js function in my application.js to handle the calculation and show it in the disabled text_field, but also put it in the hidden_field of each item to be saved in the db.

[code=js]#in application.js
function report_total(value1, value2, value3, value4) {
    s = (document.getElementById(value1).value -0) * (document.getElementById(value2).value -0);
    document.getElementById(value3).value = s;
    document.getElementById(value4).value = s;
   
}[/code]
Now, I need to know how to calculate the sum of all items, I guess I nead ajax, but I'm not quite sure how to do it. If someone knows, please tell me.

Re: Multiple child models in a dynamic form

The tutorial is brilliant...

But I'm having problems using this as I move to a non-scaffolded project. I think there's something blindingly obvious to a real Rails programmer that I've missed out or forgotten to do. Signatures of the problem? When I use "form_for" the model at the root, I don't get an "ID" in the generated  form or the response hash and I have to manually add a "url => {:action => 'update' }" because "(model)_path" is undefined. That missing ID means that every edited model instance is doubled. Amazingly, the *children"/"tasks" equivalent *do* get IDs and get updated rather than duplicated.

I couldn't see the "(model)_path" defined in any scaffolding. But something in a scaffolded project makes it spring into existence... What have I missed? My guess is that I need to define some method name to make it work.

I'm using ruby 1.8.7 patch level 72 on Mac. Rails 2.3.2. gem 1.3.3. I can run the github/alloy/complex-model-examples code, so I'm pretty sure that my base installation is OK.


--

SOLVED:

I needed to change config/routes.rb. I had explicit map.connect routes for new, create, edit, etc.

I needed to have an additional map.(model) route and now the path helpers work.

That wasn't really obvious for someone justing moving from scaffolding to "real" Rails. smile

Thanks marsvin - file name corrected, and your additional explanation is helpful!

Last edited by JezC (2009-05-21 11:03:25)

Re: Multiple child models in a dynamic form

johnT wrote:

<%= form.text_field :fakeunittotal, :disabled => 'disabled', :size => 14 %>
<%= form.hidden_field :unittotal, :size => 10 -%>

Now, I need to know how to calculate the sum of all items, I guess I nead ajax, but I'm not quite sure how to do it. If someone knows, please tell me.

If you've got a series of hidden fields with the unit costs/totals like this it's quite easy using Prototype to "sum" them. First give all the unit total fields a class so you can refer to them:

<%= form.text_field :fakeunittotal, :disabled => 'disabled', :size => 14, :class => 'unittotal' %>

You can use the prototype $$ method to grab every element with a certain css selector. In this case we'll use the class, like this:
$$('.unittotal')

This will give you an array of all unittotal elements. Then you can use Prototype's each to loop through them and calculate the total:
total = 0
$$('.unittotal').each( function(item) {
  total += item.value;
} );
$('total_sum').value = total;

This example assumes you have a text field with id "total_sum" that can hold the result price, you can use a div or whatever too if you prefer. If you do you will have to use .innerHTML instead of .value.

All that said, I personally would still sooner do this through ajax/rjs simply because it gives the end user less of a chance to mess with your inputs. I'd especially guard against letting the form determine price information that is saved to the database, as forms can be manipulated by a savvy user or even circumvented completely.

Re: Multiple child models in a dynamic form

JezC wrote:

SOLVED - at least partially.

I needed to change resources.rb. I had explicit map.connect routes for new, create, edit, etc. I needed to have an additional map.page route and now the path helpers work. That wasn't really obvious for someone justing moving from scaffolding to "real" Rails. smile

Did you mean routes.rb? The (object)_path you mention is indeed defined there, and if you're not using the correct syntax may not be available. The traditional way to define a route in rails is as follows:

map.connect '/:controller/:action/:id'

This simply connects a potential path to a controller and action without giving you any additional url helper.

Rails' default url helpers (e.g. (object)_path or edit_(object)_path) are automatically generated when you add a resource to your routes. A resource is another name for a controller and model that operate in a predictable (RESTful) way. For instance, say I have a blogs controller and model, I can define the route like this:

map.resources :blogs

This should immediately give me the blogs_path, edit_blog_path and new_blog_path url helpers.

Re: Multiple child models in a dynamic form

Thanks for the tutorial. This has been incredibly helpful!

Marketing for Mavens: Simplifying Web Marketing Management for Small Businesses.

Re: Multiple child models in a dynamic form

Thanks for this great tutorial... It works fine!

But I've one question: How can I add some scriptaculous effects like Effect.BlindDown or something like that?
My thoughts are to add this code to the Helper module, but how is the correct code?

Re: Multiple child models in a dynamic form

This was fantastic! One hiccup that I encountered (due to my ignorance of how the various components work together) was how the name of the partial is used, and how changing the name of the partial will break the code (w/out making a minor change).

Here is the handoff:

# views/projects/new.html.erb
<%= render :partial => 'form', :object => f -%>
#
# views/projects/_form.html.erb
  <%= form.error_messages %>

  <p>
    <%= form.label :title %><br />
    <%= form.text_field :title %>
  </p>
# ...


My hiccup came when I was innocently porting this over to my own project and changed the name of the partial, leading to errors like:

ActionView::TemplateError (undefined method `error_messages'...
or
ActionView::TemplateError (wrong number of arguments (0 for 1)...

To deal with this, I added the following to my partial calls in "New" and "Edit"
<%= render :partial => "add_or_edit_customer", :object => f, :locals => { :form => f }-%>

The locals hash seems to do the job of syncing up the partial with everything else, and so far I've got no issues (and can rename my partials to my heart's content). I figured I would post for anyone else who might be facing a similar situation, though I suspect my newness to RoR is a major factor smile. I'm 99% certain that I'm missing something super-clever, so feel free and share any insights you might have. I'm next going to be digging into extending this across a shared-table inheritance set of models, and we'll see how DRY we can keep things.

Thanks again!

Re: Multiple child models in a dynamic form

Hi Victor,

You're right, render :partial call does apply some Rails magic to the local variables. When you use the :locals hash, as you found out already, you can define local variables for the view. However there is a "special" local variable which you can assign through :object. This one will automatically assume the name of the partial itself.

Some examples:

# Render _project.html.erb and send it a local variable "project" which holds the value of @project:
<%= render :partial => 'project', :object => @project %>

# Render _project_tasks.html.erb and send it a local variable "project" which holds the value of @project
<%= render :partial => 'project_tasks', :locals => { :project => @project } %>

# Render _project_tasks.html.erb and send it a local array called "project_tasks"
<%= render :partial => 'project_tasks', :object => @project.tasks %>


And there's an even more magical way to handle the first case, in which we're sending a Project object to the project partial. When object and partial share the same name you can do this:

<%= render :partial => @project %>

This will assume you want the partial named _project.html.erb and pass it the local variable project (containing @project), so it has the same result as the top example above.


@ST-303:

Apologies for the late reply, I missed your question when you first posted it. Yes you can add visual effects to the helper, but it's slightly more complicated than normal because, similar to the record ids, we'll need to find a way to address the new partial which has an unpredictable (client-generated) unique ID. Here's one way to do it..

def add_task_link(form_builder)
  link_to_function 'add a task' do |page|
    form_builder.fields_for :tasks, Task.new, :child_index => 'NEW_RECORD' do |f|
      html = render(:partial => 'task', :locals => { :form => f })
      page << "$('tasks').insert({ bottom: '#{escape_javascript(html)}'.replace(/NEW_RECORD/g, new Date().getTime()) });"
      # Up to here the code is the same, the code below needs to be added to the helper

      # First let's find the new div. We'll look for all ".task" class divs and select the lastest one:
      page << "new_task_element = $$('div.task').last();"
      # Then we simply apply the Scriptaculous Effect to that element:
      page << "Effect.SlideDown( new_task_element );"
    end
  end
end


You just have to be careful there are no other elements with class "task" in your view. Another thing to keep in mind is that a lot of "appear" effects don't work properly if you don't hide the div by default, so in the _task partial we'll have to add style="display: hidden;" as follows:

# app/views/projects/_task.html.erb
<div class="task" style="display: none;">
  <p>
    <%= form.label :name %>
    <%= form.text_field :name, :size => 15 %>
    <%= remove_task_link( form ) %>
  </p>
</div>

Unfortunately putting this in css won't work, it needs to be hard-coded in the html.

Re: Multiple child models in a dynamic form

Very interresting tutorial, thanks!

However, do you have an idea how I could add a position input to each task (to have items ordered)?

I think I should save the ids generated in a global javascript array to retrieve previous/next item easily, but I don't get how do to it from the helper. Maybe someone can thing of something better smile

Re: Multiple child models in a dynamic form

Dagnan, does that mean you want to be able to insert items in the middle? Or is it alright to have new items appear at the bottom first? If the latter is ok, it's actually not terribly hard probably.

First I would make the tasks (items) act_as_list, and give them an extra position integer column. Next I'd put the tasks in the form in a list (UL) tag and add a hidden field called "position" to the _task form partial. Now you can make the UL a sortable_element and use the onChange action to loop through the tasks ($$('.task') for instance) and assign them a number based on their order in the dom, replacing the value of the hidden position field.

Then when you submit the position should be saved to the database automatically.

Last edited by marsvin (2009-06-29 11:56:50)

Re: Multiple child models in a dynamic form

Actually I've managed to realise it today.

I didn't implement the act_as_list plugin however :s
I use two buttons near each item to move up and move down the item... A bit long, because I had to do all the event handlers and verifications in jQuery, but it works.

I still have a problem though. When the form is submit, or when I want to edit a project, tasks are not in the 'position' order, but as they were entered in the DB.

How can I order them via fields_for and the position field?

Thanks a lot for your help!

edit:
I tried to add

:child_index => :position

And... it works!

Is it the right way to do it?

All the code:

<% form.fields_for :tasks, :child_index => :position do |tasks_form| %>
<% end %>

Last edited by Dagnan (2009-06-29 13:53:22)

Re: Multiple child models in a dynamic form

My motto tends to be: if it works, it works wink That said I wasn't aware the child_index affects sorting at all, and it might mess up your actual resource index numbers. Unfortunately I don't have time to try it out right now, but you may want to check your database stays consistent if you submit like this.

I think the "proper" way to do it is to add an order option to the parent object's association, then the child objects should sort on their own. So in this case in the project model:

  has_many :tasks, :order => :position

Re: Multiple child models in a dynamic form

OK. Thank you again for your reply.

Re: Multiple child models in a dynamic form

Hello, great tutorial.

I've been tinkering with this but what ever I do I can't seem to get the javascript link working properly. Everything up to that point works perfectly.

The new text fields of the new atritbute never shows up. ( I know "attribute" but it's all over my code now I'll fix it tomorrow. ) I'm guessing the "task" in the example code refers to the div and not an instance of Task. Am I correct in this?

Here is the helper module I've got.

module ConfItemHelper
def add_atribute_link(form_builder)
  link_to_function 'add atribute' do |page|
    form_builder.fields_for :conf_item_atributes,
#                              ConfItemAtribute.new do |f|
                                ConfItemAtribute.new,
                              :child_index => 'NEW_RECORD' do |f|
        html = render(:partial => 'atribute', :locals => { :form => f })
        page << "$('atribute').insert({ bottom: #{escape_javascript(html)}.replace(/NEW_RECORD/g, new Date().getTime())} );"
#       page.insert_html :bottom, :atributes, :partial => 'atribute', :locals => { :form => f }
     end
   end
end
end

Thanks

Auke

Last edited by amrypma (2009-07-08 11:47:25)

Re: Multiple child models in a dynamic form

amrypma wrote:

I've been tinkering with this but what ever I do I can't seem to get the javascript link working properly. Everything up to that point works perfectly.

The new text fields of the new atritbute never shows up. ( I know "attribute" but it's all over my code now I'll fix it tomorrow. ) I'm guessing the "task" in the example code refers to the div and not an instance of Task. Am I correct in this?

Hi Auke,

You're right! $('tasks') refers to the container div (<div id='tasks'>) in the new view (new.html.erb)

...

I just spent 10 minutes writing about how it all looks good and then I finally noticed it. I think the problem is with this line:

page << "$('atribute').insert({ bottom: #{escape_javascript(html)}.replace(/NEW_RECORD/g, new Date().getTime())} );"

I think this $('atribute') needs to be plural, so $('atributes'), same as in the old page.insert_html line under it that you commented out.

By the way, you might already have it but if you're using javascript/ajax for your apps, the Firebug addon for Firefox is a god sent. It has a javascript console that actually tells you what's going wrong instead of some obtuse "script error" message like IE has wink

Re: Multiple child models in a dynamic form

Thanks for the quick reply marsvin,

Firebug is a great help. I just slapped myself for not installing it sooner. I'm using ubuntu Jaunty btw so IE won't be in the picture.

It's not the atribute(s) insertion I think. I've tried it and it not working. I've moved the link inside the atributes div and that also didn't work.

There is a difference in the java script code. The static code uses a lot more escaped characters. i.e. \u003C/div\u003E for </div> the character encoding in in UTF-8.

I've also noticed the original link start like this.

<a onclick="try { Element.insert("atributes", { bottom: "\u003Cdiv class=\"atribute\"\u003E\n

As opposed to what the dynamic one does.
<a onclick="try { $('atributes').insert({ bottom: <div class=\"atribute\">\n

Last edited by amrypma (2009-07-09 08:24:17)

Re: Multiple child models in a dynamic form

SOLVED!
I've got it working! Here is the magic line of code.

page << "$('atributes').insert({ bottom: \"#{escape_javascript(html)}\".replace(/NEW_RECORD/g, new Date().getTime())} );"

I've put extra \" marks around the escape_javascript(). No clue yet as to how this precisely works.

Last edited by amrypma (2009-07-09 09:25:18)