Advanced Layout Rendering in Rails

Part 3: Making shared, data-rich layouts for subsections of your apps

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

To finish this series we'll finally tackle this part of the car-sharing app we've been creating in this Action View Layouts series.

To recap, in part 2 I described my process to uncover what the resources these screens show are and how these all fall under the namespace of a Vehicle (check out the article here if you need a refresher). This makes it easy to name and create the controllers that will handle these views:

  • Vehicle::DetailsController

  • Vehicle::FeaturesController

  • Vehicle::PicturesController

  • Vehicle::LocationsController

In this article, we'll explore how to design routes and leverage inheritance and implicit layout rendering to make this design a breeze.

Designing expressive routes

To access these controllers we will need to add our resources to the router. I find it helpful to visualise the actual URLs first before jumping in. Here's what I imagine:

  • vehicles/:vehicle_id/details

  • vehicles/:vehicle_id/features

  • vehicles/:vehicle_id/pictures

  • vehicles/:vehicle_id/location 👀

See anything special? All of these URLs describe plural resources except for one.

Even if these aren't Active Record models with backing database tables, you can still think of them as entities having relationships between them.

  • "A vehicle has many details". Can be translated as an index action for details

  • "A vehicle is enhanced with many features. Can be translated as an index action for features.

  • "A vehicle is presented visually using many pictures". Can be translated as an index action for pictures

  • "A vehicle is parked at a location". Can be translated as a show action for its location

Resources (plural) vs Resource (singular)

Rails' router allows you to declare singular or plural versions of a resource and this will take on a different meaning and create different URLs for your application. Let's take the Vehicles::Location controller as an example. If we were to use the plural version:

Rails.routes.draw do 
  # ...
  resources :vehicles, only: %w(index) do
    resources :locations, only: %w(show) # 👀 resourceS: plural
  end
end

The output of this will be:

...
vehicle_location_path    
GET    /vehicles/:vehicle_id/location/:id
location#show
...

But what if your vehicle's location doesn't live in another model and table? What if it's just an attribute of the vehicle itself? You won't have an ID to go look for it.

This is what the singular resource method is for:

Rails.routes.draw do 
  # ...
  resources :vehicles, only: :index do
    resource :locations, only: :show # 👀 resource: singular!
  end
end

Now the output is just as we described above

vehicle_location_path    
GET    /vehicles/:vehicle_id/location ✨ singular, with not id param
locations#show

The documentation for the resource method explains it very well:

Sometimes, you have a resource that clients always look up without referencing an ID. A common example, /profile always shows the profile of the currently logged in user. In this case, you can use a singular resource to map /profile (rather than /profile/:id) to the show action [...]

Scoping controllers to modules

We've solved one piece but we still have a problem. Did you notice the controller names in code snippets?

vehicle_location_path    
GET    /vehicles/:vehicle_id/location
locations#show ⬅️ this are the controller#action names

The controller is just locations instead of vehicles::locations. This can be an issue if there are other "locations" in your app. Imagine the user can have multiple stored locations where they usually search cars from. Like, from home and work. Both a "vehicle's location" and "a user's usual locations" need a controller with the word "locations" in them. This is why the router provides namespacing and scoping methods.

The most immediate benefit of using these methods is to allow your controllers to be nested within a module and to live in a different folder:

app/
  controllers/
    vehicles_controller.rb
    vehicles
      locations_controller.rb
      ...
# app/controllers/vehicles/locations_controller.rb
module Vehicles
  class LocationsController < ApplicationController
  end
end

namespace and scope are similar but have some slight nuances that make them better for different scenarios. If we were to use namespace :

Rails.application.routes.draw do 
  resources :vehicles, only: :index do 
    namespace :vehicles do 
      resource :location, only: :show
    end
  end
end

And look at the outcome:

vehicle_vehicles_location_path    GET    /vehicles/:vehicle_id/vehicles/location(.:format)    
vehicles/locations#show

You can see that we got (spoiler, it's sad):

  • A URL that has the word vehicles two times

  • A path helper that also has the word vehicle twice.

If we want to design beautiful and expressive routes, this is not the way. So let's try scope. This method receives several arguments but we care only about module today which will scope or nest our controllers under a different Ruby module:

Rails.application.routes.draw do
  resources :vehicles, only: :index do
    scope module: :vehicles do
      resource :location, only: :show
    end
  end
end
vehicle_location_path
GET    /vehicles/:vehicle_id/location(.:format)    
vehicles/locations#show

Now we got (spoiler: it's good!):

  • A simple and succinct URL 🎉

  • A very expressive path helper 🎉

  • A locations controller nested under the vehicles namespace 🎉

Why care so much?

We've gone on and on about routes and diving deep into this. Who cares if paths are simple or if things are nested? Why is this important? Because taking the time to design these properly has unlocked all of the pieces to create the interface we set out to create.

Putting it all together

A wireframe of the details page of a vehicle. Two rectangles highlight important parts of the image. A red one at the tops highlight information that is the same across all other pages for the vehicle. The green one encompasses the area where data will change based on the page visited

So how do we make it so all of the pages for the vehicle share the same information at the top (red square), but render different content at the bottom (green square)? Does this ring a bell?

A vehicle is composed of many resources such as location or pictures, it's the parent of all of these resources...

Its children share the same layout.

... see where I'm getting at here? If you've followed the series you'll probably start to see that we have seen this problem before. If there is a parent controller that can dictate how its children behave, we should create one:

module Vehicles
  class MainController < ApplicationContrller
  end
end

Saying it out loud helps bring it home:

We've created the main controller for the vehicle section of our app.

Because all of its children will use the same layout, we should create one for this main controller:

app/
  views/
    layouts/
      application.html.erb 
      vehicles
        main.html.erb ✅

We now make it so all child controllers inherit from this Main controller:

class Vehicles::DetailsController < Vehicles::MainController
  def index
    @vehicle = Vehicle.find(params[:vehicle_id])
    @details = @vehicle.details
  end
end

class Vehicles::FeaturesController < Vehicles::MainController
  def index
    @vehicle = Vehicle.find(params[:vehicle_id])
    @features = @vehicle.features
  end
end

class Vehicles::ImagesController < Vehicles::MainController
  def index
    @vehicle = Vehicle.find(params[:vehicle_id])
    @images = @vehicle.images
  end
end

All of these controllers need a @vehicle to render the top section of our layout. And because our routes are beautifully designed and conventional, all of them share the same param vehicle_id which means we can pull this out to the parent controller as a callback.

module Vehicles
  class MainController < ApplicationContrller
    before_action :set_vehicle

    private
    def set_vehicle
      @vehicle = Vehicle.find(params[:vehicle_id])
    end
  end
end

Enabling change

But what if this design didn't work well with your users and they are getting confused about tabs? Maybe you design an experience where there aren't tabs anymore and all information is stacked in one single view.

Does this mean you have to go back to a single controller with a bunch of instance variables?

No (if you use Turbo)

Since we have built a strong foundation that separates every resource the vehicle has, it's a matter of removing the tabs and making a template that leverages turbo frames. Let's give it a quick go.

For a Vehicle::Details#index page that will stack all of the info in our tabs into a single, scrollable view you could do something like this:

<!-- app/views/vehicles/details -->
<section>
  <h2>Details</h2>
  <%= render "details", locals: @details" %>
  <%= turbo_frame_tag "location", 
                       src: vehicle_location_path(@vehicle),
                       loading: :lazy %>
  <%= turbo_frame_tag "features", 
                      src: vehicle_features_path(@vehicle), 
                      loading: :lazy %>
  <%= turbo_frame_tag "pictures", 
                      src: vehicle_pictures_path(@vehicle), 
                      loading: :lazy %>
</section>

Notice how the frame tags now point to the controllers we had created and how we can even leverage the lazy loading feature of Turbo Frames to optimise the rendering of this view.

Conclusion

To recap, we have learnt:

  • How implicit rendering works in Action View in Rails

  • How to create layouts for specific controllers

  • How to create layouts that multiple controllers will share through the default lookup chain Action View goes through

  • How to design expressive routes that...

  • Allow you to use all the concepts above to create complex views

Can you think of a place in your app where you could apply this? Let me know!

See you in the next one.

Off the Rails

I've been loving writing these articles and seeing the reception from you all. Thanks for liking, reposting, retweeting and sharing this.

If you're a subscriber to the newsletter, I'd love for you to reply to this article and let me know if you liked it and maybe tell me what you'd like to hear about next.
If you haven't subscribed though, click here and subscribe!