Wednesday, 22 October 2008

Rendering and Layouts

I'm assuming that most people reading this are .Net folk and that you will have at least a passing familiarity with ASP.Net, so occasionally I'll draw comparisons with Microsoft's framework. ASP.Net has had, since 2.0, the concept of Master pages. These are pages that provode the overall structure of parts of a web site. ASP.Net lets a page author design a template that contains content placeholders, this template is called a 'master page', individual pages can then specify that they want to use a specific master page and also specify which content replaces which placeholder within the page. Internally this turns out to be a fairly convoluted mechanism as the master page and the content page have to be woven together so that the right events get delivered to the right thing at the right time.

Rails, as you would expect, has a similar mechanism which known as 'layouts'.

In a Rails app, content is defined in a template. If you use the default generators of Rails 2.0 these templates are in the form of .erb files. A .erb file is essentially an HTML page with "turd-lets" of code embedded into it between <% %> and <%= %> symbols. This is just like other templating technologies such as ASP, ASP.Net, JSP or PHP

Templates are part of the view (obviously) and a typical template renders content for an action. For example in the rblog application there is a BlogsController class this has an index action with an associated index template. Of course none of this is mandatory, this is though the default behaviour if you use the scaffolding. The templates live in app/views and this index template lives in app/views/blogs/index.html.erb

The template looks something like this:

<h1>Latest Output</h1>


<div id='blog-entries-main-body'>
	<div id='blog-entries'>
		<% for blog_entry in @blog_entries %>
			<div class='blog-entry-surround'>
			    <span class="blog-title"><%=h blog_entry.title %></span>
			    <div><%=h blog_entry.entry %></div>
			    <span class="blog-author"><%=h blog_entry.author_id %></span>
  			</div>
		<% end %>
	</div>
</div>

The template is a mixture of HTML and ruby. This Ruby code iterates over a collection of blog_entries (stored in @blog_entries) and for each one formats some output.

The action that causes this particular view to be rendered looks like:

def index
    @blog_entries = BlogEntry.find(:all, :order => 'updated_at DESC', :limit => 10)
    @blogs = Blog.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @blogs }
    end
  end

The first thing to note is that there are two member variables initialized, @blogs and @blog_entries. These are initialized in the controller and are also available in the view, that's where the view gets its @blog_entries reference from.

The other piece of interest is the respond_to section. This says that if the format is html use the default rendering (the '#' is a comment, so the 'index.html.erb' on that line is there as an aide memoire), and if the format is XML then use the XML renderer.

This means that when a request comes into /blogs then the index action of the blogs controller is executed, the @blogs and @blog_entries objects are created, control passes to the view and the output is rendered. However the output looks like this...

200810170814.jpg

There is some extra stuff in here, for example there is styling and also things like a Register and Login button. This extra HTML comes from the layouts.

The layouts live in the app/layouts directory and by default there is one layout per controller, this is generated as part of the scaffolding. This means that if nothing explicit is done a default layout will be used, however there are various other ways to specify the layout needed, as will be seen.

The layout used for the blogs page looks like this

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>RBlog: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'general' %>
</head>
<body>
<div class="header" id="header">
	<div id="header-message">
		Welcome to RBlog
	</div>
	<div id="flash">
		<p><%= flash[:notice] %></p>
	</div>
	<div id="logon">
		<%= link_to "Register", :action => "register", :controller => "authenticate"  %> 
		<%= link_to "Login", :action => "login", :controller => "authenticate" %>
	</div>
</div>

<div class="main" id="main">
<%= yield  %>
</div>
<div class="sidebar" id="sidebar">
	<div id="bloglist">
		<div id="blogs-list-title">Blogs</div>
		<% for blog in @blogs %>
		    <div class="blog-title"><%= link_to h(blog.title), blog %></div>
		<% end %>
	</div>
</div>

<div class="footer" id="footer">
	
</div>

</body>
</html>

The highlights are:

  • The use of <%= controller.action_name %> to get the name of the action used to show this page
  • <%= stylesheet_link_tag 'general' %> to load a stylesheet, more than one can be specified
  • The use if 'flash' to display messages
  • The use of link_to to display URLs
  • Access to data members from the controller (for blog in @blogs)
  • The call to yield

It's the last one that's most interesting here. It's the call to yield that says 'take the default rendered output from the view and display it at this point in the page'. One thing to note here is that the view 'code' has already been executed at this point, i.e. the view template is rendered, that output is saved, then the layout is executed and the output from the template inserted into the layout.

This layout is called 'blogslisting.html.erb' so is not the default layout for the blogs controller, this means that the code has to explicitly specify this layout to use. Like many things in Rails there is a great deal of flexibility in doing this.

A global or default layout can be specified in app/controllers/application.rb , this contains the base class that all controllers derive from. A 'layout' declaration can be added In the class definition.

class ApplicationController < ActionController::Base
  layout "general"

A layout can also be specified per controller

class BlogsController < ApplicationController

  layout "blogslisting"

And this can be further overwritten on a per action basis. So if a given controller wants different layouts for each action it can have code something like this:

  def show
    
    @blog = Blog.find(params[:id])

    respond_to do |format|
      format.html {render(:layout => "layouts/blog" )}
      format.xml  { render :xml => @blog }
    end
  end

Where the code in the format.html block says to render with the layout with the 'layouts/blogs' layout file.

This piece is already much longer than I though it would be so it's time to stop. There's more to be said about this, such as getting multiple content into the layout (multiple calls to yield), and sharing content with partial page templates. More on that soon.

Posted by kevin at 7:07 AM in Ruby

« October »
SunMonTueWedThuFriSat
   1234
567891011
12131415161718
19202122232425
262728293031