24

We've all seen the brilliant complex forms railscast where Ryan Bates explains how to dynamically add or remove nested objects within the parent object form using Javascript.

Has anyone got any ideas about how these methods need to be modified so as to work with Haml Formtastic?

To add some context here's a simplified version of the problem I'm currently facing:

# Teacher form (which has nested subject forms) [from my application]

- semantic_form_for(@teacher) do |form|
  - form.inputs do
    = form.input :first_name
    = form.input :surname
    = form.input :city
    = render 'subject_fields', :form => form 
    = link_to_add_fields "Add Subject", form, :subjects   

# Individual Subject form partial [from my application]

- form.fields_for :subjects do |ff| 
  #subject_field
    = ff.input :name
    = ff.input :exam
    = ff.input :level
    = ff.hidden_field :_destroy
    = link_to_remove_fields "Remove Subject", ff 

# Application Helper (straight from Railscasts)

  def link_to_remove_fields(name, f)
    f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)")
  end

  def link_to_add_fields(name, f, association)
    new_object = f.object.class.reflect_on_association(association).klass.new
    fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
      render(association.to_s.singularize + "_fields", :f => builder)
    end
    link_to_function(name, h("add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}  \")"))
  end

#Application.js (straight from Railscasts)

  function remove_fields(link) {
  $(link).previous("input[type=hidden]").value = "1";
  $(link).up(".fields").hide();
  }

function add_fields(link, association, content) {
  var new_id = new Date().getTime();
  var regexp = new RegExp("new_" + association, "g")
  $(link).up().insert({
    before: content.replace(regexp, new_id)
  });
  }

The problem with implementation seems to be with the javascript methods - the DOM tree of a Formtastic form differs greatly from a regular rails form.

I've seen this question asked online a few times but haven't come across an answer yet - now you know that help will be appreciated by more than just me!

Jack

Jack Kinsella
  • 4,098
  • 2
  • 34
  • 52

4 Answers4

27

You're on the right track:

...the DOM tree of a Formtastic form differs greatly from a regular rails form.

To adapt Ryan's example for formtastic, it's helpful to be reminded that the semantic_fields_for helper is similar to the semantic_form_for helper, which outputs the inputs in list form.

To keep things as close to the Railscast code as possible, you'll need to:

  • enclose the collection of nested fields in a wrapper (I use a div with the subjects CSS ID.)
  • enclose the nested fields in a ul/ol wrapper (I applied a nested-fields CSS class.)

Here's what your files should like.

Teacher form (with nested Subject fields):

- semantic_form_for(@teacher) do |form|
  - form.inputs do
    = form.input :first_name
    = form.input :surname
    = form.input :city

    %h2 Subjects
    #subjects
      - form.semantic_fields_for :subjects do |builder|
        = render :partial => "subject_fields", :locals => { :f => builder }
      .links
        = link_to_add_fields "Add Subject", form, :subjects

Subject fields partial (for nested Subject):

%ul.nested-fields
  = f.input :name
  = f.input :exam
  = f.input :level
  = link_to_remove_fields "Remove Subject", f

ApplicationHelper:

def link_to_remove_fields(name, f)
  f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)")
end

def link_to_add_fields(name, f, association)
  new_object = f.object.class.reflect_on_association(association).klass.new
  fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
    render(association.to_s.singularize + "_fields", :f => builder)
  end
  link_to_function(name, h("add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")"))
end

Application.js:

function remove_fields(link) {
  $(link).previous("input[type=hidden]").value = "1";
  $(link).up(".nested-fields").hide();
}

function add_fields(link, association, content) {
  var new_id = new Date().getTime();
  var regexp = new RegExp("new_" + association, "g")
  $(link).up().insert({
    before: content.replace(regexp, new_id)
  });
}

Using following gems:

  • formtastic (0.9.10)
  • haml (3.0.2)
  • gherkin (1.0.26)
  • rails (2.3.5)
Bennett
  • 336
  • 4
  • 5
  • Thanks for your solution -- so the subject fields are displaying as are the buttons but I get scrolled to the top of the screen with no action when I click the either link (I have js defaults on in case you were wondering). I tried Steve's solution and with that the remove link works but unfortunately not the add button. It may be a case that my markup could be a little different -- using Rails 3 and latest Formtastic. Any ideas? – Jack Kinsella Sep 30 '10 at 19:52
  • I've only tried this setup with the gems that I listed above. – Bennett Oct 16 '10 at 19:34
  • Also, it doesn't sound like you're following my example exactly so you might have your form structured differently. I recommend that you get the link_to_add_fields and add_fields helper working first before moving on to removing fields. – Bennett Oct 16 '10 at 19:38
3

application.js for jQuery:

function remove_fields(link) {
  $(link).prev("input[type=hidden]").val("1");
  $(link).parent(".nested-fields").hide();
}

function add_fields(link, association, content) {
  var new_id = new Date().getTime();
  var regexp = new RegExp("new_" + association, "g")
  $(link).parent().before(content.replace(regexp, new_id));
}
Steve
  • 815
  • 9
  • 14
1

In Rails 3, there's no need to use the h() function in the helper. Just omit and it should work.

Nolsto
  • 180
  • 6
1

I've been beating my head against this one on and off for years, and I just today came up with something I'm proud of - elegant, unobtrusive, and you can do it only two or three (really long) lines of code.

ParentFoo has_many NestedBars, with appropriate accepts_nested_parameters and all those goodies. You render a set of fields for a nested_bar into a data-add-nested attribute on the link, using the :child_index to set a string that you can replace later. You pass that string in a second parameter, data-add-nested-replace

parent_foos/_form.html.haml

- semantic_form_for @parent_foo do |f|
  -# render existing records
  - f.semantic_fields_for :nested_bars do |i|
    = render 'nested_bar_fields', :f => i

  # magic!
  - reuse_fields = f.semantic_fields_for :nested_bars, @parent_foo.nested_bars.build, :child_index => 'new_nested_bar_fields' do |n| render('nested_bar_fields', :f => n) end
  = link_to 'Add nested bar', '#', 'data-add-nested' => reuse_fields, 'data-add-nested-replace' => 'new_nested_bar_fields'

the nested fields partial is nothing fancy

parent_foos/_nested_bar_fields.html.haml

- f.inputs do
  = f.input :name
  = f.input :_delete, :as => :boolean

In your jQuery you bind the click of elements with a data-add-nested field (I've used live() here so ajax-loaded forms will work) to insert the fields into the DOM, replacing the string with a new ID. Here I'm doing the simple thing of inserting the new fields before() the link, but you could also provide an add-nested-replace-target attribute on the link saying where in the DOM you want the new fields to wind up.

application.js

$('a[data-add-nested]').live('click', function(){
  var regexp = new RegExp($(this).data('add-nested-replace'), "g")
  $($(this).data('add-nested').replace(regexp, (new Date).getTime())).insertBefore($(this))
})

You could put this in a helper, of course; I'm presenting it here directly in the view so the principle is clear without mucking about in metaprogramming. If you're worried about names colliding, you could generate a unique ID in that helper and pass it in nested-replace.

getting fancy with the delete behaviour is left as an exercise for the reader.

(shown for Rails 2 because that's the site I'm working on today - almost the same in Rails 3)

stephan.com
  • 1,420
  • 15
  • 24
  • note: it may not be that easy in Rails 3, thanks to html escaping :( – stephan.com Feb 01 '11 at 03:53
  • using escape_once seems to work, as in this semi-related example with inserting a single field: - new_field = escape_once f.input(:related_thing) = link_to 'Add another', '#', 'data-insert' => new_field – stephan.com Feb 01 '11 at 04:44
  • 1
    I've developed this technique a little further for use without a model - I had a case where a client wanted to specify sets of fields to show in a report. This version uses a block instead of a separate template for the fields, and uses a 'generic model' to generate the replaceable parameter. Here's the gist: https://gist.github.com/1554795 – stephan.com Jan 04 '12 at 01:42