So far, each of our views has had an <html>
tag and a <body>
tag. By using a "layout", we can extract common code that is used for several view templates to one location. We do this by creating an application.html.erb
file.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>BlogApp</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
"Common code" usually includes the <html>
, <head>
, and <body>
tags.
Notice that we also have HTML tag helpers from the chapter on unobtrusive scripting. We make sure to include those in our layout as well. These should be included so any additions or changes to CSS and Javascript will be included in our application.
You can define and use other layouts, but layouts/application.html.erb
is the default, so that's what we create here. Rails will actually generate a layout similar to the one above whenever you run $ rails new
to generate a new app.
yield
identifies where content from our view should be inserted into the enclosing layout.
Typically, you'll have a layout for most view templates, so as to cut out code duplication.
We've shown that it is possible to insert code from a view template into a layout, and
then render that layout and view together to the user. We can even go a step further,
it is possible to move certain HTML/ERB code that is reused in various view templates to one location. This is called a partial view template or just "partial". Let's move some redundant
HTML/ERB code to a partial.
The convention we want to follow is to create a partial in the same view folder as the resource it is used for. So, if this is a partial for posts, then we'll save the partial view template file under the views/posts
folder. One other convention that is important to remember is that partial view templates start with an underscore. We do this to help differentiate partials from other view templates. First, let's first take a look at the two view templates we are working with, posts/edit.html.erb
and posts/new.html.erb
.
<!-- app/views/posts/new.html.erb -->
<div class="errors">
<% @post.errors.each do |attribute, error| %>
<p class="error" style="color: orange">
<%= attribute %>: <%= error %>
</p>
<% end %>
</div>
<%= form_tag posts_path, method: "post" do %>
<%= label_tag 'title' %>
<%= text_field_tag 'post[title]', @post.title %>
<br /> <br />
<%= label_tag 'body' %>
<%= text_area_tag 'post[body]', @post.body %>
<br /> <br />
<%= label_tag 'author' %>
<%= text_field_tag 'post[author]', @post.author %>
<br /> <br />
<%= submit_tag "Create Post" %>
<% end %>
<!-- app/views/posts/edit.html.erb -->
<div class="errors">
<% @post.errors.each do |attribute, error| %>
<p class="error" style="color: orange">
<%= attribute %>: <%= error %>
</p>
<% end %>
</div>
<%= form_tag url_for(action: 'update'), method: "patch" do %>
<%= label_tag 'Title' %>
<%= text_field_tag 'post[title]', @post.title %>
<br /> <br />
<%= label_tag 'Body' %>
<%= text_area_tag 'post[body]', @post.body %>
<br /> <br />
<% label_tag 'Author' %>
<%= text_field_tag 'post[author]', @post.author %>
<br /> <br />
<%= submit_tag 'Update Post' %>
<% end %>
<br />
<%= my_link_to 'Back to Post', posts_path %>
Let's look at what similarities these two views contain:
We can address each of these commonalities by extracting them to their own partial views. Let's tackle the one for error messages first.
<!-- app/views/posts/edit.html.erb -->
<%= render "errors" %>
<!-- ... -->
<!-- app/views/posts/new.html.erb -->
<%= render "errors" %>
<!-- ... -->
<!-- app/views/posts/_errors.html.erb -->
<div class="errors">
<% @post.errors.each do |attribute, error| %>
<p class="error" style="color: orange">
<%= attribute %>: <%= error %>
</p>
<% end %>
</div>
By using the code above, we're able to move any error related code to one place. Then,
we can use that code in any view template within the same directory as _errors
by calling
render "errors"
. Now, if we wanted to make our error partial usable for multiple resources,
we could store it in a separate directory. Let's say we do that, and now _errors.html.erb
is now in a directory called shared
. If we want to still use that partial in new
and edit
view template for posts, then we'll have to provide the relative path to that file, starting from the views
directory: <%= render "shared/errors" %>
.
The above partial would then look like this:
<!-- app/views/shared/_errors.html.erb -->
<div class="errors">
<% obj.errors.each do |attribute, error| %>
<p class="error" style="color: orange">
<%= attribute %>: <%= error %>
</p>
<% end %>
</div>
And when we call render
for that errors partial, we'll have to specify what the local obj
is:
<!-- For Posts -->
<%= render 'shared/errors', obj: @post %>
<!-- For Comments -->
<%= render 'shared/errors', obj: @comment %>
Next, let's work on view template similarity number 2, the fields for our two forms.
Note, that our two forms for edit.html.erb
and new.html.erb
go to different
paths. For the time being we'll only extract the fields for these forms to a partial.
But, later on we'll be able to extract the entire form.
<!-- app/views/posts/new.html.erb -->
<%= render "shared/errors", obj: @post %>
<%= form_tag posts_path, method: "post" do %>
<%= render "form_fields" %>
<% end %>
<br />
<%= link_to "Back to Posts", posts_path %>
<!-- app/views/posts/edit.html.erb -->
<%= render "shared/errors", obj: @post %>
<%= form_tag url_for(action: 'update'), method: "patch" do %>
<%= render "form_fields" %>
<% end %>
<br />
<%= my_link_to 'Back to Post', posts_path %>
<!-- app/views/posts/_form_fields.html.erb -->
<%= label_tag 'Title' %>
<%= text_field_tag 'post[title]', @post.title %>
<br /> <br />
<%= label_tag 'Body' %>
<%= text_area_tag 'post[body]', @post.body %>
<br /> <br />
<%= label_tag 'Author' %>
<%= text_field_tag 'post[author]', @post.author %>
<br /> <br />
<%= submit_tag "#{@post.new_record? ? 'Create' : 'Update'} Post" %>
So far we have seen that we can render partials by calling <%= render "path/to/partial" %>
. There is actually one other way to call render
, and that is by passing a hash to render
.
<%= render { partial: 'shared/errors' } %>
The code above would give us the same result as calling: <%= render 'shared/errors' %>
In the controller, we may specify a different layout (instead of the default one, application.html.erb
) for our views. It's also possible to set a specific layout for all actions in a controller. We do this by using the layout macro:
layout 'layout_file_name', options
As an example, what if we wanted to use a different layout for all actions in our
PostsController
. First, we would have to create a new layout file and store it
under the views/layouts
folder. Then, all that needs to be done is to specify
that file in PostsController
.
class PostsController < ApplicationController
# ...
layout 'file_name'
# ...
end
Pretty useful. But we may want to only use this new layout file in certain actions, not all of them. We have two ways to go about that. One way would be to specify which actions to apply the layout to via our options hash. Code such as the following is perfectly valid:
layout 'file_name', only: :index
Or, if we want specify more than one action:
layout 'file_name' only: [:index, :new]
If we don't want to specify which actions apply for this layout, we can instead specify which ones we don't want by using the syntax except:
instead of only:
layout 'file_name' except: [:edit, :create, :update, :destroy]
Finally, we may also specify the layout in the action itself. Recall that in these
actions we have been specifying which view template to render, e.g. render :edit
.
render
can take a layout key as part of its options hash.
render :edit, layout: 'file_name'
In Rails, the flash message is a convenient way to show messages on a page, typically as confirmation or error message after user actions.
Let's add flash messages to our app for the create
action of the CommentsController
:
def create
@comment = @post.build_comment(params[:comment])
if @comment.save
redirect_to post_path(@post.id)
else
render 'posts/show'
end
end
Becomes:
def create
@comment = @post.build_comment(params[:comment])
if @comment.save
flash[:success] = "You have successfully created the comment."
redirect_to post_path(@post.id)
else
flash.now[:error] = "Comment couldn't be created. Please check the errors."
render 'posts/show'
end
end
You can consider flash as a hash that can store messages which can then be pulled out in the next HTTP request. This is why we added flash[:success]
before the redirection. Flash messages are automatically deleted after the next request.
If we want the messages to be available to the current request, we can use the flash.now
hash instead, like we did for the error path.
We'll also need to put the flash messages in the view. In fact we're going to put the snippet to handle flash messages in a partial and render it from the application
layout.
Let's do that now.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>BlogApp</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= render 'layouts/messages' %>
<%= yield %>
</body>
</html>
<!-- app/views/layouts/_messages.html.erb -->
<% flash.each do |name, message| %>
<% if message.is_a? String %>
<p style="color: <%= name == 'success' ? 'green' : 'red' %>">
<%= message %>
</p>
<% end %>
<% end %>
There, now we may include a flash message in our view templates. This is a feature that is
available throughout our entire app. That call to <%= render 'layouts/messages' %>
makes it all possible.