Search code examples
ruby-on-railsformscocoon-gem

Cocoon gem, deep association not built when edit or error raised


I'm using the Cocoon gem to build a nested form with a field_for containing another field_for. The hierarchy looks like this: -Letter form -Card field_for -Button field_for

I have a link_to_add_associationin order to dynamically add cards with their buttons. This part looks like this:

<div class="connected-carousels">
                  <div class="stage">
                      <div class="carousel carousel-stage">
                          <ul id="carousel-stage-ul">
                            <%= f.fields_for :cards do |card_fields| %>
                              <% if @blabla %>
                                <%= render 'card_fields', f: card_fields %>
                              <% end %>
                            <% end %>
                            <% end %>
                          </ul>
                        </div>
                        <a href="#" class="prev prev-stage"><span>&lsaquo;</span></a>
                        <a href="#" class="next next-stage"><span>&rsaquo;</span></a>
                    </div>
              <div class="navigation">
                  <a href="#" class="prev prev-navigation">&lsaquo;</a>
                  <a href="#" class="next next-navigation">&rsaquo;</a>
                  <div class="carousel carousel-navigation">
                    <ul id="carousel-navigation-ul"></ul>
                  </div>
              </div>
              <div>
                <%= link_to_add_association '+ Add Card', f, :cards, id: 'add-card-button-bis', data: { association_insertion_node: '#carousel-stage-ul', association_insertion_method: :append } %>
              </div>
            </div>

_card_fields.html.erb partial rendering the buttons:

<% f.object.buttons.build %>
  <%= f.fields_for :buttons do |button_card_fields| %>
      <%= render 'button_fields', f: button_card_fields %>
  <% end %>

_button_fields.html.erb partial:

<div class="add-button-card-modal">
  <h4>Add New Button</h4>
  <label>Button Text</label>
  <%= f.text_field :button_text, :maxlength => 20, placeholder: "Enter the text to display on the button..." %>
  <br><br>
  <label>Button URL</label>
  <%= f.text_field :button_url, placeholder: "Paste URL..." %>
  <div class="nav-popups-buttons">
    <button type="button" id="validate_new_card_button" class="small-cta2">Add Button</button>
    <p class="remove-link" id="delete_new_card_button">Remove Button</p>
  </div>
</div>

Letter model:

class Letter < ApplicationRecord
  validates :campaign_name, :presence => true

  belongs_to :core_bot
  has_many :messages, dependent: :destroy
  has_many :cards, dependent: :destroy
  has_many :filters, dependent: :destroy
  has_many :analytic_deliveries, dependent: :destroy
  has_many :analytic_reads, dependent: :destroy
  has_many :analytic_sends, dependent: :destroy


  accepts_nested_attributes_for :filters, allow_destroy: true, :reject_if => :all_blank
  accepts_nested_attributes_for :messages, allow_destroy: true, :reject_if => :all_blank
  accepts_nested_attributes_for :cards, allow_destroy: true, :reject_if => :all_blank
end

Card model:

class Card < ApplicationRecord
  validates :remote_image_url, :format => URI::regexp(%w(http https)), presence: { message: '%{value} : please enter valid url' }, :allow_blank => true
  validates :title, :subtitle, :presence => true

  belongs_to :letter, optional: true
  has_many :buttons, dependent: :destroy

  accepts_nested_attributes_for :buttons, :reject_if => Proc.new { |att| att[:button_text].blank? && att[:button_url].blank? }, allow_destroy: true
end

Button model:

class Button < ApplicationRecord
  validates :button_url, :format => URI::regexp(%w(http https)), presence: { message: '%{value} : please enter valid url' },
    unless: Proc.new { |a| a.button_url.blank? }
  validates :button_text, :presence => true, unless: Proc.new { |a| a.button_url.blank? }

  belongs_to :message, optional: true
  belongs_to :card, optional: true
  has_one :short_url, dependent: :destroy
end

Create action in the letter controller in case of error raised:

@letter.filters.build

        if params[:letter]['cards_attributes'].present? == false
          @letter.cards.build.buttons.build
        end

        format.html { render :new }
        format.json { render json: @letter.errors, status: :unprocessable_entity }

The problem is that buttons are not built when the create action raises an error or when I edit the record. If I add @letter.cards.build.buttons.build into the controller, it adds a new card but buttons inputs still don't appear.

UPDATE In fact buttons are built. The only problem is my custom jquery I am using to display the popup showing the buttons fields are not working since there were created in the cocoon:after-insert callback.

When an error is raised or on the edit view, the cards I created are rendered instead of added by link_to_add_association. This the cocoon:after-insert callback is not called...

Any idea how I could use the same logic of link_to_add_association calling the after-insert callback on rendered cards fields?

Here is the console test, my buttons are here: Console Test


Solution

  • Ok, so normally there's a couple issues buried in one of these problems. This answer is only to trouble shoot why the Edit view always fails to load.

    My initial guess is in the controller, two problems ... the if params & the @letter.cards.build.buttons.build is the problem & instead of trying to design around it - you need to change it, accept whatever the error is & then fix that error in the models. The first fix is get the conditional if params out of there.

    I also noticed there's no accept_nested_attributes_for :buttons on your letter model.

    Lastly, before we begin trouble shooting - I forgot to ask for your strong_params section - please post the entire controller action for create & the strong_params method in the private section of the controller.


    Since it's not loading at all. Ever. It's probably not a totally a jquery issue. We should see a rails generated collection of all the cards & buttons belonging via the model.

    I'm guessing so the .build method is the first issue ...

    I imagine if you examine the rails console ... rails c ...

    First, try calling @test = Letter.first, then explore it's values when it sets ... you should be able to see other ID's in there & then be able to call them with @test.{whatever} .. ie: @test.cards which will give a collection of all the cards that exist via the relationship in the models.

    Second, if the first fails, @test.cards.create!(name: "test") to see if the relationship creates the entry correctly. I'd be happy if anything shows up in @letter.cards.buttons in console, then we trouble shoot the other branches (letter.filters) of your hierarchy later.

    Third, we move generating test data via console to ensure those values exist to be fed thru the rails controller & to the view to be displayed.

    Fourth, we check the jquery isn't interfering with them being displayed.


    Demo code

    Here's the 3 deep nesting ...

    Note all of this worked & took no effort for the EDIT, the setup for new/create in the _form.html.erb just automatically worked for editing.

    Models

    country.rb
    
    class Country < ApplicationRecord
      has_many  :states
      has_many  :counties, through: :states
    
      accepts_nested_attributes_for :states, reject_if: proc { |attributes| attributes[:name].blank? }, allow_destroy: true
      accepts_nested_attributes_for :counties, reject_if: proc { |attributes| attributes[:name].blank? }, allow_destroy: true
    end
    
    state.rb
    class State < ApplicationRecord
      belongs_to  :country
      has_many    :counties
    
      validates :name, presence: true
    
      # can't recall if this is needed 
      accepts_nested_attributes_for :counties, reject_if: proc { |attributes| attributes[:name].blank? }, allow_destroy: true
    end
    
    county.rb
    class County < ApplicationRecord
      belongs_to :state
      validates :name
    end
    

    countries_controller.rb ... Controller (you don't even need to generate a controller for others)

    class CountriesController < ApplicationController
      before_action :set_country, only: [:show, :edit, :update, :destroy]
    
      def index
        @countries = Country.paginate(page: params[:page], per_page: 10)
      end
    
      def show
      end
    
      def new
        @country = Country.new
      end
    
      def edit
        # @partial_choice = params[:partial_choice]
      end
    
      def create
        @country = Country.new(country_params)
    
        respond_to do |format|
          if @country.save
            format.html { redirect_to @country, notice: 'Country was successfully created.' }
            format.json { render :show, status: :created, location: @country }
          else
            format.html { render :new }
            format.json { render json: @country.errors, status: :unprocessable_entity }
          end
        end
      end
    
      def update
        respond_to do |format|
          if @country.update!(country_params)
            format.html { redirect_to @country, notice: 'Country was successfully updated.' }
            format.json { render :show, status: :ok, location: @country }
          else
            format.html { render :edit }
            format.json { render json: @country.errors, status: :unprocessable_entity }
          end
        end
      end
    
    
      def destroy
        @country.destroy
        respond_to do |format|
          format.html { redirect_to countries_url, notice: 'Country was successfully destroyed.' }
          format.json { head :no_content }
        end
      end
    
    private
      # Use callbacks to share common setup or constraints between actions.
      def set_country
        @country = Country.find(params[:id])
      end
    
    
     def country_params
      params.require(:country).permit(:id, :name, :description, :size, :player_id,
         countryneighbor_attritubtes: [:id, :bordercountry_id, :country_id, :_destroy],
         states_attributes: [:id, :name, :description, :country_id, :_destroy,
         counties_attributes: [:id, :name, :description, :state_id, :_destroy]])
      end
    end
    

    Views - if you've already tested the data & relationships, strong_params in rails console as well as checked server logs in log/development.log, then maybe compare the HAML style calls in the views here ...

    /basicB/app/views/countries/_form.html.haml

    .fieldset.form-inline
      = simple_form_for @country do |f|
        = f.error_notification
        .countries
          = f.input :name, input_html: {:placeholder => "...insert country name..."}
          = f.input :description
        .row-fuild
          %a.btn.btn-primary{"aria-controls" => "neighborsDisplay", "aria-expanded" => "false", "data-toggle" => "collapse", :href => "#neighborsDisplay"} Show Neighbors
          #neighborsDisplay.collapse
            .list-group
              - for neighbor in @country.neighbors
                = render partial: 'country_neighbors', locals: {neighbor: neighbor}
          .states
          = f.simple_fields_for :states do |state|
            = render 'state_fields', :f => state
        .links.row
          = link_to_add_association 'Add State', f, :states,  render_options: { wrapper: 'inline_form' }, :class => "btn btn-default"
        .form-actions.row
          = f.button :submit
    

    /app/views/countries/_state_fields.html.haml

    .nested-fields.list-group-item
        .well
            %h4 State
            .form-inline.text
                = f.input :name, input_html: {:placeholder => "...insert State Name ..."} 
                = f.input :description
                = link_to_remove_association f, class: 'btn btn-default btn-xs' do
                    .glyphicon.glyphicon-remove
            .counties
            = f.simple_fields_for :counties do |state|
                = render 'county_fields', :f => state
        .links.row
            = link_to_add_association 'Add County', f, :counties,  render_options: { wrapper: 'inline_form' }, :class => "btn btn-default"    
    

    /app/views/countries/_county_fields.html.haml

    .nested-fields.list-group-item.my-well
      .row.text
        = f.input :name, input_html: {:placeholder => "... County Name ..."}
        = f.input :description
        = link_to_remove_association f, class: 'btn btn-default btn-xs' do
          .glyphicon.glyphicon-remove