mkrinblk mkrinblk - 3 days ago 5
CoffeeScript Question

Rails: How can I dynamically load from db into a bootstrap accordion only if it opens

I am using the
Awesome Nested Set gem to create a hierarchy now I want to display this hierarchy.

I have an index page with a typical controller action

klasses_controller.rb

def index
@klasses = Klass.where(depth: 0).order('lft ASC')
end


klasses/index.rb

<div id="accordion" role="tablist" aria-multiselectable="true">
<% @klasses.each do |klass| %>
<%= render :partial => "klass", locals: {klass: klass} %>
<% end %>
</div>


in my index action I am calling a partial

klasses/_klass.html.erb

<div class="klass">
<div class="klass-header" role="tab" id="heading-<%=klass.id%>">
<h5 class="mb-0">
<a data-toggle="collapse"
data-parent="#accordion"
href="#collapse-<%=klass.id%>"
aria-expanded="false"
aria-controls="collapse-<%=klass.id%>">

<%= klass.symbol + " : " + klass.title%>
</a>
</h5>
</div>

<div id="collapse-<%=klass.id%>"
class="collapse"
role="tabpanel"
aria-labelledby="heading-<%=klass.id%>">

<div class="klass-block">
<Here is where I want to render partial for each child>
</div>
</div>
</div>


This is standard accordion and is working as is. Here is the problem I have a very big db and I don't want to render anything unnecessarily. I want to wait until the header is clicked and then asynchronously retrieve the children of the class i clicked and render each using the same partial.

How can I achieve this? Is there a way using a route/controller action combination or should I start writing coffee/java-script? I am not trying to reinvent the wheel so if anyone knows of an example that would be welcome.

Answer

This was a tricky problem only because there were a lot of moving parts. It was a real headscratcher but in hindsight it is pretty straight forward.

First I wanted to use a custom controller action so I added a route for my "children" method which is set up to receive an ajax call.

routes.rb

get 'klasses/:id/children', to: 'klasses#children', as: 'children_of'

klasses_controller.rb

def index
    @klasses = Klass.where(depth: 0).order('lft ASC')
end

def children
  respond_to do |format| 
      format.js {render 'children', :klass => @klass }
  end
end

This respond block is expecting to have a children.js.erb to call for.

children.js.erb

$("#collapse-<%=@klass.id%>").html("<%= escape_javascript(render :partial => 'klasses/children',    locals: { :klass => @klass })%>");

This is where we specify the div that we are going to insert the new content into and we call another partial.

_children.html.erb

<div class="child-block">
    <% children = @klass.children %>
    <% children.each do |child| %>
         <%= render :partial => 'klasses/klass', locals: { :klass => child } %>
    <% end %>
</div>

Finally I rewrote my _klass.html.erb partial so It has a link in the heading that will both trigger bootstrap's accordion js and call our new controller action.

_klass.html.erb

<div class="klass">
    <div class="klass-header" role="tab" id="heading-<%=klass.id%>">
      <h5 class="mb-0">

    <%= link_to klass.symbol + " : " + klass.title,
                children_of_path(klass.id), 
                remote: true,
                :data => {  :toggle => "collapse", 
                            :parent => "#accordion",
                            :target => "#collapse-#{klass.id}"

                },
                :aria => {  :expanded => "false", 
                            :controls => "collapse-#{klass.id}"
                }%>
      </h5>
    </div>

    <div id="collapse-<%=klass.id%>" 
        class="collapse" 
        role="tabpanel" 
        aria-labelledby="heading-<%=klass.id%>">

    </div>
</div>

That's pretty much it. Add some padding to the child-block and you have yourself a nice indented Hierarchy that dynamically loads content.

I did have to disable cross site Request forgery for my custom action in my controller

protect_from_forgery :except => :children

If anyone knows a way around that let me know.

Comments