Drab

Drab allows to query and manipulate the User Interface directly from the Phoenix server backend.

Drab operates on top of the Phoenix application, to run it you must already have it configured. In case you operate the app under an umbrella, all Drab configuration and installation should be done in the Web application (in most cases the one ending with _web).

All Drab functions (callbacks and event handlers) should be placed in a module called a ‘commander’. It is very similar to controller, but it does not render any pages - it works with the live page instead. For example, pages generated by DrabExample.PageController are handled by commander with the corresponding name, in this case DrabExample.PageCommander:

defmodule DrabExample.PageCommander do
  use Drab.Commander

  onload :page_loaded

  # Drab Callbacks
  def page_loaded(socket) do
    set_prop socket, "div.jumbotron h2", innerHTML: "This page has been DRABBED"
  end

  # Drab Events
  defhandler button_clicked(socket, sender) do
    set_prop socket, this(sender), innerText: "already clicked"
  end
end

More on commander is in Drab.Commander documentation.

Drab handler are launched from the client (browser) side by running javascript function Drab.exec_elixir(), or defining the handler function name directly in the html:

<button drab-click="button_clicked">Clickety click</button>

More on event handlers in Drab.Core documentation.

Debugging Drab in IEx

When started with iex (iex -S mix phx.server) Drab shows the helpful message on how to debug its functions:

    Started Drab for /drab/docs, handling events in DrabPoc.DocsCommander
    You may debug Drab functions in IEx by copy/paste the following:
import Drab.{Core, Query, Modal, Waiter}
socket = Drab.get_socket(pid("0.443.0"))

    Examples:
socket |> select(:htmls, from: "h4")
socket |> exec_js("alert('hello from IEx!')")
socket |> alert("Title", "Sure?", buttons: [ok: "Azaliż", cancel: "Poniechaj"])

All you need to do is to copy/paste the line with socket = ... and now you can run Drab function directly from IEx, observing the results on the running browser in the realtime.

Modules

Drab is modular. You may choose which modules to use in the specific Commander by using :module option in use Drab.Commander directive or set it globally by :default_modules config option. By default, Drab.Live, Drab.Element and Drab.Modal are loaded.

Every module must have the corresponding javascript template, which is added to the client code in case the module is loaded. This is why it is good to keep the modules list small, if you are not using all modules.

Drab.Core module is always loaded.

List of Drab Modules

Drab.Core

Contains core functions, like exec_js. It is always loaded, as it is essential for the rest of Drab.

Drab.Live, the living assigns

This module is responsible for the living assigns. Contains function to push (poke) and pull (peek) new assign values to the browser, live. Works only with pages compiled with Drab.Live.EExEngine (pages with .drab extension).

Drab.Element, DOM element manipulation

Use functions from this module to get (query) or set (set_prop) properties or attributes of DOM elements. All functions are based on CSS selectors.

Drab.Modal, Bootstrap modal window

This module contains only one function, modal, which shows synchronous modal windows. It requires Bootstrap to work. Good to ask for a customer input.

Drab.Waiter, waits for the user input

This is an optional module, so must be listed in :module option in use Drab.Commander directive or in :default_modules config option. Analogically to modal, waits for the user input.

Drab.Query, the jQuery module

This is an optional module, so must be listed in :module option in use Drab.Commander directive or in :default_modules config option. Also, jQuery must be available as a global (see “Manuall Installation” in README). Drab.Query has a number of useful functions, brings jQuery to the server side.

Drab.Browser, browser related functions

This module is standalone (does not contain its own JS), so it does not have to be listed in use Drab.Commander or in the setup. Contains browser related functions, like get the local time, language or set the url in the browser bar.

Handling Exceptions

Drab intercepts all exceptions from event handler function and let it die, but before it presents the error message in the logs and an alert for a user on the page.

By default it is just an alert(), but you can easly override it by creating the template in the priv/templates/drab/drab.error_handler.js folder with your own javascript presenting the message. You may use the local variable message there to get the exception description, like:

alert(<%= message %>);

Drab in production and behind a proxy

When using in production, an app is often behind an apache/nginx server for domain virtualization or balancing, so the external port (80) is different from the actual app port (i.e. 4000). The necessary mapping between the two ports is usually done by configuring a proxy, but a particularly care have to be taken to correctly handle websocket calls, as they are at the core of Drab mechanism to communicate between the client browser and the backend server.

You can find more information and examples to how to configure your nxinx or apache environments on the Drab wiki page at https://github.com/grych/drab/wiki/Drab-in-production-and-behind-a-proxy

Learnig Drab

There is a tutorial/demo page.

The point to start reading docs should be Drab.Core.


Drab.Core

Drab module providing the base of communication between the browser and the server.

Drab.Core defines the method to declare client-side events, which are handled server-side in the commander module. Also provides basic function for running JS code directly from Phoenix on the browser.

Commander

Commander is the module to keep your Drab functions (event handlers) in. See Drab.Commander for more info, and just for this part of docs let’s assume you have the following one defined:

defmodule DrabExample.PageCommander do
  use Drab.Commander, modules: []

  defhandler button_clicked(socket, payload) do
    socket |> console("You've sent me this: #{payload |> inspect}")
  end
end

Events

Events are defined directly in the HTML by adding the drab attribute with the following pattern:

<button drab='event_name#options:event_handler_function_name(argument)'>clickme</button>

Example:

<button drab='click:button_clicked'>clickme</button>

Clicking above button launches DrabExample.PageCommander.button_clicked/2 on the server side.

<button drab='click:button_clicked(42)'>clickme</button>

Clicking the button above launches DrabExample.PageCommander.button_clicked/3 on the server side, with third argument of value 42. This is evaluated on the client side, so it could be any valid JS expression:

<button drab='click:button_clicked({the_answer: 42})'>
<button drab='click:button_clicked(window.location)'>

You may have multiple events defined for a DOM object, but the specific event may appear there only once (can’t define two handlers for one event). Separate event:handler pairs with whitespaces:

<button drab='click:button_clicked mouseover:prepare_button'>clickme</button>

Shortcut form

There are few shortcuts for the most popular events: click, keyup, keydown, change. For those events an attribute drab-EVENTNAME must be set. The following is an equivalent for the previous one:

<button drab-click='button_clicked'>clickme</button>

As above, there is a possibility to define multiple event handlers for one DOM object, but the only one handler for the event. The following form is valid:

<button drab-click='button_clicked' drab-mouseover='prepare_button(42)'>clickme</button>

But the next one is prohibited:

<button drab-click='handler1' drab-click='handler2'>INCORRECT</button>

In this case you may provide options with drab-options attribute, but only when you have the only one event defined.

There is a possibility to configure the shortcut list:

config :drab, MyAppWeb.Endpoint,
  events_shorthands: ["click", "keyup", "blur"]

Please keep this list short, as it affects client script performance.

Defining optional argument in multiple nodes with drab-argument attribute

If you add drab-argument attribute to any tag, all children of this tag will use this as an optional attribute. Notice that the existing arguments are not overwritten, so this:

<div drab-argument='42'>
  <button drab-click='button_clicked'>
  <button drab-click='button_clicked(43)'>
</div>

is the equivalent to:

<button drab-click='button_clicked(42)'>
<button drab-click='button_clicked(43)'>

Handling event in any commander (Shared Commander)

By default Drab runs the event handler in the commander module corresponding to the controller, which rendered the current page. But it is possible to choose the module by simply provide the full path to the commander:

<button drab-click='MyAppWeb.MyCommander.button_clicked'>clickme</button>

Notice that the module must be a commander module, ie. it must be marked with use Drab.Commander, and the function must be marked as public with Drab.Commander.public/1 macro.

Form values

If the sender object is inside a <form> tag, it sends the “form” map, which contains values of all the inputs found withing the form. Keys of that map are “name” attribute of the input or, if not found, an “id” attribute. If neither “name” or “id” is given, the value of the form is not included.

Control of element enabled/disabled state of element

By default, Drab takes control of enabled/disabled state of the Drab element. It disables the element when the handler is still running, to prevent multiple clicks. Element is back to the previous (enabled) state after the handler finish. Also in case of disconnection, Drab-controlled elements are disabled.

You may turn off this behaviour globally using the config options, see Drab.Config.

There is also a possibility to turn it off individually, using drab-no-disable attribute:

<button drab-click="clickety" drab-no-disable>Button</button>

Running Elixir code from the Browser

There is the Javascript method Drab.exec_elixir() in the global Drab object, which allows you to run the Elixir function defined in the Commander.

Store

Analogically to Plug, Drab can store the values in its own session. To avoid confusion with the Plug Session session, it is called a Store. You can use functions: put_store/3 and get_store/2 to read and write the values in the Store. It works exactly the same way as a “normal”, Phoenix session.

Session

Although Drab Store is a different entity than Plug Session (used in Controllers), there is a way to access the Session. First, you need to whitelist the keys you want to access in access_session/1 macro in the Commander (you may give it a list of atoms or a single atom). Whitelisting is due to security: it is kept in Token, on the client side, and it is signed but not encrypted.

defmodule DrabPoc.PageCommander do
  use Drab.Commander

  onload :page_loaded,
  access_session :drab_test

  def page_loaded(socket) do
    socket
    |> update(:val, set: get_session(socket, :drab_test), on: "#show_session_test")
  end
end

There is no way to update the session from Drab. Session is read-only.

Broadcasting

Normally Drab operates on the user interface of the browser which generared the event, but you may use it for broadcasting changes to all connected browsers. Drab uses a topic for distinguishing browsers, which are allowed to receive the change.

Broadcasting function receives socket or topic as the first argument. If socket is used, function derives the topic from the commander configuration. See Drab.Commander.broadcasting/1 to learn how to configure the broadcasting options. It is also possible to subscribe to the external topic in a runtime, using Drab.Commander.subscribe/2.

Broadcasting functions may be launched without the socket given. In this case, you need to define it manually, using helper functions: Drab.Core.same_path/1, Drab.Core.same_topic/1 and Drab.Core.same_controller/1. See broadcast_js/3 for more.

List of broadcasting functions:

exec_js/3

Synchronously executes the given javascript on the client side.

Returns tuple {status, return_value}, where status could be :ok or :error, and return value contains the output computed by the Javascript or the error message.

Options

  • timeout in milliseconds

Examples

iex> socket |> exec_js("2 + 2")
{:ok, 4}

iex> socket |> exec_js("not_existing_function()")
{:error, "not_existing_function is not defined"}

iex> socket |> exec_js("for(i=0; i<1000000000; i++) {}")
{:error, :timeout}

iex> socket |> exec_js("alert('hello from IEx!')", timeout: 500)
{:error, :timeout}



broadcast_js/3

Asynchronously executes the javascript on all the browsers listening on the given subject.

The subject is derived from the first argument, which could be:

  • socket - in this case broadcasting option is derived from the setup in the commander. See Drab.Commander.broadcasting/1 for the broadcasting options

  • same_path(string) - sends the JS to browsers sharing (and configured as listening to same_path in Drab.Commander.broadcasting/1) the same url

  • same_commander(atom) - broadcast goes to all browsers configured with :same_commander

  • same_topic(string) - broadcast goes to all browsers listening to this topic; notice: this is internal Drab topic, not a Phoenix Socket topic

First argument may be a list of the above.

The second argument is a JavaScript string.

See Drab.Commander.broadcasting/1 to find out how to change the listen subject.

iex> Drab.Core.broadcast_js(socket, "alert('Broadcasted!')")
{:ok, :broadcasted}
iex> Drab.Core.broadcast_js(same_path("/drab/live"), "alert('Broadcasted!')")
{:ok, :broadcasted}
iex> Drab.Core.broadcast_js(same_controller(MyApp.LiveController), "alert('Broadcasted!')")
{:ok, :broadcasted}
iex> Drab.Core.broadcast_js(same_topic("my_topic"), "alert('Broadcasted!')")
{:ok, :broadcasted}
iex> Drab.Core.broadcast_js([same_topic("my_topic"), same_path("/drab/live")],
"alert('Broadcasted!')")
{:ok, :broadcasted}

Returns {:ok, :broadcasted}

put_store/3

Saves the key => value in the Store. Returns unchanged socket.

put_store(socket, :counter, 1)

get_store/2

Returns the value of the Drab store represented by the given key.

uid = get_store(socket, :user_id)

get_session/2

Returns the value of the Plug Session represented by the given key.

counter = get_session(socket, :userid)

You must explicit which session keys you want to access in :access_session option in use Drab.Commander or globally, in config.exs:

config :drab, MyAppWeb.Endpoint,
  access_session: [:user_id]

get_store/3

Returns the value of the Drab store represented by the given key or default when key not found

counter = get_store(socket, :counter, 0)


Drab.Query

Drab Module which provides interface to jQuery on the server side. You may query (select/2) or manipulate (update/2, insert/2, delete/2, execute/2) the selected DOM object.

This module is optional and is not loaded by default. You need to explicitly declare it in the commander:

use Drab.Commander, modules: [Drab.Query]

This module requires jQuery installed as global, see README.

General syntax:

return = socket |> select(what, from: selector)
socket |> update(what, set: new_value, on: selector)
socket |> insert(what, into: selector)
socket |> delete(what, from: selector)
socket |> execute(what, on: selector)

where:

Object manipulation (update/2, insert/2, delete/2, execute/2) functions return socket. Query select/2 returns either a found value (when using singular version of jQuery method, eg :html), or a Map of %{name|id|__undefined_XX => value}, when using plural - like :htmls.

Select queries always refers to the page on which the event were launched. Data manipulation queries (update/2, insert/2, delete/2, execute/2) changes DOM objects on this page as well, but they have a broadcast versions: update!/2, insert!/2, delete!/2 and execute!/2, which works the same, but changes DOM on every currently connected browsers, which has opened the same URL, same controller, or having the same channel topic (see Drab.Commander.broadcasting/1 to find out more).


select/2 and select/3

Returns a value get by executing jQuery method on selected DOM object, or a Map of %{name|id|_undefined[INCREMENT]: value} when method name is plural, or a Map of %{ method => returns_of_methods}, when the method is :all.

Plural version uses name attribute as a key, or id, when there is no name, or __undefined_[INCREMENT], when neither id or name are specified.

In case the method requires an argument (like attr()), it should be given as key/value pair: method_name: “argument”.

Options:

  • from: “selector” - DOM selector which is queried
  • attr: “attribute” - DOM attribute
  • prop: “property” - DOM property
  • css: “css”
  • data: “att” - returns the value of jQuery data("attr") method

Examples:

name = socket |> select(:val, from: "#name")
# "Stefan"
name = socket |> select(:vals, from: "#name")
# %{"name" => "Stefan"}
font = socket |> select(css: "font", from: "#name")
# "normal normal normal normal 14px / 20px \"Helvetica Neue\", Helvetica, Arial, sans-serif"
button_ids = socket |> select(datas: "button_id", from: "button")
# %{"button1" => 1, "button2" => 2}

Available jQuery methods:

html text val
width height
innerWidth innerHeight outerWidth outerHeight
position offset scrollLeft scrollTop
attr: val prop: val css: val data: val

Available jQuery plural methods:

htmls texts vals
widths heights
innerWidths innerHeights outerWidths outerHeights
positions offsets scrollLefts scrollTops
attrs: val props: val csses: val datas: val

:all

In case when method is :all, executes all known methods on the given selector. Returns Map %{name|id => medthod_return_value}. The Map key are generated in the same way as those with plural methods.

socket |> select(:all, from: "span")
%{"first_span" => %{"height" => 16, "html" => "Some text", "innerHeight" => 20, ...

Additionally, id and name attributes are included into a Map.

 

First span with class qs_2  Second span with class qs_2 
 

 

 
 
 
 
 


update/2 and update/3

Updates the DOM object corresponding to the jQuery method.

In case when the method requires an argument (like attr()), it should be given as key/value pair:

method_name: "argument".

Waits for the browser to finish the changes, returns socket so it can be stacked.

Options:

  • on: selector - DOM selector, on which the changes are made
  • set: value - new value
  • attr: attribute - DOM attribute
  • prop: property - DOM property
  • class: class - class name to be replaced by another class
  • css: updates a given css
  • data: sets the jQuery data storage by calling data("key", value); it does not update the data-* attribute

Examples:

socket |> update(:text, set: "saved...", on: "#save_button")
socket |> update(attr: "style", set: "width: 100%", on: ".progress-bar")
# the same effect:
socket |> update(css: "width", set: "100%", on: ".progress-bar")

Update can also switch the classes in DOM object (remove one and insert another):

socket |> update(class: "btn-success", set: "btn-danger", on: "#save_button")

You can also cycle between values - switch to the next value from the list or to the first element, if the actual value is not on the list:

socket |> update(:text, set: ["One", "Two", "Three"], on: "#thebutton")
socket |> update(css: "font-size", set: ["8px", "10px", "12px"], on: "#btn")

When cycling through the class attribute, system will update the class if it is one in the list. In the other case, it will add the first from the list.

socket |> update(:class, set: ["btn-success", "btn-danger"], on: "#btn")

Please notice that cycling is only possible on selectors which returns one node.

Another possibility is to toggle (add if not exists, remove in the other case) the class:

socket |> update(:class, toggle: "btn-success", on: "#btn")

Available jQuery methods: see Drab.Query.select/2

span with some text
























insert/2

Adds new node (html) or class to the selected object.

Waits for the browser to finish the changes and returns socket so it can be stacked.

Options:

  • class: class - class name to be inserted
  • into: selector - class will be added to specified selectors; only applies with :class
  • before: selector - creates html before the selector
  • after: selector - creates html node after the selector
  • append: selector - adds html to the end of the selector (inside the selector)
  • prepend: selector - adds html to the beginning of the selector (inside the selector)

Example:

socket |> insert(class: "btn-success", into: "#button")
socket |> insert("<b>warning</b>", before: "#pane")

span with some text










delete/2

Removes nodes, classes or attributes from selected node.

With selector and no options, removes it and all its children. With given from: selector option, removes only the content, but element remains in the DOM tree. With options class: class, from: selector removes class from given node(s). Given option prop: property or attr: attribute it is able to remove property or attribute from the DOM node.

Waits for the browser to finish the changes and returns socket so it can be stacked.

Options:

  • class: class - class name to be deleted
  • prop: property - property to be removed from selected node(s)
  • attr: attribute - attribute to be deleted from selected node(s)
  • from: selector - DOM selector

Example:

socket |> delete(".btn")       # remove all `.btn`
socket |> delete(from: "code") # empty all `<code>`, but node remains
socket |> delete(class: "btn-success", from: "#button")



node to be cleared


node to be removed

execute/2

Execute given jQuery method on selector. To be used in case built-in method calls are not enough.

Waits for the browser to finish the changes and returns socket so it can be stacked.

socket |> execute(:click, on: "#mybutton")
socket |> execute(trigger: "click", on: "#mybutton")
socket |> execute("trigger("click")", on: "#mybutton")






Drab.Modal

Drab Module to launch Bootstrap Modal in the browser.

Requires Bootstrap to work. Because there are differences beetween Bootstrap 3 and 4, you should configure which version you use (by default it is :bootstrap3):

config :drab, :modal_css, :bootstrap4


alert/4

Modal, synchronous alert box. This function shows Bootstrap modal window on the browser and waits for the user input.

Parameters:

  • title - title of the message box
  • body - html with the body of the alert box. When contains input, selects, etc, this function return their values

Options:

  • class - additional classes to .modal-dialog, ex. modal-lg, modal-sm, modal-xs
  • buttons - list of name/text of the buttons (:ok, :cancel are only available names by default; you need to create a template if you want more buttons), eq. [ok: “Yes”, cancel: “No”]
  • timeout - in milliseconds - after this time modal window will close and the function will return {:cancel, _}

Returns a tuple {clicked_button, params}, where:

  • clicked_button is an atom of :ok or :cancel. Notice that pressing esc or closing the modal window will return :cancel, while pressing enter returns :ok
  • params: Map %{name|id => value} of all inputs, selects, etc which are in the alert box body. Uses name attribute as a key, or id, when there is no name. If there is no id or name, this form value will not be included to the output.

Examples:

socket |> alert("Title", "Shows this message with default OK button")

# Yes/No requester, returns :ok or :cancel
{button, _} = socket |> alert("Message", "Sure?", buttons: [ok: "Azaliż", cancel: "Ney"])

# messagebox with two input boxes in body
form = "<input name='first'><input id='second'>"
name = case socket |> alert("What's your name?", form, buttons: [ok: "OK", cancel: "No"]) do
  { :ok, params } -> "#{params["first"]} #{params["second"]}"
  { :cancel, _ }  -> "anonymous"
end

Templates used to generate HTML for the alert box could be found in deps/drab/priv/templates/drab/. If you want to modify it, copy them to priv/templates/drab in your application. There are two templates for default :ok and :cancel buttons, but you may create new one and use them in the same way. For example, to have a new button called unspecified create a template priv/templates/drab/modal.alert.button.unspecified.html.eex:

<button id="_drab_modal_button_unspecified" name="unspecified" type="button"
 class="btn btn-default drab-modal-button" data-dismiss="modal">
    <%= label %>
</button>

The button must have drab-modal-button class and its name should correspond to key in buttons list. Now you can use your button in the same way as :ok and :cancel

{button, _} =
  socket |> alert("3 buttons", "Choice?",
            buttons: [ok: "Yes", cancel: "No", unspecified: "Don't know"])












Drab.Waiter

Enables Drab Waiter functionality - synchronous wait for browser events in the Commander handler function.

This module is optional and is not loaded by default. You need to explicitly declare it in the commander:

use Drab.Commander, modules: [Drab.Waiter]

Introduces DSL for registering events. Syntax:

waiter(socket) do
  on "selector1", "event_name", fn (sender) ->
  end
  on "selector2", "event_name", fn (sender) ->
  end
  on_timeout 5000, fn -> end
end


Drab.Controller

Set up Drab options for pages generated by the controller.

By default Drab searches for the Commander with the name corresponding to the Controller, eg. NameController - NameCommander. You may specify the commander module by using commander or commanders option:

use Drab.Controller, commander: MyApp.NameCommander

See also Drab.Commander


Drab.Commander

Drab Commander is a module to keep event handler functions.

All the Drab functions (callbacks, event handlers) are placed in the module called Commander. Think about it as a controller for the living pages. Commanders should be placed in the web/commanders directory. They should have a corresponding controller, except the shared commander.

defmodule DrabExample.PageCommander do
  use Drab.Commander

  defhandler click_button_handler(socket, sender) do
    ...
  end

  defhandler click_button_handler(socket, sender, optional) do
    ...
  end
end

Remember the difference: controller renders the page while commander works on the living stuff.

Event handler functions

Event handler is the function which process the request coming from the browser. It is done by running JS method Drab.exec_elixir() or from the DOM object with drab attribute. See Drab.Core, section Events, for a more description.

The event handler function receives two or three parameters:

The sender map:

%{
  "id"      => "sender object ID attribute",
  "name"    => "sender object 'name' attribute",
  "class"   => "sender object 'class' attribute",
  "text"    => "sender node 'text'",
  "html"    => "sender node 'html', result of running .html() on the node",
  "value"   => "sender object value",
  "data"    => "a map with sender object 'data-xxxx' attributes, where 'xxxx' are the keys",
  "event"   => "a map with choosen properties of `event` object"
  "drab_id" => "internal"
  "form"    => "a map of values of the sourrounding form"
  :params   => "a map of values of the sourrounding form, normalized to plug params"
}

The event map contains choosen properties of event object:

altKey, data, key, keyCode, metaKey, shiftKey, ctrlKey, type, which,
clientX, clientY, offsetX, offsetY, pageX, pageY, screenX, screenY

Example:

defhandler button_clicked(socket, sender) do
  # using Drab.Query
  socket |> update(:text, set: "clicked", on: this(sender))
end

sender may contain more fields, depending on the used Drab module. Refer to module documentation for more.

Event handlers are running in their own processes, and they are linked to the channel process. This means that in case of disconnect or navigate away from the page, event handler processes are going to terminate. But please be aware that the process terminates just after the handler finish - and it terminates with the :normal state, which means all the linked processes are not going to stop. If you run infinite loop with spawn_link from the handler, and the handler finish normally, the loop will be unlinked and will stay with us forever.

The only functions defined with defhandler/2 or public/1 are considered as handlers.

For the safety, you must declare your function in the commander as a handler, using defhandler/2 or public/1 macro.

Shared commanders

By default, only the page rendered with the corresponding controller may run handler functions in the commander. But there is a possibility to create a shared commander, which is allowed to run from any page.

defmodule DrabExample.SharedCommander do
  use Drab.Commander

  defhandler click_button_handler(socket, sender) do
    ...
  end
end

To call the shared commander function from page generated with the different controller, you need to specify its full path”.

<button drab-click="DrabExample.SharedCommander.click_button_handler">Clickety</button>

If you want to restrict shared commander for only specified controller, you must use before_handler/1 callback with controller/1 and action/1 functions to check out, where the function is calling from.

Define Shared Commander with drab-commander attribute on all children nodes

If you add drab-commander attribute to any tag, all children of this tag will use Shared Commander defined in this tag. Notice it will not redefine nodes, which already has Shared Commander defined.

Thus this:

<div drab-commander="DrabExample.SharedCommander">
  <button drab-click="button1_clicked">1</button>
  <button drab-click="button2_clicked">1</button>
  <button drab-click="DrabExample.AnotherCommander.button3_clicked">1</button>
</div>

is equivalent of:

<div>
  <button drab-click="DrabExample.SharedCommander.button1_clicked">1</button>
  <button drab-click="DrabExample.SharedCommander.button2_clicked">1</button>
  <button drab-click="DrabExample.AnotherCommander.button3_clicked">1</button>
</div>

See Drab.Core.this_commander/1 to learn how to use this feature to create reusable Drab components. See also Drab.Live to learn how shared commanders works with living assigns.

Callbacks

Callbacks are an automatic events which are launched by the system. They are defined by the macro in the Commander module:

defmodule DrabExample.PageCommander do
  use Drab.Commander

  onload :page_loaded
  onconnect :connected
  ondisconnect :disconnected

  before_handler :check_status
  after_handler  :clean_up, only: [:perform_long_process]

  def page_loaded(socket) do
    ...
  end

  def connected(socket) do
    ...
  end

  def disconnected(store, session) do
    # notice that this callback receives store and session, not socket
    # this is because socket is not available anymore (Channel is closed)
    ...
  end

  def check_status(socket, sender) do
    # return false or nil to prevent event handler to be launched
  end

  def clean_up(socket, dom_sender, handler_return_value) do
    # this callback gets return value of the corresponding event handler
  end
end

Notice that the order of callbacks is not guaranteed, they are all running in the separate processes, and are processing in the same time.

onconnect

Launched every time client browser connects to the server, including reconnects after server crash, network broken etc

onload

Launched only once after page loaded and connects to the server - exactly the same like onconnect, but launches only once, not after every reconnect

ondisconnect

Launched every time client browser disconnects from the server, it may be a network disconnect, closing the browser, navigate back. Disconnect callback receives Drab Store as an argument

before_handler

Runs before the event handler. If any of before callbacks return false or nil, corresponding event will not be launched. If there are more callbacks for specified event handler function, all are processed in order or appearance, then system checks if any of them returned false.

Can be filtered by :only or :except options:

before_handler :check_status, except: [:set_status]
before_handler :check_status, only:   [:update_db]

after_handler

Runs after the event handler. Gets return value of the event handler function as a third argument. Can be filtered by :only or :except options, analogically to before_handler

Using callbacks to check user permissions

Callbacks are handy for security. You may retrieve controller name and action name from the socket with controller/1 and action/1.

before_handler :check_permissions
def check_permissions(socket, _sender) do
  if controller(socket) == MyApp.MyController && action(socket) == :index do
    true
  else
    false
  end
end

Callbacks in Shared Commanders

Handler-specific callbacks used in the Shared Commander works as expected - they are raised before or after the event handler function, and might work regionally (if they are called from inside the tag which has drab-commander attibute).

However, page-specific callbacks (eg. onload) do not work regionally, as there is no specific object, which triggered the event. Thus, Drab.Core.this_commander/1 can’t be used there.

Broadcasting options

All Drab function may be broadcasted. By default, broadcasts are sent to browsers sharing the same page (the same url), but it could be override by broadcasting/1 macro.

Modules

Drab is modular. You my choose which modules to use in the specific Commander by using :module option in use Drab.Commander directive. There is one required module, which is loaded always and can’t be disabled: Drab.Code. By default, modules Drab.Live, Drab.Element and Drab.Modal are loaded. The following code:

use Drab.Commander, modules: [Drab.Query]

will override default modules, so only Drab.Core and Drab.Query will be available.

Every module has its corresponding JS template, which is loaded only when module is enabled. This is why it is good to keep the module list as short as it is possible, if you are not using them.

You may override the default modules list with the :default_modules config option:

config :drab, :default_modules, [Drab.Query]

Using templates

Drab injects function render_to_string/2 into your Commander. It is a shorthand for Phoenix.View.render_to_string/3 - Drab automatically chooses the current View.

Examples:

buttons = render_to_string("waiter_example.html", [])

Generate the Commander

There is a mix task (Mix.Tasks.Drab.Gen.Commander) to generate skeleton of commander:

mix drab.gen.commander Name

See also Drab.Controller


Drab.Socket

Drab operates on websockets. To enable it, you need to tell your application’s socket module to use Drab. For this, you will need to modify the socket module (by default it is UserSocket in web/channels/user_socket.ex).

There are two ways to archive this: let the Drab do the stuff, or provide your own connect/2 callback. First method is good for the application without socket level authentication. Second one is more elaborate, but you could provide check or socket modification while connect.

Method 1: Inject the code with use Drab.Socket

The straightforward one, you only need to inject the Drab.Socket module into your Socket (by default it is UserSocket in web/channels/user_socket.ex):

defmodule MyApp.UserSocket do
  use Phoenix.Socket
  use Drab.Socket
  ...
end

This creates a channel “__drab:*” used by all Drab operations.

You may create your own channels inside a Drab Socket, but you can’t provide your own connect callback. Drab Client (on JS side) always connects when the page loads and Drab’s built-in connect callback intercepts this call. If you want to pass the parameters to the Channel, you may do it in Drab.Client.run/2, they will appear in Socket’s assigns. Please visit Drab.Client to learn more.

This method is supposed to be used with Drab.Client.run/2 JS code generator.

Method 2: Use your own connect/2 callback

In this case, you must not add use Drab.Socket into your UserSocket. Instead, use the following code snippet:

defmodule MyApp.UserSocket do
  use Phoenix.Socket

  channel "__drab:*", Drab.Channel

  # For Phoenix <= 1.3
  def connect(params, socket) do
    Drab.Socket.verify(socket, params)
  end

  # For Phoenix 1.4
  def connect(params, socket, _connect_info) do
    Drab.Socket.verify(socket, params)
  end
end

Drab.Socket.verify/2 returns tuple {:ok, socket} or :error, where socket is modified with Drab internal assigns, as well as with the additional assigns you may pass to Drab.Client.generate/2.

This method is supposed to be used with Drab.Client.generate/2 JS code generator, followed by the javascript Drab.connect({token: ...}), or with Drab.Client.run/2 with additional assigns.

The following example adds "auth_token" => "forty-two" key-value pair to params in the connect/2 callback:

### app.html.eex
<%= Drab.Client.generate(@conn) %>
<script>
  if (window.Drab) Drab.connect({auth_token: "forty-two"});
</script>

Please do not forget to verify Drab token, even when using external authorization library:

### user_socket.ex
def connect(%{"auth_token" => auth_token} = params, socket) do
  case MyAuthLib.authorize(auth_token) do
    {:ok, authorized_socket} -> Drab.Socket.verify(authorized_socket, params)
    _ -> :error
  end
end
def connect(_, _), do: error

Please visit Drab.Client for more detailed information.

Configuration Options

By default, Drab uses “/socket” as a path. In case of using different one, configure it with:

config :drab, MyAppWeb.Endpoint,
  socket: "/my/socket"

This entry must correspond with the entry in your endpoint.ex.


Drab.Client

Enable Drab on the browser side. Must be included in HTML template, for example in web/templates/layout/app.html.eex:

<%= Drab.Client.run(@conn) %>

after the line which loads app.js:

<script src="<%= static_path(@conn, "/js/app.js") %>"></script>

at the very end of the layout (after template rendering functions).

Own channels inside the Drab’s socket

On the browser side, there is a global object Drab, which you may use to create your own channels inside Drab Socket:

ch = Drab.socket.channel("mychannel:whatever")
ch.join()

Custom socket constructor (Webpack “require is not defined” fix)

If you are using JS bundler other than default brunch, the require method may not be availabe as global. In this case, you might see the error:

require is not defined

in the Drab’s javascript, in line:

this.Socket = require("phoenix").Socket;

In this case, you must provide it. In the app.js add a global variable, which will be passed to Drab later:

window.__socket = require("phoenix").Socket;

Then, tell Drab to use this instead of default require("phoenix").Socket. Add to config.exs:

config :drab, MyAppWeb.Endpoint,
  js_socket_constructor: "window.__socket"

This will change the problematic line in Drab’s javascript to:

this.Socket = window.__socket;

Drab JS client API

Drab.connect(token_object)

Connects to the Drab’s websocket. Must be called after injecting JS code with Drab.Client.generate/2:

<%= Drab.Client.generate(@conn) %>
<script>
  if (window.Drab) Drab.connect({auth_token: window.my_token});
</script>

Drab.exec_elixir(elixir_function_name, argument, callback)

Run elixir function (which must be a handler in the commander) from the browser side.

Arguments:

Function name may be given with the commander name, like “MyApp.MyCommander.handler_function”, or the function name only: “handler_function”. In this case the corresponding commander module will be used. This function must be marked as public with Drab.Commander.public/1 or defhandler macro.

Returns:

Example:

<button onclick="Drab.exec_elixir('clicked', {click: 'clickety-click'});">
  Clickme
</button>

The code above runs function named clicked in the corresponding Commander, with the argument %{"click" => "clickety-click}"

Drab.enable_drab_on(selector_or_node)

Evaluates DOM to set up Drab events.

Called automatically on page load for the whole document. You need to call it after adding/changing html fragments from the client side. No need to call it when updating html with Drab commands (poke, insert_html, etc).

Arguments:

Return:

js/1

Generates JS code and runs Drab.

Passes controller and action name, tokenized for safety. Works only when the controller, which renders the current action, has a corresponding commander, or has been compiled with use Drab.Controller.

Optional argument may be a list of parameters which will be added to assigns to the socket. Example of layout/app.html.eex:

<%= Drab.Client.run(@conn) %>
<%= Drab.Client.run(@conn, user_id: 4, any_other: "test") %>

Please remember that your parameters are passed to the browser as Phoenix Token. Token is signed, but not ciphered. Do not put any secret data in it.


Mix.Tasks.Drab.Gen.Commander

Generates a Drab commander.

mix drab.gen.commander Name
mix drab.gen.commander Context/Name

This will generate a module NameCommander in web/commanders, if NameController is already present.