- This guide explains the interaction between the Controller and View in the Model-View-Controller (MVC) architecture of Rails.
-
Controller's Role Manages request handling and delegates complex operations to the Model.
-
View's Role: Responsible for rendering the response to the user.
-
Handoff Process: The Controller determines what to send as a response and calls an appropriate method to generate it.
-
View Rendering:
-
Rails wraps views in a layout.
-
It may also include partial views for modularity.
-
-
Full-Blown Views: When rendering a complete view, Rails manages additional processes like layout application and partial inclusion.
-
In Rails, controllers can create an HTTP response in three ways:
-
render: Creates a full response to send back to the browser.
-
redirect_to: Sends an HTTP redirect status code to the browser.
-
head: Creates a response consisting solely of HTTP headers.
-
-
Rails follows the principle of "convention over configuration", meaning that:
Controllers automatically render views with names matching valid routes.
-
If no explicit render is used, Rails looks for an action_name.html.erb template in the controller's view path.
Controller (BooksController):
class BooksController < ApplicationController
end
Routes (config/routes.rb):
resources :books
View (app/views/books/index.html.erb):
<h1>Books are coming soon!</h1>
- When navigating to
/books
, Rails automatically rendersapp/views/books/index.html.erb
.
- To display data from the database:
Updated Controller (BooksController):
class BooksController < ApplicationController
def index
@books = Book.all
end
end
- Rails automatically renders
app/views/books/index.html.erb
.
ERB Template to Display Books:
<h1>Listing Books</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.content %></td>
<td><%= link_to "Show", book %></td>
<td><%= link_to "Edit", edit_book_path(book) %></td>
<td><%= link_to "Destroy", book, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to "New book", new_book_path %>
-
The actual rendering is handled by
ActionView::Template::Handlers
. -
The file extension determines the template handler
(e.g., .erb for Embedded Ruby)
. -
This summarizes how Rails handles responses in controllers efficiently using conventions.
-
The render method in Rails handles rendering content for the browser.
-
It allows customization of rendering behavior, including:
-
Rendering default views
-
Rendering specific templates or files
-
Rendering inline code or nothing at all
-
Rendering text, JSON, or XML
-
Specifying content type or HTTP status
-
render_to_string
-
Returns a string representation of the rendered content instead of sending a response to the browser.
-
Accepts the same options as render.
- You can render a different template within the same controller using render:
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render "edit"
end
end
- If update fails, it renders the edit.html.erb template within the same controller.
Alternative Syntax
- You can use a symbol instead of a string and specify an HTTP status:
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render :edit, status: :unprocessable_entity
end
end
status: :unprocessable_entity
indicates a validation failure (HTTP 422).
- In Rails, you can render a template from a different controller by specifying the full path relative to app/views.
Usage
- If you want to render a view from a different controller, use the full path:
render "products/show"
-
Rails detects the different controller because of the slash (/) in the path.
-
You can also explicitly specify the template option:
render template: "products/show"
- This approach was required in Rails 2.2 and earlier but is now optional.
Example Scenario
- If you're inside
AdminProductsController
located inapp/controllers/admin
, and you need to render a view fromapp/views/products
, you can do:
render "products/show"
- or explicitly:
render template: "products/show"
Key Takeaways
-
Use the full path relative to
app/views
when rendering from another controller. -
The embedded
slash (/)
signals Rails to look for the template in a different controller’s view directory. -
:template
is an optional explicit way to define the view path.
-
Two Ways of Rendering:
-
Rendering the template of another action in the same controller
-
Rendering the template of another action in a different controller
-
-
Both methods are variations of the same operation.
Rendering edit.html.erb
in BooksController#update
- If the book update fails, the following render calls will all render the edit.html.erb template from views/books:
render :edit
render action: :edit
render "edit"
render action: "edit"
render "books/edit"
render template: "books/edit"
render inline
allows rendering ERB directly within the method call.
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
-
Not recommended as it violates
MVC principles
. -
You can specify Builder instead of ERB using type: :builder.
render inline: "xml.p {'Horrid coding practice!'}", type: :builder
render plain
sends pure text without markup.
render plain: "OK"
-
Useful for AJAX or API responses.
-
To include the layout, use
layout: true
and a.text.erb
layout file.
render html
sends HTML back to the browser.
render html: helpers.tag.strong("Not Found")
-
HTML entities are escaped unless composed with
html_safe
. -
Prefer using a view template for complex
HTML
.
render json
automatically callsto_json
on objects.
render json: @product
render xml
automatically callsto_xml
on objects.
render xml: @product
render js sends JavaScript to the browser.
render js: "alert('Hello Rails');"
render body
sends content without specifying content type.
render body: "raw"
- Default response type is
text/plain
.
render file
renders a raw file(without ERB processing)
.
render file: "#{Rails.root}/public/404.html", layout: false
-
Security concern: Avoid user input for file paths.
-
Alternative:
send_file
is often a better choice.
render
an object callsrender_in
on the object.
class Greeting
def render_in(view_context)
view_context.render html: "Hello, World"
end
def format
:html
end
end
render Greeting.new # => "Hello, World"
- Alternative: Use
renderable:
option.
render renderable: Greeting.new
-
:content_type
-
:layout
-
:location
-
:status
-
:formats
-
:variants
- By default, Rails serves responses as
text/html
(or application/json for:json
and application/xml for:xml
). You can specify a different content type:
render template: "feed", content_type: "application/rss"
-
Controls the layout used in rendering:
- Use a specific layout:
render layout: "special_layout"
- Render without a layout:
render layout: false
- Sets the HTTP Location header:
render xml: photo, location: photo_url(photo)
- Overrides the default HTTP response status:
render status: 500
render status: :forbidden
- Rails understands both numeric codes and symbols:
200 :ok
201 :created
404 :not_found
500 :internal_server_error
- If a non-content status (100-199, 204, 205, or 304) is used, content will be dropped.
- Specifies response formats:
render formats: :xml
render formats: [:json, :xml]
- Raises
ActionView::MissingTemplate
if the specified format template does not exist.
- Allows specifying template variations:
render variants: [:mobile, :desktop]
- Rails searches for templates in this order:
app/views/home/index.html+mobile.erb
app/views/home/index.html+desktop.erb
app/views/home/index.html.erb
- Alternatively, set variants in the controller:
def index
request.variant = determine_variant
end
private
def determine_variant
variant = :mobile if session[:use_mobile]
variant
end
- Raises
ActionView::MissingTemplate
if no matching template is found.
-
Rails looks for a layout file in
app/views/layouts
matching the controller name. -
If not found, it defaults to
application.html.erb
orapplication.builder
. -
.erb
layout is prioritized over.builder
if both exist.
- Override default layout using layout declaration in controllers.
class ProductsController < ApplicationController
layout "inventory"
end
- Set a global layout in
ApplicationController
:
class ApplicationController < ActionController::Base
layout "main"
end
- Use a method to dynamically select a layout:
class ProductsController < ApplicationController
layout :products_layout
private
def products_layout
@current_user.special? ? "special" : "products"
end
end
- Use a
Proc
for inline logic:
class ProductsController < ApplicationController
layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
end
- Use
:only
and:except
options to limit layout usage:
class ProductsController < ApplicationController
layout "product", except: [:index, :rss]
end
-
Layouts cascade down in the hierarchy.
-
More specific layouts override general ones.
class ApplicationController < ActionController::Base
layout "main"
end
class ArticlesController < ApplicationController
end
class SpecialArticlesController < ArticlesController
layout "special"
end
class OldArticlesController < SpecialArticlesController
layout false
def show
@article = Article.find(params[:id])
end
def index
@old_articles = Article.older
render layout: "old"
end
end
Layout Usage:
-
Default:
main
-
SpecialArticlesController#index
:special
-
OldArticlesController#show
: No layout -
OldArticlesController#index
:old
-
If a template/partial isn't found, Rails looks up the controller hierarchy.
-
Example hierarchy:
app/views/admin/products/
app/views/admin/
app/views/application/
- Shared partials can be placed in
app/views/application/
.
<%# app/views/admin/products/index.html.erb %>
<%= render @products || "empty_list" %>
<%# app/views/application/_empty_list.html.erb %>
There are no items in this list <em>yet</em>.
- Rails developers may encounter the error:
Can only render or redirect once per action
-
This occurs due to a misunderstanding of how render works. If multiple render calls are executed within the same action, Rails will throw an error.
-
Example of Double Render Error
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
render action: "regular_show"
end
- If
@book.special?
istrue
,render action: "special_show"
is executed. - However, execution continues, and
render action: "regular_show"
is also called, causing an error.
A simple way to avoid this error is by using return
to stop execution after the first render
:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
return
end
render action: "regular_show"
end
-
Alternative Approach: Rely on Implicit Rendering
-
Rails automatically renders the default template if render is not explicitly called. The following works without errors:
def show
@book = Book.find(params[:id])
render action: "special_show" if @book.special?
end
- How This Works:
If @book.special? is true, special_show is rendered.
- Otherwise, Rails will implicitly render
show.html.erb
.
redirect_to
sends a new request to a different URL.
redirect_to photos_url
redirect_back
returns the user to the previous page usingHTTP_REFERER
.
redirect_back(fallback_location: root_path)
redirect_to
andredirect_back
do not halt execution immediately; usereturn
to stop further execution if needed.
-
By default,
redirect_to
uses HTTP status code 302 (temporary redirect). -
To specify a different status, use the
:status
option:
redirect_to photos_path, status: 301 # Permanent redirect
- Accepts both numeric and symbolic header designations.
-
redirect_to
instructs the browser to make a new request. -
render
does not trigger a new request; it renders the specified template within the current request. -
Example of render Issue:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
render action: "index" # Problem: @books is not set
end
end
- If
@book
isnil
, rendering index will fail because@books
is not initialized.
Correcting with redirect_to
:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
redirect_to action: :index
end
end
-
This triggers a new request for
index
, ensuring@books
is properly set. -
Downside: Requires a round-trip request, adding latency.
Alternative Using render
with flash.now
:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
@books = Book.all
flash.now[:alert] = "Your book was not found"
render "index"
end
end
-
This avoids a round-trip request.
-
The
flash.now[:alert]
message is displayed without persisting across requests.
-
The
head
method in Rails is used to send responses containing only HTTP headers without a response body. It accepts:-
An HTTP status code (number or symbol)
-
An optional hash of header names and values
-
Sending an Error Header
head :bad_request
Response Headers:
HTTP/1.1 400 Bad Request
Connection: close
Date: <timestamp>
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
X-Runtime: <execution_time>
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
Sending a Header with Location
head :created, location: photo_path(@photo)
Response Headers:
HTTP/1.1 201 Created
Connection: close
Date: <timestamp>
Transfer-Encoding: chunked
Location: /photos/1
Content-Type: text/html; charset=utf-8
X-Runtime: <execution_time>
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
Key Takeaways
-
head
allows sending HTTP headers without a response body. -
Supports HTTP status codes (e.g.,
:bad_request
,:created
). -
Can include additional headers like location.
-
Useful for API responses, redirects, and error handling.
-
When Rails renders a view, it combines the view with the current layout. Three primary tools help structure layouts:
-
Asset Tags
-
yield and
content_for
-
Partials
-
- Asset tag helpers generate HTML linking views to various assets like JavaScript, stylesheets, images, videos, and audios. These do not verify asset existence but assume correctness.
Available Asset Tag Helpers
-
auto_discovery_link_tag
(for RSS, Atom, JSON feeds) -
javascript_include_tag
(for JavaScript files) -
stylesheet_link_tag
(for CSS files) -
image_tag
(for images) -
video_tag
(for videos) -
audio_tag
(for audios)
<%= auto_discovery_link_tag(:rss, {action: "feed"}, {title: "RSS Feed"}) %>
-
Options:
-
:rel
- Default is "alternate". -
:type
- Explicit MIME type. -
:title
- Defaults to uppercase type (e.g., "ATOM", "RSS").
<%= javascript_include_tag "main" %>
-
Uses the Asset Pipeline to serve files from
app/assets
,lib/assets
, orvendor/assets
. -
Multiple files can be included:
<%= javascript_include_tag "main", "columns" %>
- External JavaScript:
<%= javascript_include_tag "http://example.com/main.js" %>
<%= stylesheet_link_tag "main" %>
-
Uses the Asset Pipeline.
-
Multiple stylesheets:
<%= stylesheet_link_tag "main", "columns" %>
- External CSS:
<%= stylesheet_link_tag "http://example.com/main.css" %>
- `Media-specific stylesheets:
<%= stylesheet_link_tag "main_print", media: "print" %>
<%= image_tag "header.png" %>
-
Default directory:
public/images
. -
Supports paths and additional options:
<%= image_tag "icons/delete.gif", height: 45 %>
-
Alternative text defaults to the capitalized filename without an extension.
-
Supports
size
:
<%= image_tag "home.gif", size: "50x20" %>
- HTML attributes:
<%= image_tag "home.gif", alt: "Go Home", id: "HomeImage", class: "nav_bar" %>
<%= video_tag "movie.ogg" %>
-
Default directory:
public/videos
. -
Options:
-
poster
: Placeholder image before playing. -
autoplay: true
: Auto-plays video. -
loop: true
: Loops video. -
controls: true
: Shows video controls.
-
<%= video_tag "movie.ogg", controls: true, autoplay: true, loop: true %>
- Multiple video sources:
<%= video_tag ["trailer.ogg", "movie.ogg"] %>
<%= audio_tag "music.mp3" %>
-
Default directory:
public/audios
. -
Options:
-
autoplay: true
: Auto-plays audio. -
controls: true
: Shows audio controls.
-
<%= audio_tag "music.mp3", controls: true, autoplay: true %>
-
yield
is used in Rails layouts to define sections where content from views will be inserted. -
The simplest usage of
yield
places the entire contents of a view into a layout:
<html>
<head>
</head>
<body>
<%= yield %>
</body>
</html>
Using Multiple yield
Regions
- You can create multiple yielding regions by using named
yield
placeholders:
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
-
The main body of the view will always render into the unnamed
yield
. -
Named yield regions can be populated using
content_for
:
<% content_for :head do %>
<title>My Page</title>
<% end %>
Default Rails Layout
- Newly generated Rails applications include
<%= yield :head %>
within the element ofapp/views/layouts/application.html.erb
.
- The
content_for
method allows you to insert content into a namedyield
block within a layout. This is useful for structuring distinct regions in a layout, such as sidebars, footers, or the<head>
section.
<% content_for :head do %>
<title>A simple page</title>
<% end %>
<p>Hello, Rails!</p>
- Corresponding Layout Rendering:
<html>
<head>
<title>A simple page</title>
</head>
<body>
<p>Hello, Rails!</p>
</body>
</html>
Benefits of content_for
-
Enables inserting page-specific content into layouts.
-
Useful for adding:
-
Page-specific <title> elements.
-
JavaScript <script> elements.
-
CSS elements.
-
Context-specific elements.
-
-
Helps manage structured sections like sidebars and footers efficiently.
When to Use
-
When different pages need to insert unique content into a predefined layout region.
-
When you need to override or append content within a layout dynamically.
-
Partials are named with a leading
underscore
(e.g., _menu.html.erb). -
Render a partial using:
<%= render "menu" %>
- For partials in another folder:
<%= render "application/menu" %>
- Located in
app/views/application/_menu.html.erb
.
- Used to break down views into smaller components.
<%= render "application/ad_banner" %>
<h1>Products</h1>
<%= render "application/footer" %>
- Can be used with
yield
to clean up layouts.
- A partial can have its own layout:
<%= render partial: "link_area", layout: "graybar" %>
-
Layouts for partials follow the same leading underscore convention.
-
Located in the same folder as the partial.
- Pass variables to a partial using locals:
<%= render partial: "form", locals: {zone: @zone} %>
- Example using
local_assigns
:
<%= render article, full: true %>
<% if local_assigns[:full] %>
<%= simple_format article.body %>
<% else %>
<%= truncate article.body %>
<% end %>
- Implicit local variable:
<%= render partial: "customer", object: @new_customer %>
customer
in_customer.html.erb
refers to@new_customer
.
<%= render @customer %>
- Pass a collection to a partial:
<%= render partial: "product", collection: @products %>
<%= render @products %>
- Rails selects the correct partial for mixed collections:
<%= render [customer1, employee1, customer2, employee2] %>
- Handle empty collections:
<%= render(@products) || "There are no products available." %>
- Change local variable name:
<%= render partial: "product", collection: @products, as: :item %>
- Pass additional local variables:
<%= render partial: "product", collection: @products, as: :item, locals: {title: "Products Page"} %>
- Rails provides a counter variable for collections:
<%= product_counter %> # 0 for first, 1 for second, etc.
- If using
as: :item
, the counter will beitem_counter
.
- Insert a spacer template between collection items:
<%= render partial: @products, spacer_template: "product_ruler" %>
- Apply a layout to each item in a collection:
<%= render partial: "product", collection: @products, layout: "special_layout" %>
-
Nested layouts (or sub-templates) allow a controller-specific layout that extends the main application layout without duplication.
-
Application Layout (application.html.erb)
<html>
<head>
<title><%= @page_title || "Page Title" %></title>
<%= stylesheet_link_tag "layout" %>
<%= yield :head %>
</head>
<body>
<div id="top_menu">Top menu items here</div>
<div id="menu">Menu items here</div>
<div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
</body>
</html>
- News Layout (news.html.erb)
<% content_for :head do %>
<style>
#top_menu {display: none}
#right_menu {float: right; background-color: yellow; color: black}
</style>
<% end %>
<% content_for :content do %>
<div id="right_menu">Right menu items here</div>
<%= content_for?(:news_content) ? yield(:news_content) : yield %>
<% end %>
<%= render template: "layouts/application" %>
Key Points
-
Avoid layout duplication by reusing the
application.html.erb
layout. -
Modify layouts selectively (e.g., hiding the top menu and adding a right menu for
NewsController
). -
Use
content_for
to define and override specific sections like:head
and:content
. -
Render base layout using
<%= render template: "layouts/application" %>
. -
Nested templating possible using
render template: 'layouts/news'
for further layout customizations.