Marking Layered Nested Forms as Dirty in Rails

I have a three-tier multi-user form in Rails. The setup is this: projects have many steps, and there are many Notes in milestones. The goal is to make everything editable on the page with JavaScript, where we can add several new steps to the project on the page and add new notes to new and existing steps.

Everything works as expected, except that when I add new notes to an existing Milestone (new milestones work fine when adding notes to them), new notes will not be saved if I do not edit any of the fields that really belong Milestone to indicate dirty / edited form.

Is there a way to mark Milestone to keep the new Notes that have been added?

Edit: Sorry, it's hard to paste all the code because there are so many parts, but here goes:

Models

class Project < ActiveRecord::Base
  has_many :notes, :dependent => :destroy
  has_many :milestones, :dependent => :destroy

  accepts_nested_attributes_for :milestones, :allow_destroy => true
  accepts_nested_attributes_for :notes, :allow_destroy => true, :reject_if => proc { |attributes| attributes['content'].blank? }
end

class Milestone < ActiveRecord::Base
  belongs_to :project
  has_many :notes, :dependent => :destroy

  accepts_nested_attributes_for :notes, :allow_destroy => true, :allow_destroy => true, :reject_if => proc { |attributes| attributes['content'].blank? }
end

class Note < ActiveRecord::Base
  belongs_to :milestone
  belongs_to :project

  scope :newest, lambda { |*args| order('created_at DESC').limit(*args.first || 3) }
end

I use a jQuery-based, unobtrusive version of Ryan Bates command assistant / JS code to do this.

Application assistant

def add_fields_for_association(f, association, partial)
  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(partial, :f => builder)
  end
end

I present the association form in a hidden div, and then use the following JavaScript to find it and add it as needed.

Javascript

function addFields(link, association, content, func) {
    var newID = new Date().getTime();
    var regexp = new RegExp("new_" + association, "g");
    var form = content.replace(regexp, newID);
    var link = $(link).parent().next().before(form).prev();
    if (func) {
        func.call();
    }
    return link;
}

I assume that the only suitable piece of code that I can imagine will be the create method in NotesController:

def create
  respond_with(@note = @owner.notes.create(params[:note])) do |format|
    format.js   { render :json => @owner.notes.newest(3).all.to_json }
    format.html { redirect_to((@milestone ? [@project, @milestone, @note] : [@project, @note]), :notice => 'Note was successfully created.') }
  end
end

The @owner initiator is created as follows before the filter:

def load_milestone
  @milestone = @project.milestones.find(params[:milestone_id]) if params[:milestone_id]
end

def determine_owner
  @owner = load_milestone || @project
end

, , , , , . "", , Rails .

+5
2

# 4242 Rails 2.3.5, Rails 2.3.8.

+2

, . . .

class Project < ActiveRecord::Base
  has_many :milestones, :dependent => :destroy
  has_many :notes, :through => :milestones
  accepts_nested_attr ibutes_for :milestones, :allow_destroy => true
end

class Milestone < ActiveRecord::Base
  belongs_to :project
  has_many :notes, :dependent => :destroy

  accepts_nested_attributes_for :notes, :allow_destroy => true, :reject_if => proc { |attributes| attributes['content'].blank? }
end

class Note < ActiveRecord::Base
  belongs_to :milestone
end

: , :

## project controller

# PUT /projects/1
def update
  @project = Project.find(params[:id])

  if @project.update_attributes(params[:project])
    redirect_to(@project)
  else
    render :action => "edit"
  end
end

# GET /projects/1/edit
def edit
  @project = Project.find(params[:id])
  @project.milestones.build
  for m in @project.milestones
    m.notes.build
  end
  @project.notes.build
end

## edit.html.erb
<% form_for(@project) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <% f.fields_for :notes do |n| %>
      <p>
        <div>
          <%= n.label :content, 'Project Notes:' %>
          <%= n.text_area :content, :rows => 3 %>
        </div>
      </p>
  <% end %>
  <% f.fields_for :milestones do |m| %>
      <p>
        <div>
          <%= m.label :name, 'Milestone:' %>
          <%= m.text_field :name %>
        </div>
      </p>
      <% m.fields_for :notes do |n| %>
          <p>
            <div>
              <%= n.label :content, 'Milestone Notes:' %>
              <%= n.text_area :content, :rows => 3 %>
            </div>
          </p>
      <% end %>
  <% end %>
  <p>
    <%= f.submit 'Update' %>
  </p>
<% end %>
0

All Articles