Drab: remote controlled frontend framework for Phoenix

It is not documented (yet), but I was about to publish it as an official API (internally it is used by drab modules). You may register your own callbacks with functions from global Drab

Drab.on_disconnect(function() {console.log("disconnect");});

Similar is for on_connect and on_load.

Awesome, I’ll give that a try when next I’m working on it. :slight_smile:

It’s safe to just send that as javascript from elixir via on_connect or so?

1 Like

I would rather send it on load fro elixir, as with on connect you might end with multiple callbacks

1 Like

Also, what is the recommended way to kill the session when, say, they logged out, thus killing all their other open browser windows and connections. I kill the websocket entirely via disconnect but they are able to reconnect using the same token that Drab supplied, is there any way to mark it as invalid and prevent reconnection as I’m not able to override the socket initialization with drab?

1 Like

No, there is no such thing. Let’s do it then!
How would you do it with standard channels? Or how do you see the API?

Hmm, you are, or I misunderstood?

This is because of the max_age of the token, it is set to 86400. I am going to allow you to set it.

1 Like

A cache of sessions (I have my own API so as long as I can plug it in) to check against on websocket connections with the associated session token is how I do it. :slight_smile:

Ah right, that was added in later, it wasn’t possible for an early period of time there. ^.^

Actually I prefer very very short tokens but I also like to be able to send token updates. Like on another websocket connection that is long-lived a new token is sent across to the client periodically that they can use to reconnect with if the connection dies (I have it set to 15 minutes tokens refreshed after 10 minutes to allow a 5 minute delay of no-contact else a page refresh is required). Keeping a long-lived token means it can be re-used more easily.

EDIT: For note, the token usually just packs a session ID, of which ‘that’ also can independently be elapsed via the database/cache.

1 Like

I see. Now I think it is out of Drab scope, but we might build something like in a future, as it may be useful, especially for beginners!

Also, what I am still thinking of, and may be connected with your idea, is to build some abstraction (session) over the socket, which would be used to survive disconnections. So the handlers, instead of socket, would get some session (browser or even user) id, and if you run the background process (not linked to the handler) you may proceed even in case of the disconnection in the meantime.

You may do something like this now, just broadcasting to the topic, but it is one-way communication only - trying to read from the browser on disconnected socket returns {:error, :disconnected} and you must deal with it yourself.

It may also store the messages sent during disconnection, so after reconnect browser will run all remaining code.

Also, we could provide a “waiting for reconnect” API, like:

defhandler handle_event(session, _payload) do
  spawn fn ->
    do_some_long_process()
    x = query_one(session, "#input1", :value) do
       on {:ok, x} -> x
       after 50000 -> raise "gone forever"
    end
  end
end

But, as I said, it is rather a future stuff.

Yep, it was added after you persuaded me to do it :slight_smile:

1 Like

v0.9.1 is out - jQueryless Boostrap Modal

Hi guys,
I’ve just released v0.9.1, where the most important change is the Drab.Modal is not dependent on jQuery anymore! And because of this, I have included it to the default modules, so it works out of the box (important for beginners).

Modal can be extremely useful, when you need a user input. Imagine that your function runs some SQL update, and you want to ask whether we should commit or rollback. With Drab.Modal.alert it is easier than anything:

case alert(socket, "DB update", "#{no_rows} updated.", buttons: [ok: "Commit", cancel: "Rollback"]) do
  {:ok, _} -> commit(db, query)
  {:cancel, _} -> rollback(db, query)
end

Modal can also have some inputs inside, in this case it returns their values as a second element of the return tuple.

iex(5)> alert socket, "Title", "<input name='fname'><input name='lname'>"
{:ok, %{"fname" => "Grzegorz", "lname" => "Brzęczyszczykiewicz"}}

This is a bootstrap modal, so it obviously needs bootstrap. But everything is prepared to add more css frameworks. I will add Foundation support in the spare time.

Drab.Query is (and will be) the only one module which is jQuery dependent. Long way since the first version announced here almost two years ago, as Drab - the jQuery in Elixir :slight_smile:

7 Likes

I’m trying to setup drab in my existing app, but I just can’t get it to work. Followed every step in the installation guide and tried the simple PageController test. The only thing that shows when I run iex -S mix phx.server is

19:31:47.314 [info] Compiling Drab partial: lib/risk_web/templates/page/index.html.drab (giydamrugmztimrw)

But nothing changes in the template. Any ideas on what could be the problem? Phoenix version is 1.3 and elixir is 1.6.

I also checked the manual installation and everything was in place.

Elaborate? Like ‘how’ is the template not changing? You do only have a *.drab version of the template and not a *.eex version too yes? o.O

only a .drab version yes. Could it be related to the fact that I’m not using brunch but npm scripts?

I assume you mean that nothing change in the rendered page, as you are viewing the page source?

Do you have the PageCommander? You may generate it with

mix drab.gen.commander Page

Drab only starts on pages, which has the corresponding commander. PageController → PageCommander. Even if you only debug it with iex, it needs an “empty” commander.

This is because Drab JS it quite heavy, and should not be included to the pages which does not need it.

1 Like

This is the PageCommander

defmodule RiskWeb.PageCommander do
  use Drab.Commander
  # Place your event handlers here
  #
  # defhandler button_clicked(socket, sender) do
  #   set_prop socket, "#output_div", innerHTML: "Clicked the button!"
  # end
  #
  defhandler uppercase(socket, sender) do
    text = sender.params["text_to_uppercase"]
    poke(socket, text: String.upcase(text))
  end

  # Place you callbacks here
  #
  onload(:page_loaded)

  def page_loaded(socket) do
    poke(socket, msg: "This page has been drabbed")
    set_prop(socket, "div.jumbotron p.lead", innerText: "This page has been drabbed")
  end
end

This is the PageController

defmodule RiskWeb.PageController do
  use RiskWeb, :controller
  plug(:put_layout, {RiskWeb.LayoutView, "home.html"})

  def index(conn, _params) do
    render(conn, "index.html", msg: "hello", text: "helloworld")
  end
end

And these are the pieces in the index.html.drab template

 <p> <%= @msg %> </p>

<form>
  <input name="text_to_uppercase" value="<%= @text %>">
  <button drab="click:uppercase">Upcase</button>
  <button drab="click:downcase">Downcase</button>
</form>

I assume the @msg shown should be “This page has been drabbed” when the page loads and when clicking on the Upcase button the input text should transform

1 Like

This is where the issue may origin. Installer (and manual installation procedure) assumes you inject Drab to “app.html.eex”.

Can you see Drab’s javascripts when you check the generated page sources?

What is the home.html template and does it have the Drab client run call in it?

1 Like

home.html is the layout for the home page. Yeah that was definitely one problem, there was no Drab call in that layout template.
Now I tried with a controller that uses the app.html template and I get the following errors in the console:

Uncaught ReferenceError: require is not defined
        at Object.create (evaluations:229)
        at evaluations:1203
        at evaluations:1207

Which are

  window.Drab = {
    create: function (drab_return_token, drab_session_token, broadcast_topic) {
      this.Socket = require("phoenix").Socket; <-------------

      this.drab_return_token = drab_return_token;

and

  Drab.create('SFMyNTY.g3QAAAACZAAEZGF0YWwAAAAFaAJkAAxfX2NvbnRyb2xsZXJkACNFbGl4aXIuUmlza1dlYi5FdmFsdWF0aW9uQ29udHJvbGxlcmgCZAALX19jb21tYW5kZXJkACJFbGl4aXIuUmlza1dlYi5FdmFsdWF0aW9uQ29tbWFuZGVyaAJkAAZfX3ZpZXdkAB1FbGl4aXIuUmlza1dlYi5FdmFsdWF0aW9uVmlld2gCZAAIX19hY3Rpb25kAAVpbmRleGgCZAAJX19hc3NpZ25zampkAAZzaWduZWRuBgBZh9a0ZAE.bVBSJrPdCwG3HvemM8d99yMZ3iNpy4OjGAG21O5FS08', 'QTEyOEdDTQ.FiF0kbVXbsSjWYIL0rs2ZynwnJhd_nUcQOzZQlGtdSbYcPD-cNeg-08kRC0.m1HmEJ5NUBJExlq5.vdxfgTut.9QL16o9CC5vHzjwlgFKHtg', '__drab:same_path:/evaluations');
  
    Drab.connect();
  
})();

Solved! I found someone had that issue too here: Drab webpack problem - Uncaught ReferenceError: require is not defined.
In Drab.Client there is the solution to this problem:

## 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;

Thank you! And great work with Drab!

1 Like

I have updated README to clearly indicate that you need to do it for all layouts you use. I think it may be good for installer to warn if it finds multiple layouts, or install it in all of them.

Actually I am not happy with the way how Drab injects its javascripts, and this workaround for the require is not defined. But I could not find a better solution. Javascripts generated by the Drab.Client are not static - for example, token needs to be included.

2 Likes

When it really should be passable via an httponly cookie holding a single-use token
 >.>

Would need a new websocket driver then since Phoenix doesn’t allow that (see other recent thread for the reason which I still don’t understand why it’s used
).

Hmm, you could stuff a single-use token into a javascript-accessible single-request cookie (immediate timeout) that is then pulled out from static javascript that could then be bundled up as normal


1 Like