Ruby on Rails

Advanced Topics

new

Ruby on Rails Course

Introduction

There are some topics that we just haven’t had a chance to get into yet but will prove useful for you to know. In this section we’ll cover advanced routing, layouts, and a brief introduction to metaprogramming.

Lesson overview

This section contains a general overview of topics that you will learn in this lesson.

  • Singular resources.
  • Nested routes.
  • Member routes and collection routes.
  • Nesting layouts.
  • What metaprogramming is.
  • What design patterns are.

Advanced routing

You should be quite familiar by now with the bread and butter of routing – converting RESTful requests using the familiar HTTP verbs into mappings for specific controller actions (whether using the #resources method or explicitly specifying them using the get method). That’s 90% of what you’ll use your routes file for… but that other 10% gives you some pretty neat options like redirecting directly from the routes file, nesting routes inside each other, or parsing parameters from the incoming request.

Singular resources

You might have already run into this at some point without necessarily understanding it. Up until now, we’ve been talking about resources (like “posts” and “users”) where there are a whole lot of them. It seems fairly intuitive. In your config/routes.rb file, you represent these with a single line like resources :users.

Sometimes there are also resources where it actually only makes sense for there to be one. An example would be a User dashboard which displays interesting facts based on whichever user is logged in. There is only one dashboard template, it just happens to be smart enough to display things that are relevant for the user who is currently logged in.

In this case, it doesn’t make a whole lot of sense to display an “index” of dashboards, since there is only one (it just changes based on who is logged in). We can also say that, for any of the other actions which would normally require an ID to differentiate which resource we’re operating on (like #show), since there’s only one, we no longer need the id parameter.

The routes file line for a singular resource would look like:

  # in config/routes.rb
  resource :dashboard

Just note that the word “resource” is singular and so is dashboard. That trips up a lot of people who make the typo of writing “resource” instead of “resources” when they really want plural resources (which are more common).

The $ rails routes for a singular resource would only contain 6 routes (since we don’t use #index anymore), and you would no longer see any of the :id portions of the routes, e.g.

  edit_dashboard  GET /dashboard/edit(.:format)  dashboards#edit

…compared with the plural version of the same route:

  edit_dashboard  GET /dashboards/:id/edit(.:format)  dashboards#edit

Nested routes

Sometimes it just makes sense for one resource to be nested inside of another. For instance, a listing of lessons like this logically falls within a listing of courses – so you’d expect a URL sort of like http://example.com/courses/1/lessons/3. The way to achieve this nesting is in the routes file by literally nesting one resource inside a block given to another, which might look something like:

  # config/routes.rb
  TestApp::Application.routes.draw do
    resources :courses do
      resources :lessons
    end
  end

Note that the #resources method now takes a block which will consist of a set of routes.

When you visit the URL, you’ll have to specify the :id parameter for BOTH objects. The $ rails routes for the above would include something like:

  course_lesson  GET  /courses/:course_id/lessons/:id(.:format)  lessons#show

It should also be noted that you’re being taken to the controller of the deepest nested resource, and that’s also the :id parameter which will be called :id (any parent resource parameters, as in the above, will be specifically called something like :course_id).

View helpers are also automatically generated in a logical way (as you can see in your $ rails routes output). When you use view helpers like #course_lesson_path you will need to specify both parameters in order, e.g. course_lesson_path(1,3).

Don’t nest routes too deeply! If you’re more than a layer or two deep, something should be different. In fact, oftentimes you’ll see only some of the controller actions nested – only the ones that actually need the parent’s ID to uniquely specify it. For instance, you can grab a specific Lesson by knowing only its ID. But to get all the lessons that are listed beneath a specific Course, you need the Course ID so it will have to be nested. Same is true for creating lessons, since they will need a parent specified:

  # config/routes.rb
  TestApp::Application.routes.draw do
    resources :courses do
      resources :lessons, :only => [:index, :create]
    end
  end

If this seems a bit confusing at first, you’ll pick it up quickly when you actually run into it in your own coding. If you find yourself working inside your controller and needing the parent’s ID, the route should have been nested. If you find that you don’t need the parent’s ID, it doesn’t need to be nested. Easy enough.

Member and collection routes

Sometimes you want to add another non-RESTful route to a resource. If you’d like to add a route to just a single member of that resource, use the #member method:

  # config/routes.rb
  TestApp::Application.routes.draw do
    resources :courses do
      member do
        get "preview"   # Preview a single course
      end
    end
  end

That route would map to the courses#preview action. You can add as many as you’d like.

If you’d like to add a non-RESTful route to the whole collection of your resource (so you don’t need to specify the :id attribute, like with the index action), you instead use the #collection method:

  # config/routes.rb
  TestApp::Application.routes.draw do
    resources :courses do
      member do
        get "preview"  # Preview a single course (requires ID)
      end
      collection do
        get "upcoming"  # Show a list of *all* upcoming courses (no ID needed)
      end
    end
  end

The upcoming route will map to the courses#upcoming action but will not take an :id parameter.

If any of this seems confusing, just play around with them and run $ rails routes to see what is happening behind the scenes.

Redirects and wildcard routes

You might want to provide a URL out of convenience for your user but map it directly to another one you’re already using. Use a redirect:

  # config/routes.rb
  TestApp::Application.routes.draw do
    get 'courses/:course_name' => redirect('/courses/%{course_name}/lessons'), :as => "course"
  end

Well, that got interesting fast. The basic principle here is to just use the #redirect method to send one route to another route. If your route is basic, it’s a really straightforward method. But if you want to also send the original parameters, you need to do a bit of gymnastics by capturing the parameter inside %{here}. Note the single quotes around everything.

In the example above, we’ve also renamed the route for convenience by using an alias with the :as parameter. This lets us use that name in methods like the #_path helpers. Again, test out your $ rails routes with questions.

Controllers, models and keeping things RESTful

Along with the advanced routing topics covered, it can also be helpful to think about controllers in Rails that don’t necessarily have their own ActiveRecord model to work with. Consider that we have a request for the application so that a lesson can have accompanying images. That seems easy enough, so we can update our model:

# app/models/lesson.rb
class Lesson < ApplicationRecord
  # other stuff
  has_many_attached :images
end

Then, we think about how we might want to manage these images from the route and controller side. We might think of something like this at first:

# config/routes.rb
resources :lessons do
  member do
    patch :attach_image
    delete :remove_image
  end
end

Then we have accompanying methods in the LessonsController to process the images. This would work well enough, but when we think about Rails controllers as standalone concepts, we might choose to implement this feature differently. Consider this second approach to the implementation:

# config/routes.rb
resources :lessons do
  resources :images, only: [:create, :delete]
end

Along with a new controller Lessons::ImagesController which looks like this:

# app/controllers/lessons/images_controller.rb
module Lessons
  class ImagesController < ApplicationController
    def create
      # logic to attach images to a course
    end

    def destroy
      # logic to remove an image image from a course
    end
  end
end

What we’ve done is made the implementation more RESTful, because we no longer have any custom non-RESTful actions. Instead, we have a whole new (RESTful) controller. This controller doesn’t relate to its own model to handle these actions, but works on the Lesson model. Not only that, by using a new controller we are able to stick to the REST actions to describe what we are doing: creating a new image attachment, or destroying an image for a lesson.

In fact, if you think about it, this was implicit in our original attempt: attach_image and remove_image both follow the <action>_<noun> pattern, which might be a signal we could use another resource. We’ve also used a nested controller here to provide a clue to the next developer of our intention. By keeping our actions in the RESTful realm, it can become easy to extend the application. Keeping things RESTful also makes it easier to understand what is happening without having to do a lot of digging around.

When we can think beyond the controller/model coupling in Rails, it can open the door to many possibilities. You could have a controller tied to a specific attribute of a model, even!

For more information and examples, there is an excellent talk by Derek Prior called “In Relentless Pursuit of REST” in the additional resources section that is highly recommended.

Advanced layouts: Nesting layouts and passing information

We got pretty good coverage of view layouts in the lesson on Views but one other topic involves rendering multiple layouts for one page, which allows you to create unique sections that still reuse a lot of the stylings that you might want to keep consistent across your whole site (e.g. the footer). For example, maybe the user pages should have a different styling than your home page. The first thought might be to try and have a different stylesheet for each layout but remember that Rails’ Asset Pipeline jams all your stylesheets together anyway.

A better way of doing things is to tell your layout to do some stuff (whatever you might normally have your layout do) and then render another layout using the render :template => "your_layout.html.erb" method. You are sort of using your layouts like a view might use a view partial.

You can also pass information from the first layout to the one it renders by using the #content_for method. This lets you create logic in your main layout that is dependent on what is passed by your individual layout files… the possibilities are endless.

For instance, you might have a specific layout file for your static pages called app/views/layouts/static_pages.html.erb. This file will be rendered by default (if it exists) for views generated from your StaticPagesController (which is a Rails default). Let’s say, though, that you want your static pages to look almost identical to the rest of the site but you don’t want the navbar to appear across the top.

In this case, you would tell your static_pages.html.erb layout to call the application.html.erb layout but also pass it some special CSS by using the #content_for method, e.g.

  # app/views/layouts/static_pages.html.erb

  <% content_for :stylesheets do %>
    #navbar {display: none}
  <% end %>
  <%= render :template => "layouts/application" %>

Then your application.html.erb layout needs to be set up to catch that content and use it, for instance by adding this #yield line:

  # app/views/layouts/application.html.erb
  ...
  <head>
    ...
    <style><%= yield :stylesheets %></style>
  </head>
  ...
  render :template => "static_pages.html.erb"
  ...

When you #yield to a particular content block, in this case :stylesheets, it will essentially drop the code from inside of that content_for’s block to where the #yield method was. So in the above example, we effectively added some CSS styling to the application layout by first rendering a special static_pages.html.erb layout and then passing the styles to the main application.html.erb layout using #content_for. The result would look like:

  # app/views/layouts/application.html.erb
  ...
  <head>
    ...
    <style> #navbar {display: none} </style>
  </head>
  ...

This trick is useful for more than just passing stylesheet information… any time you find yourself wanting to make a section of your site look different but without totally redesigning it with a fully new layout, you might consider nesting your layouts and passing information from one to another.

Metaprogramming Rails

What is “Metaprogramming”? It’s a great and useful concept that’s used all over Rails and you can put it to work yourself too. It’s basically the idea that your application or script actually creates functions or methods or classes on the fly while it’s running and can dynamically call them as well. It’s one of the great parts of using an interpreted language like Ruby… it’s sort of baked into the language. We’ll just skim the surface here but you should definitely look into it more on your own once you feel comfortable with the nuts and bolts of Rails.

An example of metaprogramming in action in Rails is with the route helpers. When your Rails application fires up for the first time, it loads the config/routes.rb file, which might contain the line get "home" => "static_pages#home" so your users can type http://www.yoursite.com/home to get back to the home page. Rails then creates a couple methods for you, including the home_path and home_url helpers. That’s one part of metaprogramming!

The routes example almost isn’t fair, though, because you wrote your routes.rb file and probably hard coded a bunch of #home_path or #home_url method calls based on what you knew would be in there. What about more dynamic situations where you don’t know ahead of time what the method is going to be called?

Ruby provides the #send method to save the day. If you want to run a method on an object, just send that object the method and any arguments you want. A basic example you can do on your command line is 1+2:

  > 1 + 2
  => 3
  > 1.send(:+, 2)
  => 3

In an ordinary situation, there’s no reason to use the #send method but if you don’t know which method you’re going to need to call, it’s a lifesaver. Just pass it the symbolized name of the method you want to run on that object and Ruby will go looking for it.

But how do you define a new method on the fly anyway? In this case, you can use the #define_method method, which takes the symbol of what you’d like to define and a block representing the method itself. The following examples were taken from this metaprogramming guide from ruby-metaprogramming.rubylearning.com:

  class Rubyist

    define_method :hello do |my_arg|
      my_arg
    end
  end

  obj = Rubyist.new
  puts(obj.hello('Matz')) # => Matz

Another very powerful tool is the #method_missing method. You’ve certainly seen errors that say something to the effect of “Hey you, you tried to call a method that doesn’t exist!” and the stack trace will probably run through something called method_missing. Most likely, you had a typo and spelled your method incorrectly.

Basically, #method_missing is a method of Ruby’s BasicObject class which gets inherited by every single object in Ruby and it is called whenever you try to run a method that doesn’t actually exist. It also gets passed all the arguments you tried to send and any blocks that went with it. That means that you can override #method_missing yourself for a given object and use whatever was previously called, for example printing out a message saying the name of the method you tried to call and its arguments:

  class Rubyist

    def method_missing(m, *args, &block)
      str = "Called #{m} with #{args.inspect}"

      if block_given?
        puts str + " and also a block: #{block}"
      else
        puts str
      end
    end
  end
  > Rubyist.new.anything
  "Called anything with []"
  => nil

  > Rubyist.new.anything(3, 4) { "something" }
  "Called anything with [3, 4] and also a block: #<Proc:0x007fa0261d2ae0@(irb):38>"
  => nil

Metaprogramming is really nifty stuff and there are tons of interesting uses for it. You don’t need to master it to learn Rails, so only dive into it once you’re comfortable with Rails, but it will certainly be useful to you in the real world. There are all kinds of metaprogramming tricks and patterns and tips out there but it’s beyond the scope of this course to dive into them.

Here’s a good example of metaprogramming to DRY up your code.

Check out Metaprogramming Ruby by Paolo Perrotta if you’re really curious.

Design patterns

Design patterns have a mixed reputation among software developers. On the one hand, they represent “best practices” for how to code past a given situation (not specific code, just a template for how to fix something). On the other, they can be sort of needlessly prescriptive. See the Wikipedia article on Design Patterns for an overview. We won’t be covering specific patterns in this course.

The Wikipedia article on SOLID provides a good overview and good links related to SOLID software design. If you’re looking to write great code, you’ll need to know each of the principles the letters represent (paraphrasing):

Luckily, Rails has done a pretty good job of following these, so you should have absorbed some good habits just through using it. But you’ll want to take a minute and read up on each of them (including the odd-sounding ones) because they’re fairly central to all software engineering (and a ripe interview question).

There’s a useful book written on anti-patterns, which can help you clean up your code by identifying bad smells, called Rails Antipatterns by Tammer Saleh and Chad Pytel.

I18n: Internationalization

Internationalization and Localization is the process of adapting your application to fit specific geographies and/or languages. It’s outside our scope to cover, but for those who are interested, check out this Sitepoint tutorial on internationalization, as suggested by K. Bates.

Assignment

  1. Skim the Rails Guide on Routing section 2.6 about namespacing.
  2. Read the same guide sections 2.7-3.7 to learn about nested, member and collection routes and more.
  3. Read the same guide, sections 3.8-3.15 for a variety of different advanced routing topics including constraining the inputs to your routes and redirection.
  4. Skim the same guide, chapter 4. Some stuff we’ve seen but most is just to give you a sense for what’s possible. When you need it, you’ll probably Google your way back there.
  5. Read the Rails Guide on Layouts section 3.5 to see how to pass information between your view file and your layout file, including CSS styles. Really take a minute to understand what’s going on in the example there.
  6. If you’re interested, take a peek at Ruby metaprogramming. It’s not essential to building early Rails apps but you’ll definitely start running into it more in “the wild”.
  7. Glance through this Slideshare Presentation on SOLID principles.

Conclusion

In this lesson we covered some fairly random and intricate concepts but useful stuff to at least get familiar with, even if you’re not going to use it every day. Experience is the real key here – in the course of building awesome stuff you’ll run into the need for all of the things you just learned and it might just save you a lot of time and complexity in your code.

The more general principles like SOLID design and metaprogramming will be useful to you regardless of whether you stick with Ruby and Rails or move on to better and brighter things.

Knowledge check

The following questions are an opportunity to reflect on key topics in this lesson. If you can’t answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.

Additional resources

This section contains helpful links to related content. It isn’t required, so consider it supplemental.

Support us!

The Odin Project is funded by the community. Join us in empowering learners around the globe by supporting The Odin Project!