Search code examples
javascriptruby-on-railsrubynested-attributes

Nested_form_fields show existing has_many nested fields upon validation failure


I am using the nested_form_fields gem. I have the following associations to allow a contact to have associations to multiple teams:

#models/contact.rb
class Contact < ActiveRecord::Base
  has_many :contacts_teams
  has_many :teams, through: :contacts

  accepts_nested_attributes_for :contacts_teams, allow_destroy: true
end

#models/contacts_team.rb
class ContactsTeam < ActiveRecord::Base
  belongs_to :contact
  belongs_to :team
end

#models/team.rb
class Team < ActiveRecord::Base
  has_many :contacts_team
  has_many :contacts, through: :contacts_teams
end

A contact always Must have at least one team. I created a custom validation which checks for this when the user is creating a new contact or is updating an existing contact.

#custom validation within models/contact.rb
class Contact < ActiveRecord::Base
  ...
  validate :at_least_one_contacts_team

  private
  def at_least_one_contacts_team
    # when creating a new contact: making sure at least one team exists
    return errors.add :base, "Must have at least one Team" unless contacts_teams.length > 0

    # when updating an existing contact: Making sure that at least one team would exist
    return errors.add :base, "Must have at least one Team" if contacts_teams.reject{|contacts_team| contacts_team._destroy == true}.empty?
  end           
end

It works for the most part. But there is an edge case when updating an existing contact's team. Here I am updating a contact and it shows that they have two existing associated teams:

existing teams

The user clicks the X's next to each team in order to delete them, so they no longer show on the page, and then the user clicks update to put these changes into effect:

clicks the x's next to each contact

associated teams marked for deletion and don't show

submits changes

The validation properly fails because the user attempted to delete all the associated teams. It properly shows the validation error message:

error message

However, the problem is that the form does not redisplay the existing associated teams! Those associations have not been deleted yet, so they should still show:

Not showing existing team associations after validation fails

How do I show existing team associations after validations fail when updating an existing contact?

I attempted removing all the _destroy flags in hopes that by doing so those associated teams will show. Unfortunately it didn't do the trick:

# block run when update validation fails
else
  params[:contact][:contacts_teams_attributes].each do |k,v|
    v.delete_if{|k,v| k == "_destroy" && v == "1"}
  end
  render :edit
end

I am thinking that the page is remembering the nested_form_fields' javascript that was previously run. So it remembers that the teams were marked to be deleted, therefore it is not rendering them. I don't know how to reset the javascript unless I do a redirect, in which case all the validation errors will no longer be shown.

Thanks in advance!


Solution

  • Here is the answer. I changed the name of the custom validation within the Contact model so that the error message would make sense:

    class Contact < ActiveRecord::Base
    
      accepts_nested_attributes_for :contacts_teams, allow_destroy: true
      validate :at_least_one_contacts_team
    
      private
    
      def at_least_one_contacts_team
        return errors.add :A_Team, "must be present" unless contacts_teams.length > 0
        return errors.add :A_Team, "must be present" if contacts_teams.reject{|contacts_team| contacts_team._destroy == true}.empty?
      end
    end
    

    And then within the update action of the ContactsController:

    def update
      authorize @contact
      if @contact.update(contact_params)
        redirect_to(some_path), success: 'Updated Successfully.'
      else
        # This next line is what makes it work as expected for that edge case
        @contact.contacts_teams.reload if @contact.errors.keys.include?(:A_Team)
        render :edit
      end
    end