Drab - Access the browser User Interface from the Server Side. No Javascript programming needed anymore!
Drab extends Phoenix Framework to "remote control" UI on the browser, live. The idea is to move all User Interface logic to the server-side, to eliminate Javascript and Ajax calls.
<div class="progress">
<div class="progress-bar <%= @progress_bar_class %>" role="progressbar" @style.width=<%= "#{@bar_width}%" %>>
<%= "#{@bar_width}%" %>
</div>
</div>
<button class="btn btn-primary" drab-click="perform_long_process">
<%= @long_process_button_text %>
</button>
defhandler perform_long_process(socket, _sender) do
poke socket, progress_bar_class: "progress-bar-danger", long_process_button_text: "Processing..."
steps = :rand.uniform(100)
for i <- 1..steps do
Process.sleep(:rand.uniform(500)) #simulate real work
poke socket, bar_width: Float.round(i * 100 / steps, 2)
end
poke socket, progress_bar_class: "progress-bar-success", long_process_button_text: "Click me to restart"
end
Elixir ~> 1.6.0 (see Installation Guide)
Phoenix ~> 1.2 (see Installation Guide)
This has the same requirements as Phoenix.Sockets. Above that it depends on the javascript you call or other potential tools you use on top of Drab.
First of all, you need to have a Phoenix application, on top of which you will install Drab. If this is a standard app, generated with mix phx.new
, you may use Drab Installer to make it running in one, simple step. Otherwise, see Manual Installation section below.
mix.exs
in the main folder in your web application (if you have multiple application under an umbrella, this is the one ending with _web
). Locate function deps
(search for def deps
string). Add an entry {:drab, "~> 0.10.0"}
to the list. Don't forget about comma!def deps do
[
{...},
{:drab, "~> 0.10.0"}
]
end
$ mix deps.get
mix drab.install
:bash% mix drab.install
Checking prerequisites for :my_app
lib/my_app_web/templates/layout/app.html.eex
lib/my_app_web/channels/user_socket.ex
config/config.exs
config/dev.exs
The installer is going to modify those files. OK to proceed? [Yn] Y
Drab has been successfully installed in your Phoenix application.
Now it is time to create your first commander, for example, for PageController:
mix drab.gen.commander Page
Congratulations! You have installed Drab and you can proceed with your own commander.
Please notice that Drab will run only on pages, which have the corresponding commander.
All the Drab functions (callbacks, event handlers) are placed in the module called Commander
.
Think about it as a controller for the live pages. Commanders should be placed in web/commanders
directory.
To enable Drab on the pages generated with corresponding controller, you need to create a twin commander. For example, for MyApp.PageController
the commander should be named MyApp.PageCommander
.
Remember the difference: controller
renders the page, while commander
works on the live page.
PageController
should have PageCommander
:$ mix drab.gen.commander Page
* creating web/commanders/page_commander.ex
@welcome_text
assign to render/3
in index action in the controller, to be used in the future:defmodule MyApp.PageController do
use Example.Web, :controller
def index(conn, _params) do
render conn, "index.html", welcome_text: "Welcome to Phoenix!"
end
end
Rename the template from web/templates/page/index.html.eex
to index.html.drab
Edit the template web/templates/page/index.html.drab
and change the fixed welcome text to the assign:
<div class="jumbotron">
<h2><%= @welcome_text %></h2>
web/commanders/page_commander.ex
and add some real action - the onload
callback, which fires when the browser connects to the Drab server:defmodule DrabExample.PageCommander do
use Drab.Commander
onload :page_loaded
# Drab Callbacks
def page_loaded(socket) do
poke socket, welcome_text: "This page has been drabbed"
set_prop socket, "div.jumbotron p.lead",
innerHTML: "Please visit <a href='https://tg.pl/drab'>Drab</a> page for more"
end
end
The poke/2
function updates the assign. The set_prop/3
updates any property of the DOM object. All is done live, without reloading the page.
iex -S mix phoenix.server
. Go to http://localhost:4000
to see the changed web page. Now you may play with this page live, directly from iex
! Observe the instruction given when your browser connects to the page:[debug]
Started Drab for same_path:/, handling events in DrabExample.PageCommander
You may debug Drab functions in IEx by copy/paste the following:
import Drab.{Core, Element, Live}
socket = Drab.get_socket(pid("0.653.0"))
Examples:
socket |> exec_js("alert('hello from IEx!')")
socket |> poke(count: 42)
As instructed, copy and paste those two lines, and check out yourself how you could remotely control the displayed page:
iex> alert socket, "Alert title", "Do you like modals?", buttons: [ok: "A juści", cancel: "Poniechaj"]
{:ok, %{}}
iex> poke socket, welcome_text: "WOW, this is nice"
%Phoenix.Socket{...}
iex> query socket, "div.jumbotron h2", :innerText
{:ok,
%{"[drab-id='425d4f73-9c14-4189-992b-41539377c9eb']" => %{"innerText" => "WOW, this is nice"}}}
iex> set_style socket, "div.jumbotron", backgroundColor: "red"
{:ok, 1}
Visit Demo Page for a live demo and more description.
Visit Documentation page.
There is a Drab's thread on elixirforum.com, please address questions there.
Since 0.3.2, Drab is equipped with its own Phoenix Server for running integration tests automatically, for sandboxing and for playing with it.
git clone [email protected]:grych/drab.git
cd drab
mix deps.get
npm install && node_modules/brunch/bin/brunch build
iex -S mix phoenix.server
open the browser and navigate to http://localhost:4000
follow the instructions in IEx to play with Drab functions:
import Drab.{Core, Live, Element, Query, Waiter}
socket = Drab.get_socket(pid("0.784.0"))
iex> query socket, "h3", :innerText
{:ok,
%{"#header" => %{"innerText" => "Drab Tests"},
"#page_loaded_indicator" => %{"innerText" => "Page Loaded"}}}
iex> set_prop socket, "h3", innerText: "Updated from IEx"
{:ok, 2}
iex> exec_js socket, "alert('You do like alerts?')"
{:ok, nil}
Most of the Drab tests are integration (end-to-end) tests, thus they require automated browser. Drab uses chromedriver, which must be running while you run tests.
git clone [email protected]:grych/drab.git
cd drab
mix deps.get
npm install && node_modules/brunch/bin/brunch build
run chromedriver
run tests:
% mix test
Compiling 23 files (.ex)
........................................
Finished in 120.9 seconds
123 tests, 0 failures
Randomized with seed 934572
Add Drab to the dependencies in mix.exs
.
Initialize Drab client library by adding to the layout page (app.html.eex
- or any other layout you use).
<%= Drab.Client.run(@conn) %>
just after the following line:
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
user_socket.ex
:use Drab.Socket
config.exs
:config :phoenix, :template_engines,
drab: Drab.Live.Engine
config :drab, MyAppWeb.Endpoint,
otp_app: :my_app_web
:drab
to applications started by default in mix.exs
:def application do
[mod: {MyApp, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :drab]]
end
It is not needed if you are running Phoenix 1.3
.drab
extension to live reload patterns in dev.exs
:config :my_app, MyApp.Endpoint,
live_reload: [
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{web/views/.*(ex)$},
~r{web/templates/.*(eex|drab)$}
]
]
require is not defined
error. You need to provide Socket
: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"
jquery
and boostrap
to package.json
:"dependencies": {
"jquery": ">= 3.1.1",
"bootstrap": "~3.3.7"
}
brunch-config.js
:npm: {globals: {
$: 'jquery',
jQuery: 'jquery',
bootstrap: 'bootstrap'
}}
$ npm install && node_modules/brunch/bin/brunch build
(c)2016-2018 Tomek "Grych" Gryszkiewicz, [email protected]
Illustrations by https://www.vecteezy.com