Phoenix.LiveView.Engine (Phoenix LiveView v0.15.7) View Source
The .leex
(Live EEx) template engine that tracks changes.
In the documentation 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 function that takes a boolean argument
(see "Tracking changes" below), and returns a list of dynamic content.
Each element in the list is either one of:
- iodata - which is the dynamic content
- nil - the dynamic content did not change
- another
Phoenix.LiveView.Rendered
struct, see "Nesting and fingerprinting" below - a
Phoenix.LiveView.Comprehension
struct, see "Comprehensions" below - a
Phoenix.LiveView.Component
struct, see "Component" below
When you render a .leex
template, you can convert the
rendered structure to iodata by alternating 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: fn track_changes? -> ["left", "right"] end
}
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 including a changed
map in the assigns with the key __changed__
and passing
true
to the dynamic parts. 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 above expression 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 only 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.