phoenix_live_view v0.7.0 Phoenix.LiveView.Engine View Source

The .leex (Live EEx) template engine that tracks changes.

On the docs below, we will explain how it works internally. For user-facing documentation, see Phoenix.LiveView.

Phoenix.LiveView.Rendered

Whenever you render a .leex template, it returns a Phoenix.LiveView.Rendered structure. This structure has three fields: :static, :dynamic and :fingerprint.

The :static field is a list of literal strings. This allows the Elixir compiler to optimize this list and avoid allocating its strings on every render.

The :dynamic field contains a list of dynamic content. Each element in the list is either one of:

  1. iodata - which is the dynamic content
  2. nil - the dynamic content did not change, see "Tracking changes" below
  3. another Phoenix.LiveView.Rendered struct, see "Nesting and fingerprinting" below
  4. a Phoenix.LiveView.Comprehension struct, see "Comprehensions" below
  5. a Phoenix.LiveView.Component struct, see "Component" below

When you render a .leex template, you can convert the rendered structure to iodata by intercalating the static and dynamic fields, always starting with a static entry followed by a dynamic entry. The last entry will always be static too. So the following structure:

%Phoenix.LiveView.Rendered{
  static: ["foo", "bar", "baz"],
  dynamic: ["left", "right"]
}

Results in the following content to be sent over the wire as iodata:

["foo", "left", "bar", "right", "baz"]

This is also what calling Phoenix.HTML.Safe.to_iodata/1 with a Phoenix.LiveView.Rendered structure returns.

Of course, the benefit of .leex templates is exactly that you do not need to send both static and dynamic segments every time. So let's talk about tracking changes.

Tracking changes

By default, a .leex template does not track changes. Change tracking can be enabled by passing a socket with two keys :fingerprints and :changed. If the :fingerprints matches the template fingerprint, then the :changed map is used. The map should contain the name of any changed field as key and the boolean true as value. If a field is not listed in :changed, then it is always considered unchanged.

If a field is unchanged and .leex believes a dynamic expression no longer needs to be computed, its value in the dynamic list will be nil. This information can be leveraged to avoid sending data to the client.

Nesting and fingerprinting

Phoenix.LiveView also tracks changes across .leex templates. Therefore, if your view has this:

<%= render "form.html", assigns %>

Phoenix will be able to track what is static and dynamic across templates, as well as what changed. A rendered nested .leex template will appear in the dynamic list as another Phoenix.LiveView.Rendered structure, which must be handled recursively.

However, because the rendering of live templates can be dynamic in itself, it is important to distinguish which .leex template was rendered. For example, imagine this code:

<%= if something?, do: render("one.html", assigns), else: render("other.html", assigns) %>

To solve this, all Phoenix.LiveView.Rendered structs also contain a fingerprint field that uniquely identifies it. If the fingerprints are equal, you have the same template, and therefore it is possible to only transmit its changes.

Comprehensions

Another optimization done by .leex templates is to track comprehensions. If your code has this:

<%= for point <- @points do %>
  x: <%= point.x %>
  y: <%= point.y %>
<% end %>

Instead of rendering all points with both static and dynamic parts, it returns a Phoenix.LiveView.Comprehension struct with the static parts, that are shared across all points, and a list of dynamics to be interpolated inside the static parts. If @points is a list with %{x: 1, y: 2} and %{x: 3, y: 4}, the expression above would return:

%Phoenix.LiveView.Comprehension{
  static: ["\n  x: ", "\n  y: ", "\n"],
  dynamics: [
    ["1", "2"],
    ["3", "4"]
  ]
}

This allows .leex templates to drastically optimize the data sent by comprehensions, as the static parts are emitted once, regardless of the number of items.

The list of dynamics is always a list of iodatas or components, as we don't perform change tracking inside the comprehensions themselves. Similarly, comprehensions do not have fingerprints because they are only optimized at the root, so conditional evaluation, as the one seen in rendering, is not possible. The only possible outcome for a dynamic field that returns a comprehension is nil.

Components

.leex also supports stateful components. Since they are stateful, they are always handled lazily by the diff algorithm.