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.

A wireframe of the application's layout. It has a top bar navigation with link to home, about, pricing, sign up and log in. The rest of the layout is empty and a message reads: "The empty public/main.html.erb layout. Public views are rendered in here"

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.

An image of a wireframe for the default layout of the app. It has a sidebar navigation to the left with links to dashboard, search, bookings and my account. The main content area to the right has a message that reads: "The empty application.html.erb layout. Authenticated views are rendered here"

Searching cars

The most important feature of our hypothetical car-sharing app is searching for a car and choosing it.

A wireframe of the search page of the car-sharing app. It has no navigation links at the top or side of the layout. Instead it has a back button to the top left, a title to the right of the back button that reads Search, a few boxes underneath that represent filters and a content section that has a list of cars to the left and a map with pins to the right representing the search results.

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.

A wireframe that uses the application.html.erb layout with a left-hand side navigation but that in the content area, contains the whole complex search page with filters results the map etc.

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.

A wireframe of a new layout that has a top navigation bar with just a back button and a title that reads "Search". The content area is empty and has a message by the author that reads "The empty vehicles.html.erb layout. Views rendered by the Vehicles Controller are rendered here"

So when a user visits the index action for vehicles (which serves as the search page), Action View will perform the following lookup:

ControllerLayoutFound?
A specific layout for the controllerVehiclesControllervehicles.html.erb
A layout for its parent controllerApplicationControllerapplication.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:

A wireframe of the search page using the specific layout for the vehicles controller. It has a top navigation with a back button and a search title and a content area with filters at the top, search results to the left and a map to the right with pins representing the location of the filtered vehicles

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 wireframe showing the "profile" page of the car. It consists of four different screens which the user navigates to using tabs. There is a common element at the top of all 4 screens: The name of the car (Mazda 5: The all rounder!), how far away it is (200 meters, 5 minute walk), its purchase date (2021) and how many kilometres it has been driven (8,500). Below this information there is a horizontal navigation with links to: information, location, features and pictures. The four wireframes show rough drawings of each of these

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
InfoA vehicle's information or detailsVehicle::Details
LocationA vehicle's locationVehicle::Location
FeaturesA vehicle's featuresVehicle::Features
PicturesA vehicle's picturesVehicle::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?