drab v0.10.0 Drab.Live View Source

Drab Module to provide a live access and update of assigns of the template, which is currently rendered and displayed in the browser.

The idea is to reuse your Phoenix templates and let them live, to make a possibility to update assigns on the living page, from the Elixir, without re-rendering the whole html. But because Drab tries to update the smallest amount of the html, there are some limitations, for example, it when updating the nested block it does not know the local variables used before. Please check out Drab.Live.EExEngine for more detailed description.

Use peek/2 to get the assign value, and poke/2 to modify it directly in the DOM tree.

Drab.Live uses the modified EEx Engine (Drab.Live.EExEngine) to compile the template and indicate where assigns were rendered. To enable it, rename the template you want to go live from extension .eex to .drab. Then, add Drab Engine to the template engines in config.exs:

config :phoenix, :template_engines,
  drab: Drab.Live.Engine

Performance

Drab.Live re-renders the page at the backend and pushes only the changed parts to the fronted. Thus it is not advised to use it in the big, slow rendering pages. In this case it is better to split the page to the partials and poke in the partial only, or use light update with Drab.Element or Drab.Query.

Also, it is not advised to use Drab.Live with big assigns - they must be transferred from the client when connected.

Avoiding using Drab

If there is no need to use Drab with some expression, you may mark it with nodrab/1 function. Such expressions will be treated as a “normal” Phoenix expressions and will not be updatable by poke/2.

<p>Chapter <%= nodrab(@chapter_no) %>.</p>

Since Elixir 1.6, you may use the special marker “/“, which does exactly the same as nodrab:

<p>Chapter <%/ @chapter_no %>.</p>

The @conn case

The @conn assign is often used in Phoenix templates. Drab considers it read-only, you can not update it with poke/2. And, because it is often quite hudge, may significantly increase the number of data sent to and back from the browser. This is why by default Drab trims @conn, leaving only the essential fields, by default :private => :phoenix_endpoint.

This behaviour is configuable with :live_conn_pass_through. For example, if you want to preseve the specific assigns in the conn struct, mark them as true in the config:

config :drab, MyAppWeb.Endpoint,
  live_conn_pass_through: %{
    assigns: %{
      users: true
    },
    private: %{
      phoenix_endpoint: true
    }
  }

Shared Commanders

When the event is triggered inside the Shared Commander, defined with drab-commander attribute, all the updates will be done only withing this region. For example:

<div drab-commander="DrabTestApp.Shared1Commander">
  <div><%= @assign1 %></div>
  <button drab-click="button_clicked">Shared 1</button>
</div>
<div drab-commander="DrabTestApp.Shared1Commander">
  <div><%= @assign1 %></div>
  <button drab-click="button_clicked">Shared 2</button>
</div>

defhandler button_clicked(socket, sender) do
  poke socket, assign1: "changed"
end

This will update only the div with @assign1 in the same <div drab-commander> as the button.

Please notice it works also for peek - it will return the proper value, depends where the event is triggered.

Caching

Browser communication is the time consuming operation and depends on the network latency. Because of this, Drab caches the values of assigns in the current event handler process, so they don’t have to be re-read from the browser on every poke or peek operation. The cache is per process and lasts only during the lifetime of the event handler.

This, event handler process keeps all the assigns value until it ends. Please notice that the other process may update the assigns on the page in the same time, by using broadcasting functions, when your event handler is still running. If you want to re-read the assigns cache, run clean_cache/0.

Partials

Function poke/2 and peek/2 works on the default template - the one rendered with the Controller. In case there are some child templates, rendered inside the main one, you need to specify the template name as a second argument of poke/3 and peek/3 functions.

In case the template is not under the current (main) view, use poke/4 and peek/4 to specify the external view name.

Assigns are archored within their partials. Manipulation of the assign outside the template it lives will raise ArgumentError. Partials are not hierachical, eg. modifying the assign in the main partial will not update assigns in the child partials, even if they exist there.

Rendering partial templates in a runtime

There is a possibility add the partial to the DOM tree in a runtime, using render_to_string/2 helper:

poke socket, live_partial1: render_to_string("partial1.html", color: "#aaaabb")

But remember that assigns are assigned to the partials, so after adding it to the page, manipulation must be done within the added partial:

poke socket, "partial1.html", color: "red"

Limitions

Because Drab must interpret the template, inject it’s ID etc, it assumes that the template HTML is valid. There are also some limits for defining properties. See Drab.Live.EExEngine for a full description.

Update Behaviours

There are different behaviours of Drab.Live, depends on where the expression with the updated assign lives. For example, if the expression defines tag attribute, like <span class="<%= @class %>">, we don’t want to re-render the whole tag, as it might override changes you made with other Drab module, or even with Javascript. Because of this, Drab finds the tag and updates only the required attributes.

Plain Text

If the expression in the template is given in any tag body, Drab will try to find the sourrounding tag and mark it with the attribute called drab-ampere. The attribute value is a hash of the previous buffer and the expression itself.

Consider the template, with assign @chapter_no with initial value of 1 (given in render function in the controller, as usual):

<p>Chapter <%= @chapter_no %>.</p>

which renders to:

<p drab-ampere="someid">Chapter 1.</p>

This drab-ampere attribute is injected automatically by Drab.Live.EExEngine. Updating the @chapter_no assign in the Drab Commander, by using poke/2:

chapter = peek(socket, :chapter_no)     # get the current value of `@chapter_no`
poke(socket, chapter_no: chapter + 1)   # push the new value to the browser

will change the innerHTML of the <p drab-ampere="someid"> to “Chapter 2.” by executing the following JS on the browser:

document.querySelector('[drab-ampere=someid]').innerHTML = "Chapter 2."

This is possible because during the compile phase, Drab stores the drab-ampere and the corresponding pattern in the cache DETS file (located in priv/).

Injecting <span>

In case, when Drab can’t find the parent tag, it injects <span> in the generated html. For example, template like:

Chapter <%= @chapter_no %>.

renders to:

Chapter <span drab-ampere="someid">1</span>.

Attributes

When the expression is defining the attribute of the tag, the behaviour if different. Let’s assume there is a template with following html, rendered in the Controller with value of @button set to string "btn-danger".

<button class="btn <%= @button %>">

It renders to:

<button drab-ampere="someid" class="btn btn-danger">

Again, you can see injected drab-ampere attribute. This allows Drab to indicate where to update the attribute. Pushing the changes to the browser with:

poke socket, button: "btn btn-info"

will result with updated class attribute on the given tag. It is acomplished by running node.setAttribute("class", "btn btn-info") on the browser.

Notice that the pattern where your expression lives is preserved: you may update only the partials of the attribute value string.

Updating value attribute for <input> and <textarea>

There is a special case for <input> and <textarea>: when poking attribute of value, Drab updates the corresponding value property as well.

Properties

Nowadays we deal more with node properties than attributes. This is why Drab.Live introduces the special syntax. When using the @ sign at the beginning of the attribute name, it will be treated as a property.

<button @hidden=<%= @hidden %>>

Updating @hidden in the Drab Commander with poke/2 will change the value of the hidden property (without dollar sign!), by sending the update javascript: node['hidden'] = false.

You may also dig deeper into the Node properties, using dot - like in JavaScript - to bind the expression with the specific property. The good example is to set up .style:

<button @style.backgroundColor=<%= @color %>>

Additionally, Drab sets up all the properties defined that way when the page loads. Thanks to this, you don’t have to worry about the initial value.

Notice that @property=<%= expression %> is the only available syntax, you can not use string pattern or give more than one expression. Property must be stronly bind to the expression. You also can’t use quotes or apostrophes sourrounding the expressio. This is because it does not have to be a string, but any JSON encodable value.

The expression binded with the property must be encodable to JSON, so, for example, tuples are not allowed here. Please refer to Jason docs for more information about encoding JS.

Scripts

When the assign we want to change is inside the <script></script> tag, Drab will re-evaluate the whole script after assigment change. Let’s say you don’t want to use @property=<%=expression%> syntax to define the object property. You may want to render the javascript:

<script>
  document.querySelectorAll("button").hidden = <%= @buttons_state %>
</script>

If you render the template in the Controller with @button_state set to false, the initial html will look like:

<script drab-ampere="someid">
  document.querySelectorAll("button").hidden = false
</script>

Again, Drab injects some ID to know where to find its victim. After you poke/2 the new value of @button_state, Drab will re-render the whole script with a new value and will send a request to re-evaluate the script. Browser will run something like: eval("document.querySelectorAll("button").hidden = true").

Please notice this behaviour is disabled by default for safety. To enable it, use the following in your config.exs:

config :drab, enable_live_scripts: true

Broadcasting

There is a function broadcast_poke to broadcast living assigns to more than one browser.

For broadcasting using a subject instead of socket (like same_action/1), Drab is unable to automatically retrieve view and template name, as well as existing assigns values. This, the only acceptable version is broadcast_poke/4 with :using_assigns option.

iex> broadcast_poke same_action(MyApp.PageController, :mini), MyApp.PageView, "index.html",
     text: "changed text", using_assigns: [color: "red"]

Link to this section Summary

Functions

Returns a list of the assigns for the main partial

Like assigns/1 but will return the assigns for a given partial instead of the main partial

Like assigns/2, but returns the assigns for a given combination of a view and a partial

Broadcasting version of poke/2

Like broadcast_poke/2, but limited only to the given partial name

Like broadcast_poke/3, but searches for the partial within the given view

Cleans up the assigns cache for the current event handler process

Exception raising version of peek/2

Exception raising version of peek/3

Exception raising version of peek/4

Returns the current value of the assign from the current (main) partial

Like peek/2, but takes partial name and returns assign from that specified partial

Like peek/2, but takes a view and a partial name and returns assign from that specified view/partial

Exception raising version of poke/2

Exception raising version of poke/3

Exception raising version of poke/4

Updates the current page in the browser with the new assign value

Like poke/2, but limited only to the given partial name

Like poke/3, but searches for the partial within the given view

Link to this section Types

Link to this section Functions

Returns a list of the assigns for the main partial.

Examples:

iex> Drab.Live.assigns(socket)
[:welcome_text]
Link to this function assigns(socket, partial) View Source
assigns(Phoenix.Socket.t(), String.t() | nil) :: list()

Like assigns/1 but will return the assigns for a given partial instead of the main partial.

Examples:

iex> assigns(socket, "user.html")
[:name, :age, :email]
Link to this function assigns(socket, view, partial) View Source
assigns(Phoenix.Socket.t(), atom() | nil, String.t() | nil) :: list()

Like assigns/2, but returns the assigns for a given combination of a view and a partial.

iex> assigns(socket, MyApp.UserView, "user.html")
[:name, :age, :email]
Link to this function broadcast_poke(socket, assigns) View Source
broadcast_poke(Drab.Core.subject(), Keyword.t()) :: result() | no_return()

Broadcasting version of poke/2.

Please notice that broadcasting living assigns makes sense only for the pages, which was rendered with the same templates.

Broadcasting the poke is a non-trivial operation, and you must be aware that the local assign cache of the handler process is not updated on any of the browsers. This mean that peek/2 may return obsolete values.

Returns {:ok, :broadcasted}.

iex> broadcast_poke(socket, count: 42)
%Phoenix.Socket{ ...
Link to this function broadcast_poke(socket, partial, assigns) View Source
broadcast_poke(Drab.Core.subject(), String.t() | nil, Keyword.t()) ::
  result() | no_return()

Like broadcast_poke/2, but limited only to the given partial name.

iex> broadcast_poke(socket, "user.html", name: "Bożywój")
{:ok, :broadcasted}
Link to this function broadcast_poke(socket, view, partial, assigns) View Source
broadcast_poke(Drab.Core.subject(), atom() | nil, String.t() | nil, Keyword.t()) ::
  result() | no_return()

Like broadcast_poke/3, but searches for the partial within the given view.

iex> broadcast_poke(socket, MyApp.UserView, "user.html", name: "Bożywój")
{:ok, :broadcasted}

This function allow to use subject instead of socket to broadcast living assigns without having a socket. In this case, you need to provide all other assigns to the function, with :using_assigns option.

iex> broadcast_poke same_action(MyApp.PageController, :mini), MyApp.PageView, "index.html",
     text: "changed text", using_assigns: [color: "red"]
{:ok, :broadcasted}

Hint: if you have functions using @conn assign, you may fake it with %Plug.Conn{private: %{:phoenix_endpoint => MyAppWeb.Endpoint}}

Link to this function clean_cache() View Source
clean_cache() :: term()

Cleans up the assigns cache for the current event handler process.

Should be used when you want to re-read the assigns from the browser, for example when the other process could update the living assigns in the same time as current event handler runs.

Link to this function peek!(socket, assign) View Source
peek!(Phoenix.Socket.t(), atom()) :: term() | no_return()

Exception raising version of peek/2.

Returns the current value of the assign from the current (main) partial.

iex> peek!(socket, :count)
42
iex> peek!(socket, :nonexistent)
** (ArgumentError) Assign @nonexistent not found in Drab EEx template
Link to this function peek!(socket, partial, assign) View Source
peek!(Phoenix.Socket.t(), String.t(), atom()) :: term() | no_return()

Exception raising version of peek/3.

iex> peek!(socket, "users.html", :count)
42
Link to this function peek!(socket, view, partial, assign) View Source
peek!(Phoenix.Socket.t(), atom() | nil, String.t() | nil, atom() | String.t()) ::
  term() | no_return()

Exception raising version of peek/4.

iex> peek(socket, MyApp.UserView, "users.html", :count)
42

Returns the current value of the assign from the current (main) partial.

iex> peek(socket, :count)
{ok, 42}
iex> peek(socket, :nonexistent)
** (ArgumentError) Assign @nonexistent not found in Drab EEx template

Notice that this is a value of the assign, and not the value of any node property or attribute. Assign gets its value only while rendering the page or via poke. After changing the value of node attribute or property on the client side, the assign value will remain the same.

Link to this function peek(socket, partial, assign) View Source
peek(Phoenix.Socket.t(), String.t(), atom()) :: result() | no_return()

Like peek/2, but takes partial name and returns assign from that specified partial.

Partial is taken from the current view.

iex> peek(socket, "users.html", :count)
{:ok, 42}
Link to this function peek(socket, view, partial, assign) View Source
peek(Phoenix.Socket.t(), atom() | nil, String.t() | nil, atom() | String.t()) ::
  result() | no_return()

Like peek/2, but takes a view and a partial name and returns assign from that specified view/partial.

iex> peek(socket, MyApp.UserView, "users.html", :count)
{:ok, 42}

Exception raising version of poke/2.

Returns integer, which is the number of updates on the page. It combines all the operations, so updating properties, attributes, text, etc.

iex> poke!(socket, count: 42)
3
Link to this function poke!(socket, partial, assigns) View Source
poke!(Phoenix.Socket.t(), String.t() | nil, Keyword.t()) ::
  integer() | no_return()

Exception raising version of poke/3.

Returns integer, which is the number of updates on the page. It combines all the operations, so updating properties, attributes, text, etc.

iex> poke!(socket, "user.html", name: "Bożywój")
0
Link to this function poke!(socket, view, partial, assigns) View Source
poke!(Phoenix.Socket.t(), atom() | nil, String.t() | nil, Keyword.t()) ::
  integer() | no_return()

Exception raising version of poke/4.

Returns integer, which is the number of updates on the page. It combines all the operations, so updating properties, attributes, text, etc.

iex> poke!(socket, MyApp.UserView, "user.html", name: "Bożywój")
0

Updates the current page in the browser with the new assign value.

Works inside the main partial - the one rendered in the controller - only. Does not touch children partials, even if they contain the given assign.

Raises ArgumentError when assign is not found within the partial. Please notice that only assigns rendered with <%= %> mark are pokeable; assigns rendered with <% %> or <%/ %> only can’t be updated by poke.

Returns {:error, description} or {:ok, N}, where N is the number of updates on the page. It combines all the operations, so updating properties, attributes, text, etc.

iex> poke(socket, count: 42)
{:ok, 3}

Passed values could be any JSON serializable term, or Phoenix safe html. It is recommended to use safe html, when dealing with values which are coming from the outside world, like user inputs.

import Phoenix.HTML # for sigil_E
username = sender.params["username"]
html = ~E"User: <%= username %>"
poke socket, username: html
Link to this function poke(socket, partial, assigns) View Source
poke(Phoenix.Socket.t(), String.t() | nil, Keyword.t()) :: result()

Like poke/2, but limited only to the given partial name.

iex> poke(socket, "user.html", name: "Bożywój")
{:ok, 3}
Link to this function poke(socket, view, partial, assigns) View Source
poke(Phoenix.Socket.t(), atom() | nil, String.t() | nil, Keyword.t()) ::
  result()

Like poke/3, but searches for the partial within the given view.

iex> poke(socket, MyApp.UserView, "user.html", name: "Bożywój")
{:ok, 3}