Ruby Templates Tutorial
Overview
- The Role of Templates
- Accessing CGI Data in Ruby
- Critical Considerations with CGI Data in Ruby
- Template Example
- Ruby Web Hosting
- Tutorial Details
The Role of Templates
- Templates, in the context of dynamic websites, are the XHTML page structure with most of the content removed.
- The content is stored in a separate location, which enables structure, content, presentation, and behavior to be separate.
- Dynamic websites tend to be large and the use of templates allows for easy maintenance and redesign, because content and structure are separate.
- There can also be a variety of templates (2 column, 3 column, etc.) for a website and the choice of template could be as simple as setting a variable.
Accessing CGI Data in Ruby
- Ruby uses the long-established CGI (Common Gateway Interface) for retrieving POST and GET data.
- The process for getting access to CGI functionality is to first pull in the CGI.rb module as a mixin:
require 'cgi'
and then instantiate an object based on the CGI class:
cgi = CGI::new
or via a dot notation:
cgi = CGI.new
- The object created has access to POST and GET data by their variable name, so accessing the 'selection' parameter is:
the_choice = cgi['selection']
- The method noted previously works well for scalar (single) values; for variables with multiple values the
paramsmethod returns an array of those values. Assuming that 'many_selections' had multiple values:the_many_choices = cgi::params['many_selections']
the_many_choiceswould then be an array.You can also use
paramsin other contexts, as it returns cgi data as a hash.If we wanted to clear all parameters, we could specify:
cgi::params::clear
Or we can pinpoint a parameter via
delete, as in:cgi::params::delete('the_many_choices') - Any CGI object also has access to the traditional HTTP variables as read-only properties (most are strings;
content_lengthandserver_portare integers; HTTP_ has been removed, and they are lowercase):accept, accept_charset, accept_encoding, accept_language, auth_type, cache_control, content_length, content_type, from, gateway_interface, host, negotiate, path_info, path_translated, pragma, referer, remote_ident, remote_user, request_method, query_string, remote_addr, remote_host, script_name, server_name, server_port, server_protocol, server_software, user_agent - For passing HTTP headers to the browser there is a
header()static method; consult the RDOC Documentation for details.- When passing headers it is critical to not have any white space being output / passed, including empty lines at the end of included / imported files (after the Ruby code is finished).
- That empty line is viewed as content, so no headers can be sent after that point.
- Use a text editor that allows you to view line numbers and make sure there are no empty lines before or after your Ruby code in any of the files.
- If you are getting an error about headers already being sent, this is most likely the source of the error.
- For data cleanup there is an
escapeHTML()static method that accepts a string and converts&"<>to their special character equivalent. - When the situation calls for greater precision than
escapeHTML()offers, useescapeElement()which accepts multiple parameters. The first parameter is the string to change and any subsequent parameters are tag names to convert to special characters in that string:modified_string = CGI::escapeElement(original_string,"SCRIPT","STYLE")
Any non-inline JavaScript or embedded CSS would be disabled in the
modified_stringcode. Any quotes inside the tags, such as for their attributes, would also be converted to their special character equivalent.
Critical Considerations with CGI Data in Ruby
- A CGI object can only access GET or POST data, not both data streams.
- If you are using POST only the first CGI object gets the data; any subsequent CGI objects have no data in them.
- If you are using GET then every CGI object you instantiate has the GET data.
- If you POST from a form and try to tack GET parameters to the end of the file called as part of a form tag's
actionattribute (e.g.,action="results.rhtml?userid=5") then the POST data is used and the GET data is ignored. - When a file is brought in via
ERuby::import()its local variables cannot be accessed in the document that performed the import.- Typically you created a local CGI variable in the imported file and now cannot access it.
- Changing that CGI variable in the imported file to being global is one approach that would work.
- Once you move your code to a live web server save your files as Unix format rather than Windows/PC or Mac format.
- The Unix format uses newline characters that CGI understands.
- The other formats run the risk of the newlines not being recognized which means your script is all on one line as far as the interpreter is concerned.
- This invariably breaks the script and you will find eRuby prompting you to download a CGI file type, which is the clearest indication that the newlines are not working.
- Also be sure to transfer your files to that server as ASCII/text and not as binary, for similar reasons of keeping code uncorrupted.
Template Example
- This template uses eRuby, which allows Ruby to be embedded in XHTML. Files are named with a .rhtml extension.
- View the Ruby template example
- Download the Ruby template example code (zip archive, 8k)
- The file and directory structure of the example is:
/template directory/ index.rhtml (the template file) hometext.rhtml (included data file for home page content) filenotfound.rhtml (data file pulled in if the URL passed does not exist) screen.css (styling for screen display) /classes sub-directory/ template_class.rhtml (included file defining a custom class and methods) /news sub-directory/ news.txt (news items for the home page) /gallery sub-directory/ gallery1.rhtml (data file) gallery2.rhtml (data file) gallery3.rhtml (data file) gallery4.rhtml (data file) ourgalleries.rhtml (data file) /trial sub-directory/ details.rhtml (data file) signupnow.rhtml (data file) testimonials.rhtml (data file) whysignup.rhtml (data file) - The layout template (index.rhtml) file contains:
#!/usr/bin/eruby <% ERuby::import('classes/template-class.rhtml') page = TemplatePage::new # set the 'file_to_load' parameter page::determine_load_path # pull in the data file from the template so that # the local 'page' variable in the included file is in scope ERuby::import("#{page.file_to_load}") %><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title><% page::print_title %></title> <meta name="description" content="<% page::print_meta %>" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" type="text/css" href="screen.css" media="screen,projection" /> </head> <body> <h1>Sample Client</h1> <% # output the 'text' property page::print_content # output the 'sidecolumn' property page::print_sidecolumn # output the global nav page::print_globalnav # output the local navigation bar # or the news if it is the home page page::print_localnav_news %> </body> </html> - The template_class.rhtml file contains:
<% # pull in the CGI data require 'cgi' class TemplatePage attr_accessor :title, :desc, :text, :sidecolumn, :file_to_load def initialize # property: global nav items (array containing hashes) @globalnav = [ {:id => 'home', :label => 'Home', :url => '?p=home', :tooltip => 'Return to the home page'}, {:id => 'gallery', :label => 'Sample Gallery', :url => '?p=gallery-ourgalleries', :tooltip => 'Various gallery examples'}, {:id => 'trial', :label => 'Free Trial', :url => '?p=trial-signupnow', :tooltip => 'Sign up for a free trial'} ] # property: local nav items (hash of arrays) @localnav = { 'gallery' => ['Our Galleries','Gallery 1','Gallery 2', 'Gallery 3', 'Gallery 4'], 'trial' => ['Sign Up Now','Why Sign Up?','Details','Testimonials'] } end # method: output title def print_title print @title end # method: output description meta data def print_meta print @desc end # method: output main content def print_content print '<div id="content">', @text, '</div>' end # method: output side content def print_sidecolumn print "\n\n" print '<div id="related">' if @validpage print @sidecolumn end print '</div>' end # method: load data file and set appropriate flags def determine_load_path @cgi = CGI::new # if a 'p' parameter was passed clean up its data # and then explode the string into an array # the filepath property is the array unless @cgi['p']::nil? cleaned_param = clean_data(@cgi['p']) @filepath = cleaned_param::split('-') # later code assembles a file path and in cases # of an invalid 'p' value being passed there might not be # a hyphen so this provides a second value and allows # the file path to result in a 'File Not Found' # rather than derailing Ruby with a nil for [1] if @filepath[1]::nil? then @filepath[1] = 'no' end end # if no 'p' parameter was passed or 'p' is 'home' # you are on the home page if (@cgi['p']::empty? || @cgi['p'] == 'home') @homepage = true @validpage = true @filepath[0] = 'home' @file_to_load = 'hometext.rhtml' # otherwise you are on a sub page else # boolean property for tracking if you are on the home page @homepage = false if @filepath[0] && @filepath[1] # this is the path to the file, based on the 'p' value # for concatenation use + rather than << # because << in this situation edits the item to the left @file_to_load = @filepath[0] + '/' + @filepath[1] + '.rhtml' else @file_to_load = 'invalid' end # first check to see if a data file exists at that location # if it does not, load the file not found page unless File::exists? @file_to_load @validpage = false @filepath[0] = 'filenotfound' @file_to_load = 'filenotfound.rhtml' # otherwise we have a valid page else @validpage = true end end end # method: output global nav def print_globalnav print "\n\n", '<ul id="globalnav">', "\n" @globalnav::each do |item| # unlink if in that section if @filepath[0] == item[:id] print '<li><strong>', item[:label], '</strong></li>', "\n" # link page otherwise else print '<li><a href="', item[:url], '" title="', item[:tooltip], '">', item[:label], '</a></li>', "\n" end end print '</ul>', "\n\n" end # method: output local nav or news area (for home page) def print_localnav_news # check the @homepage flag if it is true then display the news if @homepage ERuby::import('news/news.txt') # otherwise if you have a valid page output local navigation # the File Not Found scenario has @validpage set to false so no local nav elsif @validpage # the first segment of the 'p' value is the key in the local nav hash key = @filepath[0] print '<ul id="subnav">', "\n" @localnav[key]::each do |item| # create a variable that is a lowercase version # also remove objectionable characters # 'Why Sign Up?' is now 'whysignup' adjusted_nav = item::downcase::gsub(/\s/,'')::gsub(/\?/,'') # unlink/boldface if the second part of the URL # matches the modified array value if @filepath[1] == adjusted_nav print '<li><strong>', item, '</strong></li>', "\n" # otherwise link the item else print '<li><a href="?p=', @filepath[0], '-', adjusted_nav, '">', item, '</a></li>', "\n" end end print '</ul>', "\n" end end # method: remove any malicious characters from the GET data def clean_data(data) return data::gsub(/http:|ftp:|\.|\//i,'') end end %> - The hometext.rhtml file contains properties that are printed out in the template:
<% page.title = <<_DOCTITLE_ Sample Client Websites - Lots of Samples! _DOCTITLE_ page.desc = <<_METADESC_ Sentence about sample client websites _METADESC_ page.text = <<_PAGECONTENT_ <h2>Welcome to Sample Client Websites</h2> <p>We have a wide variety of:</p> <ul> <li>Internet Samples</li> <li>Intranet Samples</li> <li>Extranet Samples</li> </ul> _PAGECONTENT_ page.sidecolumn = <<_SIDECOL_ <h3>See Also</h3> <ul> <li>Website 1</li> <li>Website 2</li> <li>Website 3</li> </ul> _SIDECOL_ %>
Ruby Web Hosting
- Once you have the template functional you will need to make sure that your web hosting company supports what you have built.
- Web hosting for Ruby (eRuby as well as Ruby on Rails) is not as common as hosting for PHP or ASP.Net
Tutorial Details
- Author: Jason Withrow (jason@usabledevelopment.com)
- Last Updated: September 21st, 2009
- URL: http://www.usabledevelopment.com/rubytemplates