Layout rendering in Rails Part 2: Implicit, per controller layouts
Leverage Rails' conventions to clean up layouts on a per controller basis
In the first part of this series, we learnt how implicit rendering works to separate authenticated vs unauthenticated sections of an app. In this second part, we will learn how to use this technique to build interfaces for specific controllers.
The application's default layout
In the previous article, we designed a structure such that the unauthenticated pages of our app always inherited by a Public::MainController
and we created a public/main.html.erb
layout so that all public pages used it.
After users log in, a different layout will be selected as the default. For our case, this will be the application.html.erb
layout that comes by default on a Rails app. This also means that at this stage all of our authenticated controllers will inherit from the ApplicationController
.
Searching cars
The most important feature of our hypothetical car-sharing app is searching for a car and choosing it.
You can notice in this wireframe that the UI for searching for a car does not have a side navigation like the main section of the app. This is intentional. Through this design, we are helping the user focus on a complex task that requires filtering, looking at a map for the closest car, sifting through different options, etc.
This search page can be thought of as the index of vehicles available for hire.
class VehiclesController < ApplicationController
def index
# ...
end
end
However, if we don't do anything else, the search page will be rendered inside of the application.html.erb layout.
This is starting to look very busy. Imagine that at some point one of the filters becomes very complex and requires a drawer or slide-over. How will you fit all of that in this layout?
Using specific layouts per controller
In the previous article, we learnt that Action View has a conventional lookup chain that searches for the layout to render a view and that it starts by looking for a template named after the controller before falling back to the one named after the controller's parent.
If the inheritance chain looks like this:
VehiclesController
-> ApplicationController
This means that Action View will look first for a layout called vehicles.html.erb
.
So let's create one!
app/
controllers/
application_controller.rb
✨ vehicles_controller.rb ⬅ Our new Controller
public/
main_controller.rb
registrations_controller.rb
views/
layouts/
application.html.erb
✨ vehicles.html.erb ⬅ 👀 A layout with matching name ✨
public/
main.html.erb
This layout won't have the left-hand side navigation. Instead, it will just have a title and a back button.
So when a user visits the index action for vehicles (which serves as the search page), Action View will perform the following lookup:
Controller | Layout | Found? | |
A specific layout for the controller | VehiclesController | vehicles.html.erb | ✅ |
A layout for its parent controller | ApplicationController | application.html.erb | (it's there but it doesn't look for it because it already found one) |
The resulting view will then look like our initial wireframe:
This looks much cleaner and allows for the interface to grow and add a drawer, slide-over or any other UI element for more complex filtering in the future. But it's not just about looks though.
Our code is also very simple. We now have a couple of layouts dedicated to the specific needs of our app and we didn't have to write any conditional logic to do this.
Looking at the wireframes though, there's something even harder to do that can be solved in the same, simple way.
Choosing the right vehicle
After using our amazing search page, the user sees a good selection of vehicles nearby and wants to choose the best one for their next trip. Doing so requires evaluating the car's specific features, pictures, location, etc. So when the user clicks on one of the search results, we will present them with these screens:
A single controller and view might not be enough
Designs like these can be implemented in multiple ways. One way could be getting all of this information in a single controller action:
class VehicleCompleteDetailsController < ApplicationController
def show
@vehicle = Vehicle.find(params[:id])
@general_information = @vehicle.general_information
@coordinates = @vehicle.coordinates # to render a map
@directions = @vehicle.directions # writen notes on how to get there
@pictures = @vehicle.pictures
end
end
Presenting all of this in tabs requires us to create a tabbed interface on the front end using Javascript. Even though it's fine to do it this way, a different approach allows for simpler, atomic controller actions that have a single, concrete responsibility and are ready for extension in the future if need be.
The data in this example doesn't seem that complex but extrapolate this example to other situations where you have had controllers with many collections of different resources that sit behind tabs. These controllers and views definitely reach a breaking point.
A better controller architecture
When I see that wireframe, I immediately start thinking about the name of the underlying resources they represent. Those resources can be Active Record models but, most often than not, they are just made-up resources that map to a few columns of a model, an entire model or a combination of several model's data. To do this, I usually try to describe it out loud in sentence form. For this design, I came up with the following:
For the tab... | A sentence describing its content is ... | Which becomes a new resource's name |
Info | A vehicle's information or details | Vehicle::Details |
Location | A vehicle's location | Vehicle::Location |
Features | A vehicle's features | Vehicle::Features |
Pictures | A vehicle's pictures | Vehicle::Pictures |
Notice how by saying it in sentence form you can immediately tell that there are 4 different resources all belonging to the same namespace: The Vehicle
.
On the next episode...
I know this is a bit of a cliffhanger! Sorry about that, but I want to stop here and allow the next article to be fully focused on a route design technique that is not very common but that will allow us to:
create very expressive path helpers
design very intuitive and scalable controller structures
learn a way to continue leveraging implicit layouts for views that share common information.
The next article is almost ready so it'll come pretty soon. Stay tuned!
Sidenote
I haven't written the layout files here because I don't think writing a complete side navigation HTML layout document or a top nav one adds too much value to the article.
However, I invite you to imagine if we had a single layout, how many conditionals we would have to have written so far to achieve what we have so far?