I love car-sharing services. It has allowed me to live car-free for the past three and a half years cutting down on maintenance, taxes, stress ... you name it. I can also book small cars for short rides, vans for moving stuff and larger, more comfortable cars for trips. It's amazing!
Today, we'll use a hypothetical car-share app to learn how to use Action View layouts to their fullest.
Designing a car-sharing service
This wireframe shows four basic screens our app will have that show different areas of our application that we will want to treat a bit differently.
Conditional spaghetti 🍝
One way to achieve all of these different layouts is to squeeze everything into the default
<html> <head><!-- ... --></head> <body> <%= if current_user.signed_in? %> <%= if current_page?(booking_rides_path) %> <%= yield %> <% end %> <%= if current_page?(dashboard_path) %> <%= yield %> <% else %> <%= yield %> <% end %> <% else %> <!-- Public nav--> <%= yield %> <% end %> </body> </html>
Some pages require the user to log in, some don't. Some require their own specific set of rules and others share some visual aspects with others.
As you can probably tell, this will be hard to understand, extend and maintain. The issue with using a single layout is not just a matter of the ease of readability though. You could fix readability by turning most of this logic into several partials and call it a day. However this is a signal, and one you can train yourself to identify.
Identifying the problem
All of those conditionals are telling you something: There are several concepts here that are claiming their own space. It's an architectural problem! To allow our app to grow and have the flexibility to use different layouts we need to give it proper foundations for it to scale.
As I see this wireframe I'm thinking:
The app has public-facing, non-authenticated pages and other private, authenticated ones
Within the authenticated pages:
There's a design that houses links to most sections of the app like the dashboard, bookings and my account
There is a special case when searching where all navigation disappears and the user is fully focused on the search and car-choosing experience.
How does Rails find a layout
Before diving into this, remember that Rails Guides are probably the best and probably most underutilised resource to learn about the framework. Most of what I'll tell you about here you can find in the Layouts and Rendering in Rails guide.
As with most things Rails, the act of finding a layout that will wrap a template/view rendered by a controller action is based on conventions. In a fresh Rails app there is a single layout:
app/views/layouts/application.html.erb. This is the last fallback the framework will use to display a template. Because in a new app there aren't other layouts Rails uses this one.
You can make your controllers render a different layout by doing so explicitly or implicitly.
If you haven't thought much about it and just use the
application.html.erb layout then you are already using implicit rendering which relies on inheritance (more on this later).
On the other hand, you can tell Rails when to use a specific layout by:
I use all of these in different situations but today we will focus on implicit layout rendering leveraging inheritance because this will help you not only organise your views but also give you the tools to build a better architectural foundation for your app.
Authenticated or not authenticated
Let's focus on the first thing we identified. The app's needs are strikingly different for the authenticated and non-authenticated sections of it. It's almost as if they were two different apps and it is usually very obvious when looking at the design.
ApplicationController looks something like this:
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :authenticate_user! end
Before every controller action, you perform some type of user authentication. But, there is a portion of your app that is not authenticated and there are multiple pages that fit in that space like sign up, sign in, password recovery etc. For all of these pages, you would need to call
skip_before_action :authenticate_user!. That's very cumbersome and also error-prone. So instead of skipping this action on every controller, why not create a specific controller that all public pages will use?
# app/controllers/public/application_controller.rb class Public::MainController < ApplicationController skip_before_action :authenticate_user! end
I chose the name
Main in this article to make it obvious that it's different from
Application but you can name it whatever you want like:
Now every controller that is part of the public portion of your application and needs to skip authentication will be able to use this as its parent controller:
class Public::RegistrationsController < Public::MainController def new # renders the sign up page end def create # ... end end
At this stage, we still have a single
application.html.erb layout. Whenever we visit our new registrations controller it will use that one because remember that this is the ultimate fallback that Rails uses if no other options are provided.
The benefit of this technique is that now we can rely on Rails' conventions to have a new, specific layout for your public controllers. We can create a
main.html.erb layout under
app/views/layouts/public. Notice the name:
main.html.erb. It takes after the name of the controller which explains why in a fresh Rails app the default layout is named
app/ controllers/ application_controller.rb public/ ✨ main_controller.rb ⬅ Our new Controller registrations_controller.rb views/ layouts/ application.html.erb public/ ✨ main.html.erb ⬅ 👀 A layout with matching name ✨
Rails chooses the layout to render a view in a cascading way based on the controller's inheritance chain. For the
Public::RegistrationsController its chain looks like:
Public::RegistrationsControllerthat inherits from
Public::MainControllerthat inherits from
So when rendering the
Public::RegistrationsController#new action, Rails will perform the following lookup:
|A specific layout for the controller||❌|
|A layout for its parent controller||✅|
|A layout for its grandparent controller||(it's there but it doesn't look for it because it already found one)|
This feels like a good place to commit. Through the wireframes and the spaghetti
application.html.erb we saw earlier, we uncovered an important architectural pillar of our app: authenticated and non-authenticated pages have different needs and through different controllers and layouts we can convey those needs and give each an organised place to grow.
On the next episode...
In the next article, we'll tackle the next piece of our puzzle: organising the authenticated portion of our app.
Stay tuned and let me know what you think!
Off the Rails
This is the first article I posted after announcing on Twitter that I would start creating content to give back to the community and the response was amazing! Over one hundred people subscribed to my newsletter in the first couple of days. Thanks to everyone who subscribed, liked and retweeted!
Also, the car-sharing theme is part of this whole idea of designing beautiful cities and it's a topic a really enjoy. If you do too, take a look at this video from Not Just Bikes on car-sharing. This channel is AMAZING!