---
title: 'Views and Templates'
source: 'https://academia.sh/en/courses/mvc-framework/views-and-templates'
course: 'The Model–View–Controller Framework'
language: en
updated: '2026-10-21T05:03:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that the template engine escapes local values and what escaping gives when it is applied twice'
    - 'Shows that a missing local value does not stay silently empty but raises an error'
    - 'Measures that double escaping arises when the output of a partial is given to the outer template as a local value'
    - 'Shows that the compiled template stands in the application store and that its stay is persistent'
---

# Views and Templates

A template is compiled once and local values are escaped on every render. The lesson measures what escaping does, that a missing local value raises an error, that double escaping arises in a partial, and that the compiled template is persistent.

The previous lesson showed that the controller prepares the product and carries the flash message to the next request. The prepared value finally enters a **view**, and the view turns it into the text the user will see. The source of a view is a **template**: a string made of fixed text and places to fill, such as `{{name}}`. This lesson has two questions. What happens when a value enters a template: does `<b>pen</b>`, given as a local value, become a tag or text in the output? And where does the template itself stand after it is rendered, and for how many requests does it stay?

The core's `stay.rb` file is given below once more. The block is source, it has no output, and it is pulled in with `require_relative`.

```ruby
# stay.rb — a small request loop and a helper that classifies how many requests a value stays for

# Flash: a value written in this request is carried to the next request only.
class Flash
  def initialize(incoming)
    @incoming = incoming
    @fresh = {}
  end

  def [](key) = @fresh.fetch(key) { @incoming[key] }

  def []=(key, value)
    @fresh[key] = value
  end

  def passed_on = @fresh
end

# Every request gets a new context: params, flash and the request store start empty each time.
class Context
  attr_reader :app, :params, :session, :flash, :request_store

  def initialize(app, session)
    @app = app
    @params = {}
    @session = session
    @flash = Flash.new(session.delete(:flash) || {})
    @request_store = {}
  end
end

# The application store is the same across all requests and all clients;
# the session is chosen per client.
class App
  attr_reader :store

  def initialize
    @sessions = Hash.new { |h, client| h[client] = {} }
    @store = { products: {}, schema: { products: %i[name price] }, templates: {},
               queue: [], cache: {}, counter: 0 }
  end

  def request(client)
    context = Context.new(self, @sessions[client])
    result = yield context
    context.session[:flash] = context.flash.passed_on unless context.flash.passed_on.empty?
    result
  end
end

# Rule: request 1 (client A) writes the value; requests 2 and 3 (A) and request 4 (B) read it.
# If B sees it, persistent; if A's third request sees it, session; if only the second
# request sees it, next; if none sees it, this_request.
def stay(write, read)
  app = App.new
  first = app.request("A") { |b| write.call(b); read.call(b) }
  raise ArgumentError, "value could not be read in the request that wrote it" if first.nil?

  seen = [["A", 2], ["A", 3], ["B", 4]].map do |client, order|
    app.request(client) { |b| read.call(b) }.nil? ? "--" : "#{client}#{order}"
  end
  kind = if seen[2] != "--" then "persistent"
         elsif seen[1] != "--" then "session"
         elsif seen[0] != "--" then "next"
         else "this_request"
         end
  [kind, ["A1", *seen].join(" "), first]
end
```

## Escaping

The core's view layer consists of two helpers. `escape` replaces four characters in a text with their counterparts. `compile_template` turns a template source into a lambda once; as the lambda is called, it passes the local values through `escape` and puts them in their places. **Escaping** makes the text that comes from a user appear on the page as text and not as markup.

```ruby
# escape.rb — escaping four characters and escaping twice
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

p escape('<script>alert("x")</script>')
p escape("Tom & Jerry")
p escape(escape("<b>"))
p escape(nil)
p escape(12)
```
```
"&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;"
"Tom &amp; Jerry"
"&amp;lt;b&amp;gt;"
""
"12"
```

The first line shows all four characters: `<` and `>` protect the tag boundaries, `"` protects the attribute boundary, and `&` protects the beginning of escaping itself. A text that could be read as a tag is now only text. In the second line `&` was escaped first and became `&amp;`. The third line is the result of double escaping: `<b>` became `&lt;b&gt;` in the first pass, the `&` characters were changed again in the second pass, and the text came to the form `&amp;lt;b&amp;gt;`. On the page this shows the user the text `&lt;b&gt;`. The last two lines show that `escape` first turns every input into a string: `nil` became an empty string and `12` became `"12"`.

That escaping happens in the template engine is a choice. If the controller or the model escaped the value and stored it, the same data would appear with the text `&lt;` in a console, in an export, or in another view. Escaping belongs to the format of the output, so it is done at render time and only where the value is rendered. The value itself is stored as it is, and each view escapes it according to its own format; transforming the data twice is the first cause of double escaping.

**FS18.** This measurement is only for four characters. The single quote is not escaped in the core; putting user text into an attribute value delimited by single quotes is not safe with this escaping.

## A Missing Local Value

Every `{{name}}` in a template needs a local value. The core's lambda reads the local value with `fetch`. This makes a missing key raise an error instead of silently giving an empty string.

```ruby
# missing.rb — a place where no local value is given
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

app = App.new
template = compile_template(Context.new(app, {}), "products/show", "<h1>{{name}}</h1>")

p template.call(name: "pen")
begin
  template.call({})
rescue KeyError => e
  p e.class
end
p template.call(name: "pen", price: 12)
```
```
"<h1>pen</h1>"
KeyError
"<h1>pen</h1>"
```

With the right local value the template gave `<h1>pen</h1>`. When the local value was not given, a `KeyError` came out; the error message was not printed because the format of the message depends on the version. The third call ran with an extra local value that has no counterpart in the template and raised no error: the template looks only for its own places and ignores the excess. That a missing value is an error and an extra value is not is a design balance. Whoever writes a template wants the local values to arrive complete; a controller, however, may pass more values to a template than needed, and this is not counted as an error.

**FS19.** The measured behavior belongs to the core that uses `fetch`. A template engine that treats a missing value as an empty string produces an empty heading from the same template without an error.

## A Partial and Double Escaping

A large page is divided into small templates. The part of a template that is rendered inside another template is called a **partial**. The core has no separate helper for a partial; a compiled template can be given as the local value of another template. The helper below is this lesson's own plain Ruby helper and is not added to the core.

```ruby
# partial.rb — giving the output of a partial to the outer template
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

def part(b, name, locals) = b.app.store[:templates].fetch(name).call(locals)

app = App.new
b = Context.new(app, {})
compile_template(b, "products/row", "<li>{{name}}</li>")
compile_template(b, "products/list", "<ul>{{body}}</ul>")

row = part(b, "products/row", name: "pen")
p row
p part(b, "products/list", body: row)
p part(b, "products/list", body: escape("a&b"))
```
```
"<li>pen</li>"
"<ul>&lt;li&gt;pen&lt;/li&gt;</ul>"
"<ul>a&amp;amp;b</ul>"
```

The partial gave `<li>pen</li>`. When this output was given to the outer template as a local value, the outer template escaped it too and the list became `<ul>&lt;li&gt;pen&lt;/li&gt;</ul>`: on the page the user sees not a tag but the text of the tag. In the core's model there is no way to mark a string as already rendered and safe; every local value, whatever its source, is escaped. The third line shows the other face of the same problem: if the value is escaped by hand beforehand on the calling side, the outer template escapes it once more and the `&` character changes again. The way out of double escaping is not to give the value without escaping; the core escapes it anyway in that case. The output of a partial therefore cannot be carried to the outer template as a local value; in this model the output has to be embedded in the source of the outer template.

**FS20.** Real template engines offer a way to mark rendered output as safe and solve the double escaping problem with it. The core does not model this, so the measured behavior is not seen in an engine that carries such a mark.

## The Type of a Local Value

A template expects a string as a local value, but `escape` first turns every input into a string with `to_s`. Looking at how values that are not strings enter the page shows what a controller can give to a template.

```ruby
# kind.rb — local values that are not strings
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

p escape(:name)
p escape(1.5)
p escape([1, "<"])
p escape({ a: 1 })
```
```
"name"
"1.5"
"[1, &quot;&lt;&quot;]"
"{a: 1}"
```

The output for the symbol `:name` and for the float is in the expected form. The array and the hash were turned into strings with Ruby's own representation and then escaped: the `"` characters in the array became `&quot;` and the `<` character became `&lt;`. In this case the page gives the user Ruby's internal representation, which is mostly not the wanted output. A controller should give the template the text to be shown ready; the template only escapes the string and does no formatting.

## One Template, Many Renders

The compiled template stands as a single entry in the application store. Calling the same template with different local values does not grow the store; the rendered output is not stored either.

```ruby
# drawing.rb — one template, three renders
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

app = App.new
b = Context.new(app, {})
compile_template(b, "products/show", "<h1>{{name}}</h1>")
store = app.store[:templates]

p store["products/show"].call(name: "pen")
p store["products/show"].call(name: "notebook")
p store["products/show"].call(name: "eraser")
p store.size
p store.keys
```
```
"<h1>pen</h1>"
"<h1>notebook</h1>"
"<h1>eraser</h1>"
1
["products/show"]
```

Three calls gave three separate outputs and none affected the others: the local values pass to the lambda as input on every call and do not stay inside the lambda. There was again a single entry in the store, and its key was the name of the template. A render is therefore in the **this request** stay: the output returns only to the code that made the call and is left nowhere. What is persistent is only the compiled form of the template.

## The Template Name and Lookup

In the first lesson we saw that the template name is derived from the controller and action names. This name is not without meaning: the compiled templates in the store are looked up with this name. There is a link between the convention that produces the name and the store that looks it up, but this link is built only by the match of the name.

```ruby
# lookup.rb — looking up a template with a derived name
require_relative "stay"

# Convention: the template name is not written, it is derived from the controller and action names.
def template_name(params) = "#{params[:controller]}/#{params[:action]}"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

app = App.new
b = Context.new(app, {})
store = app.store[:templates]
name = template_name(controller: "products", action: "show")

p store[name]
begin
  store.fetch(name)
rescue KeyError => e
  p e.class
end

compile_template(b, name, "<h1>{{name}}</h1>")
p store.fetch(name).call(name: "pen")
```
```
nil
KeyError
"<h1>pen</h1>"
```

Before the template was compiled, `store[name]` gave `nil`, while `fetch` raised a `KeyError`. Which one is chosen decides whether a missing template is reported silently or with an error. After compiling, the same name found the template and the render was done. The convention produces the name and the store looks it up; that one produced the name does not mean that the other compiled a template with that name. **FS21.** The measurement is for a single name and assumes that the name is consistent at the moment it is produced and the moment it is compiled.

## The Stay of the Compiled Template

A template is compiled once and the result is written to the application store. We can ask the core's classifier for how many requests the compiled template is read.

```ruby
# views.rb — the stay of the compiled template
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

write = ->(b) { compile_template(b, "products/show", "<h1>{{name}}</h1>") }
read = ->(b) { b.app.store[:templates]["products/show"]&.call(name: "<b>pen</b>") }
p stay(write, read)
```
```
["persistent", "A1 A2 A3 B4", "<h1>&lt;b&gt;pen&lt;/b&gt;</h1>"]
```

The result was `["persistent", "A1 A2 A3 B4", "<h1>&lt;b&gt;pen&lt;/b&gt;</h1>"]`. The template was there in all four reads, and client B read it too. The stay is **persistent**, because the template stands not in the request but in the application store. The third column also shows the effect of escaping: `<b>pen</b>`, given as a local value, became text in the output and not a tag.

What is measured here is not the rendered output; the render is done again on every call. What is persistent is the compiled form of the template. This difference matters: compiling is an expensive job and is done once, while rendering is cheap and is repeated on every request.

## The Bounding Measurement

That compiling is done once also means that when the template source changes, the new source will not be seen. For the same name, the core keeps the existing template on a second compile.

```ruby
# limit.rb — a second compile with the same name keeps the old template
require_relative "stay"

# View: a template is compiled once; local values are escaped on every render.
def escape(text) = text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")

def compile_template(b, name, source)
  b.app.store[:templates][name] ||= lambda do |locals|
    source.gsub(/\{\{(\w+)\}\}/) { escape(locals.fetch(Regexp.last_match(1).to_sym)) }
  end
end

app = App.new
b = Context.new(app, {})
store = app.store[:templates]

compile_template(b, "products/show", "<h1>{{name}}</h1>")
p store["products/show"].call(name: "a")

compile_template(b, "products/show", "<h2>{{name}}</h2>")
p store["products/show"].call(name: "a")

store.delete("products/show")
compile_template(b, "products/show", "<h2>{{name}}</h2>")
p store["products/show"].call(name: "a")
```
```
"<h1>a</h1>"
"<h1>a</h1>"
"<h2>a</h2>"
```

The second compile did not see the new source: the template still gave `<h1>a</h1>`. The core does not recompile a template whose name was compiled before; the new source is silently ignored. After the template was deleted from the store, the same call gave `<h2>a</h2>`. So the only way to refresh a compiled template is to remove it from the store. **FS22.** This measurement is for a store kept in memory; in a framework that reads from a source file, detecting a change is designed separately.

## Summary

- The template engine escapes every local value; escaping changes four characters and, applied twice, gives an output such as `&amp;lt;`.
- A missing local value raises a `KeyError`; an extra local value is ignored.
- If the output of a partial is given to the outer template as a local value, it is escaped again; the core has no way to mark rendered output as safe.
- The stay of this lesson is **persistent**: the compiled template stands in the application store and is read in all requests and by all clients.
- A second compile with the same name keeps the old template; to refresh it, the template has to be deleted from the store.

## Next Step

The controller carried the flash message and the view kept the template in the application store; the saved product also stands in the same store. The next topic opens this store: it measures where the data has to stand so that a saved product is visible in another browser too, and how the model layer builds this stay.
