drab v0.10.5 Drab.Commander View Source

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:

  • socket - the websocket used to communicate back to the page
  • argument or sender - an argument used in JS Drab.exec_elixir() method; when lauching an event via drab=... atrribute, it is a map which describes the sender object
  • optional - optional argument which may be defined directly in HTML, with drab attribute

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

Link to this section Summary

Functions

Drab may allow an access to specified Plug Session values. For this, you must whitelist the keys of the session map. Only this keys will be available to Drab.Core.get_session/2

Retrieves action name in the controller, which rendered the page where handler is called from

Sets up the callback for after_handler. Receives handler function name as an atom and options

Sets up the callback for before_handler. Receives handler function name as an atom and options

Set up broadcasting listen subject for the current commander

Retrieves controller module, which generated the page the handler function is calling from, from the socket

Defines handler function

Returns list of external topics we subscribe

Sets up the callback for onconnect. Receives handler function name as an atom

Sets up the callback for ondisconnect. Receives handler function name as an atom

Sets up the callback for onload. Receives handler function name as an atom

Marks given function(s) as a handler(s). An alternative to defhandler/2

Subscribe to the external topic for broadcasting

Returns the current main broadcasting topic

Unsubscribe from the external topic

Link to this section Functions

Link to this macro access_session(session_keys) View Source (macro)

Drab may allow an access to specified Plug Session values. For this, you must whitelist the keys of the session map. Only this keys will be available to Drab.Core.get_session/2

defmodule MyApp.MyCommander do
  user Drab.Commander

  access_session [:user_id, :counter]
end

Keys are whitelisted due to security reasons. Session token is stored on the client-side and it is signed, but not encrypted.

Retrieves action name in the controller, which rendered the page where handler is called from.

Link to this macro after_handler(event_handler, filter \\ []) View Source (macro)

Sets up the callback for after_handler. Receives handler function name as an atom and options.

after_handler :event_handler_function

See Drab.Commander summary for details.

Link to this macro before_handler(event_handler, filter \\ []) View Source (macro)

Sets up the callback for before_handler. Receives handler function name as an atom and options.

before_handler :event_handler_function

See Drab.Commander summary for details.

Link to this macro broadcasting(subject) View Source (macro)

Set up broadcasting listen subject for the current commander.

It is used by broadcasting functions, like Drab.Element.broadcast_prop/3 or Drab.Query.insert!/2. When the browser connects to Drab page, it gets the broadcasting subject from the commander. Then, it will receive all the broadcasts coming to this subject.

Default is :same_path

Options:

  • :same_path (default) - broadcasts will go to the browsers rendering the same url
  • :same_controller - broadcasted message will be received by all browsers, which renders the page generated by the same controller
  • :same_action - the message will be received by the browsers, rendered with the same controller and action
  • "topic" - any topic you want to set, messages will go to the clients sharing this topic

Please notice that Drab topic is not the same as Phoenix topic, it always begins with “__drab:” string. This is because you may share the socket between Drab and your own communication. Thus, always use Drab.Core.same_topic/1 when broadcasting with Drab.

See Drab.Core.broadcast_js/2 for more.

Link to this function controller(socket) View Source
controller(Phoenix.Socket.t()) :: atom()

Retrieves controller module, which generated the page the handler function is calling from, from the socket.

Link to this macro defhandler(handler, list) View Source (macro)

Defines handler function.

Handler is the Elixir function which is called from the browser, as a response for an event or using JS function Drab.exec_elixir().

defmodule MyApp.MyCommander
  use Drab.Commander

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

Trying to run non-handler function from the browser raises the exception on the Phoenix side.

Link to this function external_topics(socket) View Source
external_topics(Phoenix.Socket.t()) :: [String.t()]

Returns list of external topics we subscribe.

This list does not contain the main topic, as set with broadcasting/1.

Link to this macro onconnect(event_handler) View Source (macro)

Sets up the callback for onconnect. Receives handler function name as an atom.

onconnect :event_handler_function

See Drab.Commander summary for details.

Link to this macro ondisconnect(event_handler) View Source (macro)

Sets up the callback for ondisconnect. Receives handler function name as an atom.

ondisconnect :event_handler_function

See Drab.Commander summary for details.

Link to this macro onload(event_handler) View Source (macro)

Sets up the callback for onload. Receives handler function name as an atom.

onload :event_handler_function

See Drab.Commander summary for details.

Link to this macro public(handler) View Source (macro)

Marks given function(s) as a handler(s). An alternative to defhandler/2.

defmodule MyApp.MyCommander
  use Drab.Commander
  public [:handler1, :handler2]

  def handler1(socket, sender) do
    ...
  end
end

Subscribe to the external topic for broadcasting.

Default broadcasting topic is set in the compile time with broadcasting/1 macro. Subscribing to the external topic may be done in the runtime.

If you have Drab.Presence configured, subscription to the topic runs presence tracker on this topic.

Please notice that you can’t subscribe to the main topic (set with broadcasting/1).

Returns :ok or :duplicate in case we are already subscribed to the external topic.

iex> subscribe(socket, same_action(MyApp.MyController, :index))
:ok
iex> subscribe(socket, same_topic("product_42"))
:ok
iex> subscribe(socket, same_topic("product_42"))
:duplicate

Returns the current main broadcasting topic.

Link to this function unsubscribe(socket, topic) View Source
unsubscribe(Phoenix.Socket.t(), Drab.Core.subject()) :: atom()

Unsubscribe from the external topic.

Unsubscription from the topic stops the presence tracker on it (if Drab.Presence is running).

Please notice that you can’t unsubscribe from the main topic (set with broadcasting/1).

iex> unsubscribe(socket, same_action(MyApp.MyController, :index))
:ok
iex> unsubscribe(socket, same_topic("product_42"))
:ok