More UJS fun with Accordion Content

I’ve already covered loading Bootstrap modals directly from the server on Rails with UJS in this blog post: Discovering UJS with AJAX. Now I’d like to continue from there and demonstrate Bootstrap’s accordion feature with content loaded directly from the server. We’ll use a personal profile example where you click on an expansion link to load either a full profile view, an edit profile view, or just a summary.

First we need create routes for this resource. In config/routes.rb we’ll do as follows.

config/routes.rb scope :ujs, defaults: { format: :ujs } do patch 'profiles' => 'ujs#profiles' end 1 2 3 4 scope : ujs , defaults : { format : : ujs } do patch 'profiles' = > 'ujs#profiles' end

Next, in our controller, we’ll need to add a matching method for the route. We’ll also need to add a way to handle parameters and content.

app/controllers/ujs_controller.rb def profiles par = lambda { params.permit(:id, :type, :target, :accordion, :format) } @target = par.call[:target] @profile = current_user.profiles.where(id: par.call[:id]).first @partial = case par.call[:type].to_sym when :snippet 'profiles/profile_snippet' when :view 'profiles/profile_view' when :edit 'profiles/form' end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 def profiles par = lambda { params . permit ( : id , : type , : target , : accordion , : format ) } @target = par . call [ : target ] @profile = current_user . profiles . where ( id : par . call [ : id ] ) . first @partial = case par . call [ : type ] . to _ sym when : snippet 'profiles/profile_snippet' when : view 'profiles/profile_view' when : edit 'profiles/form' end end

Here I’ve used a lambda par to work as my strong parameter handler. The profile is guaranteed to only load a valid profile for the current user since it’s scoped on the current_user devise object. And the @partial variable will return a string based on the :type parameter given.

The partials are all files preceded with an underscore in the profiles directory. When specifying a partial string it’s important to not have the underscore in the string even though the file name is preceded with it.

Now in the view you will need to have a target for the HTML response to be injected into. It needs to be uniquely identified on the page for the particular profile that will be loaded into it (each profile to their own section). So we’ll place a div tag underneath the profile description.

<%# SOME PROFILE STUFF HERE %> <div id="collapse<%= profile.id %>" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading<%= profile.id %>"> </div> 1 2 3 4 5 6 7 <% # SOME PROFILE STUFF HERE %> < div id = "collapse <%= profile . id %> " class = "panel-collapse collapse" role = "tabpanel" aria - labelledby = "heading <%= profile . id %> " > < / div >

As a habit I specify, and memoize, a local variable for the view Object at the top of the view like so:

<% profile ||= @profile %> 1 2 <% profile || = @profile %>

When switching content in and out at times it gets hard to track which kind of variable represents the Object for the page. So now I have a uniform way to access my objects in any page.

Next we need links to trigger the UJS controller and call the appropriate content for our profile. Whether we want the summary snippet, the full view, or the form to edit the profile with. The links will be a bit long for specification as we are providing multiple parameters for the resource.

<%= link_to "VIEW", url_for({ controller: 'ujs', action: 'profiles', id: profile.id, type: :view, target: "#collapse#{profile.id}".to_sym }), method: :patch, remote: true %> 1 2 3 4 5 6 7 8 9 10 11 12 <%= link _ to "VIEW" , url_for ( { controller : 'ujs' , action : 'profiles' , id : profile . id , type : : view , target : "#collapse#{profile.id}" . to _ sym } ) , method : : patch , remote : true %>

For our other links we just have to change the link_to text and the type to either :snippet or :edit. The url_for handles the controller#action to be called and the rest are passed as parameters. The method: :patch and remote: true are important to make the UJS feature work properly.

Now; assuming you’ve already built the partials for the summery, view, and edit; all we have left to do is write the UJS view for the ujs#profiles controller method. This has a bit of extra detail to it so I will take the time to explain each part. First you create the file to coincide with the controller: app/views/ujs/profiles.js.erb . And here’s the content for it.

app/views/ujs/profiles.js.erb var target = $('<%= @target %>'); if (target.data('view') == '<%= @partial %>'){ if (target.hasClass('in')){ target.collapse('hide'); } else { target.collapse('show'); } } else { target.data('view', '<%= @partial %>'); var the_html = "<%= j render(partial: @partial, locals: {profile: @profile}) %>"; target.html(the_html); target.collapse('show'); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 var target = $ ( ' <%= @target %> ' ) ; if ( target . data ( 'view' ) == ' <%= @partial %> ' ) { if ( target . hasClass ( 'in' ) ) { target . collapse ( 'hide' ) ; } else { target . collapse ( 'show' ) ; } } else { target . data ( 'view' , ' <%= @partial %> ' ) ; var the_html = " <%= j render ( partial : @partial , locals : { profile : @profile } ) %> " ; target . html ( the_html ) ; target . collapse ( 'show' ) ; }

First we set a jQuery pointer to the target where the HTML is going to be inserted to in the document with var target = $(‘<%= @target %>’); . Now any time we access the jQuery variable target we are using jQuery methods on our actual target destination in the page (DOM).

The first if block is checking a data variable we (will) specify on the target to see if the data variable matches the string (name) of the partial we’ve loaded into it. The reason we do this is that we need to know if we’ve already loaded the specific resource of HTML into the page. Example: you click the VIEW link twice; it only needs to load once since it can see from the data variable that it matches the previously loaded content. The rest of the if block is methods for checking with Bootstrap on whether the accordion object is open/closed and then toggling it.

The next part within the else block will set the data variable to the partial we’re loading, load the HTML into the target, and open the accordion’d content. The j render gets the partial we want and passes the partial the user profile object we want to render. It hands out user profile object onto our partial and returns a valid Javascript string we then inject into the HTML target. Having this in the else block allows us not to hit the server every single time for content that’s already been loaded.

Summary

So now you have dynamically updating content in your Rails app with jQuery UJS and Bootstrap’s accordion. This saves on load time, and makes for a nicer user experience with a more dynamic presentation.

Hope you enjoyed this! Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!

-Daniel P. Clark

Image by Kim Seng via the Creative Commons Attribution-NonCommercial-NoDerivs 2.0 Generic License.