7 Templates: Separation of View
(require web-server/templates) |
The Web Server provides a powerful Web template system for separating the presentation logic of a Web application and enabling non-programmers to contribute to Racket-based Web applications.
Although all the examples here generate HTML, the template language and the Text Preprocessing it is based on can be used to generate any text-based format: C, SQL, form emails, reports, etc.
7.1 Static
<html> |
<head><title>Fastest Templates in the West!</title></head> |
<body> |
<h1>Bang!</h1> |
<h2>Bang!</h2> |
</body> |
</html> |
7.2 Dynamic
<html> |
<head><title>Fastest @thing in the West!</title></head> |
<body> |
<h1>Bang!</h1> |
<h2>Bang!</h2> |
</body> |
</html> |
(let ([thing "Templates"]) |
(include-template "simple.html")) |
(define (fast-template thing) |
(include-template "simple.html")) |
(fast-template "Templates") |
(fast-template "Noodles") |
<html> |
<head><title>Fastest Templates in the West!</title></head> |
<body> |
<h1>Bang!</h1> |
<h2>Bang!</h2> |
</body> |
</html> |
and
<html> |
<head><title>Fastest Noodles in the West!</title></head> |
<body> |
<h1>Bang!</h1> |
<h2>Bang!</h2> |
</body> |
</html> |
Furthermore, there are no constraints on the Racket used by templates: they can use macros, structs, continuation marks, threads, etc. However, Racket values that are ultimately returned must be printable by the Text Preprocessing. For example, consider the following outputs of the title line of different calls to fast-template:
- (fast-template 'Templates)
...<title>Fastest Templates in the West!</title>...
- (fast-template 42)
...<title>Fastest 42 in the West!</title>...
...<title>Fastest Noodles in the West!</title>...
...<title>Fastest Thunks in the West!</title>...
...<title>Fastest Laziness in the West!</title>...
- (fast-template (fast-template "Embedding"))
...<title>Fastest ...<title>Fastest Embedding in the West!</title>... in the West!</title>...
7.3 Gotchas
<head><title>Fastest @s in the West!</title></head> |
<head><title>Fastest @"@"s in the West!</title></head> |
<head><title>Fastest @thing in the @place!</title></head> |
<head><title>Fastest @thing in the @|place|!</title></head> |
<table> |
@for[([c clients])]{ |
<tr><td>@(car c), @(cdr c)</td></tr> |
} |
</table> |
<table> |
</table> |
<table> |
@for/list[([c clients])]{ |
<tr><td>@(car c), @(cdr c)</td></tr> |
} |
</table> |
<table> |
</tr> |
</tr> |
</table> |
<table> |
@for/list[([c clients])]{ |
@list{ |
<tr><td>@(car c), @(cdr c)</td></tr> |
} |
} |
</table> |
<table> |
<tr><td>Young, Brigham</td></tr> |
<tr><td>Smith, Joseph</td></tr> |
</table> |
<table> |
@in[c clients]{ |
<tr><td>@(car c), @(cdr c)</td></tr> |
} |
</table> |
7.4 HTTP Responses
(response/full |
200 #"Okay" |
(current-seconds) TEXT/HTML-MIME-TYPE |
empty |
(list (string->bytes/utf-8 (include-template "static.html")))) |
7.5 API Details
(include-template path) |
(in x xs e ...) |
@in[c clients]{ |
<tr><td>@(car c), @(cdr c)</td></tr> |
} |
7.6 Conversion Example
Al Church has been maintaining a blog with Racket for some years and would like to convert to web-server/templates.
(define-struct post (title body)) |
(define posts |
(list |
(make-post |
"(Y Y) Works: The Why of Y" |
"Why is Y, that is the question.") |
(make-post |
"Church and the States" |
"As you may know, I grew up in DC, not technically a state."))) |
He has divided his code into presentation functions and logic functions. We’ll look at the presentation functions first.
(define (template section body) |
(response/xexpr |
`(html |
(head (title "Al's Church: " ,section)) |
(body |
(h1 "Al's Church: " ,section) |
(div ([id "main"]) |
,@body))))) |
One of the things to notice here is the unquote-splicing on the body argument. This indicates that the body is list of X-expressions. If he had accidentally used only unquote then there would be an error in converting the return value to an HTTP response.
(define (blog-posted title body k-url) |
`((h2 ,title) |
(p ,body) |
(h1 (a ([href ,k-url]) "Continue")))) |
Here’s an example of simple body that uses a list of X-expressions to show the newly posted blog entry, before continuing to redisplay the main page. Let’s look at a more complicated body:
(define (blog-posts k-url) |
(append |
(apply append |
(for/list ([p posts]) |
`((h2 ,(post-title p)) |
(p ,(post-body p))))) |
`((h1 "New Post") |
(form ([action ,k-url]) |
(input ([name "title"])) |
(input ([name "body"])) |
(input ([type "submit"])))))) |
This function shows a number of common patterns that are required by X-expressions. First, append is used to combine different X-expression lists. Second, apply append is used to collapse and combine the results of a for/list where each iteration results in a list of X-expressions. We’ll see that these patterns are unnecessary with templates. Another annoying patterns shows up when Al tries to add CSS styling and some JavaScript from Google Analytics to all the pages of his blog. He changes the template function to:
(define (template section body) |
(response/xexpr |
`(html |
(head |
(title "Al's Church: " ,section) |
(style ([type "text/css"]) |
"body {margin: 0px; padding: 10px;}" |
"#main {background: #dddddd;}")) |
(body |
(script |
([type "text/javascript"]) |
,(make-cdata |
#f #f |
"var gaJsHost = ((\"https:\" ==" |
"document.location.protocol)" |
"? \"https://ssl.\" : \"http://www.\");" |
"document.write(unescape(\"%3Cscript src='\" + gaJsHost" |
"+ \"google-analytics.com/ga.js' " |
"type='text/javascript'%3E%3C/script%3E\"));")) |
(script |
([type "text/javascript"]) |
,(make-cdata |
#f #f |
"var pageTracker = _gat._getTracker(\"UA-YYYYYYY-Y\");" |
"pageTracker._trackPageview();")) |
(h1 "Al's Church: " ,section) |
(div ([id "main"]) |
,@body))))) |
Some of these problems go away by using here strings, as described in the documentation on Reading Strings.
The first thing we notice is that encoding CSS as a string is rather primitive. Encoding JavaScript with strings is even worse for two reasons: first, we are more likely to need to manually escape characters such as "; second, we need to use a CDATA object, because most JavaScript code uses characters that "need" to be escaped in XML, such as &, but most browsers will fail if these characters are entity-encoded. These are all problems that go away with templates.
(define (extract-post req) |
(define binds |
(request-bindings req)) |
(define title |
(extract-binding/single 'title binds)) |
(define body |
(extract-binding/single 'body binds)) |
(set! posts |
(list* (make-post title body) |
posts)) |
(send/suspend |
(lambda (k-url) |
(template "Posted" (blog-posted title body k-url)))) |
(display-posts)) |
(define (display-posts) |
(extract-post |
(send/suspend |
(lambda (k-url) |
(template "Posts" (blog-posts k-url)))))) |
(define (start req) |
(display-posts)) |
To use templates, we need only change template, blog-posted, and blog-posts:
(define (template section body) |
(response/full |
200 #"Okay" |
(current-seconds) TEXT/HTML-MIME-TYPE |
empty |
(list (string->bytes/utf-8 (include-template "blog.html"))))) |
(define (blog-posted title body k-url) |
(include-template "blog-posted.html")) |
(define (blog-posts k-url) |
(include-template "blog-posts.html")) |
Each of the templates are given below:
<html> |
<head> |
<title>Al's Church: @|section|</title> |
<style type="text/css"> |
body { |
margin: 0px; |
padding: 10px; |
} |
|
#main { |
background: #dddddd; |
} |
</style> |
</head> |
<body> |
<script type="text/javascript"> |
var gaJsHost = (("https:" == document.location.protocol) ? |
"https://ssl." : "http://www."); |
document.write(unescape("%3Cscript src='" + gaJsHost + |
"google-analytics.com/ga.js' |
type='text/javascript'%3E%3C/script%3E")); |
</script> |
<script type="text/javascript"> |
var pageTracker = _gat._getTracker("UA-YYYYYYY-Y"); |
pageTracker._trackPageview(); |
</script> |
|
<h1>Al's Church: @|section|</h1> |
<div id="main"> |
@body |
</div> |
</body> |
</html> |
Notice that this part of the presentation is much simpler, because the CSS and JavaScript can be included verbatim, without resorting to any special escape-escaping patterns. Similarly, since the body is represented as a string, there is no need to remember if splicing is necessary.
<h2>@|title|</h2> |
<p>@|body|</p> |
|
<h1><a href="@|k-url|">Continue</a></h1> |
@in[p posts]{ |
<h2>@(post-title p)</h2> |
<p>@(post-body p)</p> |
} |
|
<h1>New Post</h1> |
<form action="@|k-url|"> |
<input name="title" /> |
<input name="body" /> |
<input type="submit" /> |
</form> |
Compare this template with the original presentation function: there is no need to worry about managing how lists are nested: the defaults just work.