BLOG
Adding the First Helpers in Ruby on Rails
Like most of the posts in this Ruby on Rails series, were going to be looking at nice small chunks of code and setting some smaller goals. While the general idea is to create a wonderful gem that is amazing, Rome wasn’t built in a day and neither is this gem. Mistakes will be made, compromises left in palace, and eventually, we will come back and smooth out some things.
This isn’t a project where we know the ending so we can skip all the dirty traveling we have to do to get there. This series will include my missteps and refactoring.
Big Ideas for This Project
Having some helpers for bootstrap code isn’t new. Others have done it. This is just one take on the problem. Because of that, there are a few things we want to keep in mind.
- We don’t want to force a use pattern if we don’t need to
- We don’t want to get in the way
- We do want to enforce markup structure when needed
- We do want to make things easier and less verbose.
Adding in Navbar Support
So let’s start at the top. Navbars in bootstrap are a great way to have basic site navigation. Their markup is a bit complex, but it’s also a complex part of the site.
This also gives us an opportunity to make some decisions on a structure. One of the things I wanted to do was to have the same kind of interaction that form_for
has.
<%= form_for @post do |f| %>
<%= f.text_field :title %>
<%= f.text_area :body %>
<%= f.submit %>
<% end %>
The important part that I wanted to emulate was the to do f.text_field
and have the form object (the form builder really) be something that could be passed around.
Let’s start with adding a simple navbar. In the Todo application let’s just have a navbar with the “brand” and a link to add a task, and to view all tasks.
<!-- application.html.erb -->
<%= render partial: 'layouts/main_navigation' %>
I like to put main navigation in it’s own partial, it can get messy, so I do this just to keep the mess clean.
<!-- /app/views/layouts/_main_navigation.html.erb -->
<%= navbar do |nav| %>
<%= nav.brand content: 'Todo' %>
<%= nav.toggler target: 'mainMenu' %>
<%= nav.collapse id: 'mainMenu' do |c| %>
<%= nav.items do |i| %>
<%= i.link 'Tasks', items_path %>
<%= i.link 'New Task', new_item_path %>
<% end %>
<% end %>
<% end %>
This markup is pretty dense, it’s more than what I would want but it does work. More importantly, it gets out of the way if the project it’s in needs it to.
Making this work is a bit interesting. First, we need to “convert” our gem to a rails engine. The smoothest way to do that is just to add some text to a file.
# /lib/bootstrap_helpers.rb
class Engine < ::Rails::Engine
end
Next, I decided to used partials instead of concatenating strings everywhere. If you’re interested in the partials you can take a look at github repo I will just use a few for examples
<!-- /app/views/helpers/bootstrap/navbar/_mobile_collapse.html.erb -->
<div class="collapse navbar-collapse <%= classes %>" id='<%= id %>' style='<%= style %>'>
<%= content %>
</div>
Now for the interesting part, I wanted a helper to kick everything off. navbar
in our example, but I also wanted it to return an object. And I wanted to be able to do links like in the example, but without having to prefix things as some monstrosity.
def navbar(css = [], &block)
raise ArgumentError, 'Missing block' unless block_given?
# we don't want to force a CSS but if nothing is provided then we need something basic to make it work
css << 'navbar-expand-lg navbar-light bg-light' if css.blank?
navbar = BootstrapHelpers::Navbar.new(self)
content = capture(navbar, &block)
bs_render template: 'navbar/navbar', content: content, locals: { classes: css }
end
This gave the object BootstrapHelpers::Navbar
to pass around like the rails form builder, but the rails form builder doesn’t use partials. It concats.
module BootstrapHelpers
class Navbar
include ActionView::Helpers::CaptureHelper
def initialize(view_context)
@ac = view_context
end
Passing in the “view_context” lets me render as needed. For example, in the collapse method
def collapse(options = {}, &block)
id = options[:id] || SecureRandom.uuid
classes = options[:classes]
bs_render template: 'navbar/collapse', locals: { classes: classes, id: id, style: options[:style] }, &block
end
You can see that bs_render is called. That method is
def bs_render(template:, locals: {}, &block)
raise ArgumentError, 'template is not formatted correctly, should be section/fragment' if template.split('/').length != 2
section = template.split('/').first
fragment = template.split('/').last
content = locals[:content]
content = @ac.capture(self, &block) if block_given?
@ac.render(partial: "helpers/bootstrap/#{section}/#{fragment}", locals: locals.merge({ content: content }))
end
Now, a lot is happening here. Most important was using the “view_context” from before to render a partial. We need to do this so that instance variables also get passed around. You will also notice that we’re using capture. That’s the magic that lets us have stuff inside other stuff. We do render it before calling the partial because if we didn’t things could not “reverse render” as needed.
Our last step is to wire up the helpers in a railtie. There are other ways, but this works really well for right now.
# /lib/bootstrap_helpers/railtie.rb
module BootstrapHelpers
class Railtie < Rails::Railtie
initializer 'bootstrap_helpers.view_helpers' do
ActiveSupport.on_load(:action_view) { include Bootstrap::BaseHelper }
ActiveSupport.on_load(:action_view) { include Bootstrap::NavBarHelper }
Questions Rails Developers Will Ask
These are the first bits put together so there are bound to be some questions. Especially where the architecture of the gem is concerned.
First, I did not want a name-spaced engine. That’s generally a good idea, but I didn’t want to have to call classes using the namespace. This gem is about making things easier, not more verbose.
Second, you can see that the “start” is a normal helper while the rest of the methods are in a class (and not a module for mixing in). This allows us to strongly control entry points. For example, you can’t call link
without already having a Navbar
object.
Next, you can see that by recalling the same class over and over we don’t force any more structure than we need to.
<%= navbar do |nav| %>
<%= nav.collapse id: 'mainMenu' do |c| %>
<%= c.items do |i| %>
<%= i.link 'Tasks', items_path %>
<%= i.link 'New Task', new_item_path %>
<% end %>
<% end %>
<% end %>
and
<%= navbar do |nav| %>
<%= nav.items do |i| %>
<%= i.link 'Tasks', items_path %>
<%= i.link 'New Task', new_item_path %>
<% end %>
<% end %>
Both examples are valid, though they will generate different markup and different nesting.
Finally, there is some code cleaning to be done. things are not DRY enough for my liking and there are some simple things that can be cleaned up, but that is a story for another post.
Making the Navbar better
Back in the Todo application, we have a simple, rather plain navbar. That’s ok, but we can do a few things to make it look a bit better.
<%= navbar classes: 'bg-dark navbar-expand-lg navbar-dark ps-4' do |nav| %>
<%= nav.brand content: 'Todo' %>
<%= nav.toggler target: 'mainMenu' %>
<%= nav.collapse id: 'mainMenu' do |c| %>
<%= nav.items class: 'ms-auto' do |i| %>
<%= i.link 'Tasks', items_path, class: 'btn btn-outline-secondary' %>
<%= i.link 'New Task', new_item_path, class: 'btn btn-outline-primary ms-4 me-4' %>
<% end %>
<% end %>
<% end %>
This adds a bit more markup but uses bootstrap css classes to modify the navbar. This allows for a lot of flexibility.
Final Thoughts
Navbar may not have been the best place to start to showcase the gem making life easier. The HTML is now drier and structure is enforced, but it’s a lot of structure. That can be cleaned up later. It’s also important to remember that it’s not the gem’s goal to make you use bootstrap a certain way, so some of the things we could do to make the erb less verbose should be done on the application level. For example, if you use many different navbars you might turn that erb into a partial with some logic that you use. But this first pass does hit the stated goals.