Taking advantage of Phoenix rendering and iodata (part 1)

DRAFT: Expect updates as people who know more than I do correct me.

Among the wonderful things about Phoenix is that it doesn't render strings. Instead, it renders iodata. That's an Elixir idiom that speeds up string processing by avoiding concatenation. But it also makes it easier to work with conditional logic in views and templates. This two-post series is about how I'm doing that in a new Phoenix app (as a new Phoenix/Elixir programmer).

First, some basics

Consider a function helper in a View module, called like this from a template:

What sort of things can helper return?

One is a string like "<strong>STRONG</strong>&copy;". As you probably know, that shows up on your screen like this:

Characters like angle brackets are escaped. The string is treated as plain text, rather than HTML. Note, though, that the protection does not happen when the results of helper are injected into the template. Consider it a final step. (I'll return to this later.)

You may also know that such helpers can return nil, which has the same effect as injecting an empty string. That's convenient because Elixir's if returns nil given a missing else, so you can write code like this:

... which produces nothing when there's no logged-in user.

What I didn't realize until recently was that helper can also return a list. You can get the same image as above returning this:

    ["<strong>", "STRONG", "</str", "ong>", "&copy;"]

Such a result needn't contain only strings. It can also contain integers that correspond to first 256 Unicode code points (extended ASCII). The above means the same thing as the following:

[60, "strong", 62, "STRONG", 60, 47, "str", "ong", 62, "&copy;"]

The lists are allowed to be recursive, so you can nest ridiculously:

    [ ["<strong>", "STRONG", [["</str"], "ong>"]], [[["&copy;"]]]]

But you can also nest non-ridiculously. As a hint at what "non-ridiculous" means, consider that HTML documents are recursive structures. By returning appropriate trees, we can have code/data that looks very like the HTML we want to produce. Moreover, we can have conditional logic that doesn't interfere with that look.

Before discussing that (in part 2), I need to cover generating HTML.

Substructures that contain approved HTML

Phoenix.HTML.raw is the function your code should use to protect helper results from the protection step (and so let HTML act as HTML). However, all it does is wrap its argument in a tuple. One that looks like this:

{:safe, "<strong>STRONG</strong>&copy;"}

If helper returns that, you get this:

But you're not limited to strings. A list works just as well:

    {:safe, [60, "strong", 62, "STRONG", 60, 47, "strong", 62, "&copy;"]}

Indeed, that's the sort of thing that the functions in Phoenix.HTML.Tag produce. For example:

iex(24)> Phoenix.HTML.Tag.content_tag(:strong, "STRONG")  
{:safe, [60, "strong", "", 62, "STRONG", 60, 47, "strong", 62]}

Conveniently, you can nest such :safe tuples within helper return values:

    [ [{:safe, "<strong>"}, "STRONG", {:safe, "</strong>"}],
      {:safe, "&copy;"}
    ]

Those are the key facts you need to build more complex helpers. There are some subtleties here because - although I haven't called that out yet - there are actually five types involved in all of this.

Types

These types can be somewhat confusing, but that needn't have much of an effect if you let Phoenix handle most of the work. Still, it's good to know about them.

An iolist is a list containing a mixture of characters, strings, and nested iolists. It is an Erlang-standard structure. Note that an iolist may not be nil, contain nil, nor contain a tuple.

iodata is either a string or an iolist. (That just means that if you have a single string, you don't have to wrap it in a list.) Erlang (and Elixir) output functions often take iodata, not just strings.

A safe tuple is a two-element tuple whose first element is :safe and whose second element is an iolist. Note that, unlike iolists, safe tuples may not be nested. That is, the following is not allowed:

{:safe, ["an hr here: ", {:safe, "<hr/>"}]}

The implication is that HTML-building code should either only use raw at leaves of the computation tree or handle handle safe tuples specially. Fortunately, functions like content_tag do that. The following shows how, when you hand a safe tuple to content_tag, it produces a proper (non-nested) safe tuple:

iex(25)> Phoenix.HTML.Tag.content_tag(:strong, {:safe, "<em>EM</em>"})  
{:safe, [60, "strong", "", 62, "<em>EM</em>", 60, 47, "strong", 62]}

Phoenix iodata is either iodata, a safe tuple, or a list of Phoenix iodata. That is, it's like Erlang iodata, except that it may also contain safe tuples. But once you "hit" a safe tuple, the contents must be pure Elixir iodata.

Finally, a helper function may return either Phoenix iodata or an atom (such as :elixir or nil). That is, the convenience value of nil doesn't apply anywhere within a list structure: nil is either the whole return value or no part of it.

Within either kind of iodata, if you want to represent "nothing" or "no contribution to the generated HTML", use either an empty list or an empty string. (I'll be using an empty list in part 2.)

Putting it all together

The way rendering works is - I think - that:

  1. A template file is processed to construct code (not just data) that looks like this:

  2. When a request is made, that code is run, creating Phoenix iodata.

  3. That result is processed. The top-level safe tuples have their embedded strings extracted. Un-tupled iodata is descended:

    • Embedded safe tuples are replaced with their content. (So, {:safe, "<strong>"} produces an HTML strong tag.)
    • Strings have their dangerous characters replaced. (So, a <strong> that's not inside a safe tuple is converted to &lt;strong&gt;.)
  4. The iodata is handed off to code that efficiently constructs an HTTP response.