drab v0.10.5 Drab.Core View Source

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>
  • event_name is the DOM event name, eg. “click”, “blur”
  • event_handler_function_name - the name of the event handler function in the commander on the server side
  • options - optional, so far the only available option is “debounce(milliseconds)” for “keyup” event
  • argument - optional, additional argument to be passed to the event handler function as a third argument

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.

  • By default, Drab Store is kept in browser Local Storage. This means it is gone when you close the browser or the tab. You may set up where to keep the data with drab_store_storage config entry, see Drab.Config
  • Drab Store is not the Plug Session! This is a different entity. Anyway, you have an access to the Plug Session (details below).
  • Drab Store is stored on the client side and it is encrypted.

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:

Link to this section Summary

Types

Return value of broadcast_js/2

Input: binary string or safe

Return value of exec_js/2

Types returned from the browser

Returned status of all Core operations

Subject for broadcasting

Functions

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

Synchronously executes the given javascript on the client side

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

Returns the value of the Plug Session represented by the given key or default, when key not found

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

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

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

Helper for broadcasting functions, returns topic for a given controller and action

Helper for broadcasting functions, returns topic for a given endpoint, controller and action

Helper for broadcasting functions, returns topic for a given controller

Helper for broadcasting functions, returns topic for a given endpoint and controller

Helper for broadcasting functions, returns topic for a given URL path

Helper for broadcasting functions, returns topic for a given endpoint and URL path

Helper for broadcasting functions, returns topic for a given topic string

Helper for broadcasting functions, returns topic for a given topic string

Like this/1, but returns selector of the object ID

Returns the selector of object, which triggered the event. To be used only in event handlers

Returns the unique selector of the DOM object, which represents the shared commander of the event triggerer

Link to this section Types

Link to this type bcast_result() View Source
bcast_result() :: {:ok, term()} | {:error, term()}

Return value of broadcast_js/2

Input: binary string or safe

Link to this type result() View Source
result() :: {status(), return() | :disconnected | :timeout}

Return value of exec_js/2

Link to this type return() View Source
return() :: String.t() | map() | float() | integer() | list() | boolean()

Types returned from the browser

Link to this type status() View Source
status() :: :ok | :error

Returned status of all Core operations

Subject for broadcasting

Link to this section Functions

Link to this function broadcast_js(socket, javascript, options \\ []) View Source
broadcast_js(subject(), input(), Keyword.t()) :: bcast_result()

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}

Link to this function exec_js!(socket, javascript, options \\ []) View Source
exec_js!(Phoenix.Socket.t(), input(), Keyword.t()) :: return() | no_return()

Exception raising version of exec_js/2

Examples

  iex> socket |> exec_js!("2 + 2")
  4

  iex> socket |> exec_js!("nonexistent")
  ** (Drab.JSExecutionError) nonexistent is not defined
      (drab) lib/drab/core.ex:100: Drab.Core.exec_js!/2

  iex> socket |> exec_js!("for(i=0; i<1000000000; i++) {}")
  ** (Drab.JSExecutionError) timeout
      (drab) lib/drab/core.ex:100: Drab.Core.exec_js!/2

  iex> socket |> exec_js!("for(i=0; i<10000000; i++) {}", timeout: 1000)
  ** (Drab.JSExecutionError) timeout
      lib/drab/core.ex:114: Drab.Core.exec_js!/3
Link to this function exec_js(socket, javascript, options \\ []) View Source
exec_js(Phoenix.Socket.t(), input(), Keyword.t()) :: result()

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}
Link to this function get_session(socket, key) View Source
get_session(Phoenix.Socket.t(), atom()) :: term()

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]
Link to this function get_session(socket, key, default) View Source
get_session(Phoenix.Socket.t(), atom(), term()) :: term()

Returns the value of the Plug Session represented by the given key or default, when key not found.

counter = get_session(socket, :userid, 0)

See also get_session/2.

Link to this function get_store(socket, key) View Source
get_store(Phoenix.Socket.t(), atom()) :: term()

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

uid = get_store(socket, :user_id)
Link to this function get_store(socket, key, default) View Source
get_store(Phoenix.Socket.t(), atom(), term()) :: term()

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

counter = get_store(socket, :counter, 0)
Link to this function put_store(socket, key, value) View Source
put_store(Phoenix.Socket.t(), atom(), term()) :: Phoenix.Socket.t()

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

put_store(socket, :counter, 1)
Link to this function same_action(controller, action) View Source
same_action(String.t() | atom(), String.t() | atom()) :: String.t()

Helper for broadcasting functions, returns topic for a given controller and action.

iex> same_action(DrabTestApp.LiveController, :index)
"controller:Elixir.DrabTestApp.LiveController#index"
Link to this function same_action(endpoint, controller, action) View Source
same_action(atom(), String.t() | atom(), String.t() | atom()) ::
  {atom(), String.t()}

Helper for broadcasting functions, returns topic for a given endpoint, controller and action.

To be used in multiple endpoint environments only.

iex> same_action(DrabTestApp.Endpoint, DrabTestApp.LiveController, :index)
{DrabTestApp.Endpoint, "controller:Elixir.DrabTestApp.LiveController#index"}
Link to this function same_controller(controller) View Source
same_controller(String.t() | atom()) :: String.t()

Helper for broadcasting functions, returns topic for a given controller.

iex> same_controller(DrabTestApp.LiveController)
"__drab:controller:Elixir.DrabTestApp.LiveController"
Link to this function same_controller(endpoint, controller) View Source
same_controller(atom(), String.t() | atom()) :: {atom(), String.t()}

Helper for broadcasting functions, returns topic for a given endpoint and controller.

To be used in multiple endpoint environments only.

iex> same_controller(DrabTestApp.Endpoint, DrabTestApp.LiveController)
{DrabTestApp.Endpoint, "__drab:controller:Elixir.DrabTestApp.LiveController"}

Helper for broadcasting functions, returns topic for a given URL path.

iex> same_path("/test/live")
"__drab:same_path:/test/live"
Link to this function same_path(endpoint, url) View Source
same_path(atom(), String.t()) :: {atom(), String.t()}

Helper for broadcasting functions, returns topic for a given endpoint and URL path.

To be used in multiple endpoint environments only.

iex> same_path(DrabTestApp.Endpoint, "/test/live")
{DrabTestApp.Endpoint, "__drab:same_path:/test/live"}
Link to this function same_topic(topic) View Source
same_topic(String.t()) :: String.t()

Helper for broadcasting functions, returns topic for a given topic string.

Drab broadcasting topics are different from Phoenix topic - they begin with “__drab:”. This is because you can share Drab socket with you own one.

iex> same_topic("mytopic")
"__drab:mytopic"
Link to this function same_topic(endpoint, topic) View Source
same_topic(atom(), String.t()) :: {atom(), String.t()}

Helper for broadcasting functions, returns topic for a given topic string.

To be used in multiple endpoint environments only.

Drab broadcasting topics are different from Phoenix topic - they begin with “__drab:”. This is because you can share Drab socket with you own one.

iex> same_topic(DrabTestApp.Endpoint, "mytopic")
{DrabTestApp.Endpoint, "__drab:mytopic"}

Like this/1, but returns selector of the object ID.

def button_clicked(socket, sender) do
  socket |> update!(:text, set: "alread clicked", on: this!(sender))
  socket |> update!(attr: "disabled", set: true, on: this!(sender))
end

Raises exception when being used on the object without an ID.

Returns the selector of object, which triggered the event. To be used only in event handlers.

def button_clicked(socket, sender) do
  set_prop socket, this(sender), innerText: "already clicked"
  set_prop socket, this(sender), disabled: true
end
Link to this function this_commander(sender) View Source
this_commander(map()) :: String.t()

Returns the unique selector of the DOM object, which represents the shared commander of the event triggerer.

In case the even was triggered outside the Shared Commander, returns “” (empty string).

To be used only in event handlers. Allows to create reusable Drab components.

<div drab-commander="DrabTestApp.Shared1Commander">
  <div class="spaceholder1">Nothing</div>
  <button drab-click="button_clicked">Shared 1</button>
</div>

def button_clicked(socket, sender) do
  set_prop socket, this_commander(sender) <> " .spaceholder1", innerText: "changed"
end