ROR : views, errors and loading paths

Today we gonna mess with views, errors and loading paths in rails.
I gonna recap basic stuffs about render method, explain layout missing error then share couple of tricks I use with loading paths.

Render basics

This part show some usage of render method. If it's your first time with Rails you should better start reading the official guide for layout and rendering :

Ready ? Let's play a with render, it will provide examples for the rest of the article.

Render : In Controllers

Rails offer multiple ways display views. Here couple way to load app/views/users/posts/index.EXT

I use EXT means extension like index.en.js.rabl that means in this specific case an index view in ENglish responding to JS output format processable by RABL handler... for less confusion, I will write just EXT when it's not helpful in described scenarios.

class Users::PostsController < UserResourcesController
  layout 'application_layout'
    def index
      # by convention it render the action name, here 'index'
    end

    def show
      render 'index'
    end

    def new
      render action: :index
    end

    def edit
      render 'users/posts/index'
    end
end

Render : In Views

It's also useful in views. You can split views in small partials and render them dynamically.By the way, I use HAML.

So here a simple layout/application.html.haml

!!! 5
%html
  %head
    = render partial: 'head' # will load head.EXT
    = yield :head
  %body
    %header
      / will display _navigation.EXT wrapped in _header.EXT
      = render partial: 'navigation', layout: 'header' 
      = yield
    %footer
      - unless current_user # this is ugly, we will change this later
        = render partial: 'footer' # will load _footer.EXT
      - else
        = render partial: 'logged_footer' # will load _footer.EXT

Missing view error !

Let say _footer partial is missing, Rails will through an error similar to:

Missing partial users/posts/_footer, application/_footer with {:handlers=>[:erb, :builder, :haml]}. Searched in:
   * "/Code/rails_app/app/views"
   * "/Code/.rvm/gems/ruby-1.9.3-p125/gems/devise-2.0.4/app/views"

Let's explain this.

Explained error : First view paths

Rails tried out to load view in the following paths top-down order.

users/posts/_footer application/_footer As you can see, it look by default at the controller view folder according to namespace and to the application folder.

It means you can have a general footer for you application and a specific one for your controller

Explained error : Then Option filter

The error describe options provided to search the correct view.

Those options can be provided directly to render or configured by the controller.

{:handlers=>[:erb, :builder, :haml]}

By default view handlers are erb and builder But because I have the gem 'rails-haml' it allow me to have view.html.haml there is more options that you should know

  • locale => view.en.html.haml
  • formats => view.(en.).js.erb

This topic is largely covered here :

Explained error : Finally, Application, libs and engines path

You may noticed, i'm using the excellent devise gem. It add another view folder path to lookup with view paths :

  • /Code/rails_app/app/views
  • /Code/.rvm/gems/ruby-1.9.3-p125/gems/devise-2.0.4/app/views

What to remember about this ? Actually you can share views (and also assets) from any lib/engine/gem

Loading path management !

You remember the comment in the application layout ? Good view contains as few logic as possible. Some people even use logic less tempting language, but I don't really enjoy the syntax and prefer HAML.

Playing with partial, collection you can already remove a lot of things. But we can go deeper and add our own loading path ...

Loading : In a subcontext

We gonna remove the visitor|logged user messy "if else" by creating a "users context". Did you noticed that PostsController parent is UserResourcesController instead of ApplicationController ?

This controller will not be routed but only shared common features to user subresources such as view loading path :

class UserResourcesController < ApplicationController
  before_filter :authenticate_user! # security
  before_filter :prepend_users_view_path # add our view path

     # ...

protected
  def prepend_users_view_path
    prepend_view_path 'users' # we tell to add our specific path
  end

end

Now your view paths for subcontrollers are :

  • users/posts/_footer
  • users/application/_footer
  • application/_footer

It means we can now replace the ugly logic in our application layout with :

%footer= render partial: 'footer'

And moving views/application/_user_footer.html.haml as views/users/application/_footer.html.haml Generally specking, it provides a way to define sub applications that can override general application views.

It could be useful for header, footer, side bar and so one ...

Loading : Same views context From multiple controller namespace

I recently met this problematic for an API.

You have multiple controllers for a unique model. Both handle different access types and filtering but they display specific collections and resources with the same views.

module TeamResources
  class PostsController < AccountResourcesController; end
  class CommentsController < AccountResourcesController; end
end

module UserResources
  class PostsController < AccountResourcesController; end
  class CommentsController < AccountResourcesController; end
end

you want to have the following view folders :

  • app/views/account_resources
  • app/views/account_resources/posts
  • app/views/account_resources/comments

If you try prepend view path like this :

class AccountResourcesController < ApplicationController
  def preprend_account_resources_view_path
    prepend_view_path 'accounts'
  end
end

It will not work the way you expect making impossible to use a render action in PostsControllers and CommentsControllers.

Missing template users/posts/index, accounts/index, application/index with {:handlers=>[:erb, :builder, :haml]}. Searched in:
  * "/Code/rails_app/app/views"
  * "/Code/.rvm/gems/ruby-1.9.3-p125/gems/devise-2.0.4/app/views"

The solution I found is to override controller_path of ActionController::Base

class AccountResourcesController < ApplicationController
  RESOURCES_PATH_REG = "(?<resource_path>([a-z0-1_]+\/)+[a-z0-1_]+)_controller/"
  SUB_RESOURCES_PATH_REG = /account_resources_path\/#{RESOURCES_PATH_REG}/

  def controller_path
    @_controller_path ||= "accounts/#{account_resources_controller_path}"
  end

protected
  def account_resources_controller_path!
    _str = self.class.to_s.underscore
    _system= AccountResourcesController
    matching = system::SUB_RESOURCES_PATH_REG.match? _str
    raise "cannot match sub resources path in #{_str}" unless matching
    matching[:account_resources_path]
  end
end

This way it will load app/views/accounts/posts/index for both index posts action methods of team and user namespace.

Hope you find this useful.

Done.