Drab: remote controlled frontend framework for Phoenix

Issue solved. Actually it was not an problem with Drab, but with the Demo page. I will describe it, as it was quite an interesting case.

On the demo page there is a part which shows you /var/log/nginx/access.log in a realtime (to show how the server-side events can manipulate User Interface). This functionality uses Sentix which is a wrapper around command-line fswatch. This is a demo, so the code should be easiest possible, not have to be efficient or sophisticated.

This weekend I moved everything (not only Drab, also some other projects) to the new server, running Debian. The old one was running on 10-years old Mac Mini (OSX Lion). Movement was quite smooth, everything worked so after few tests Iā€™ve updated the DNS.

Then I realized that the beam process of Drab Demo eats a memory, and it is growing very fast. Strange, as the same stuff worked without any issue on OSX. I recompiled Erlang and Elixir to the exact same version as on the old machine, without success. So I started :etop and tried to analyze whatā€™s going on internally. And Iā€™ve found the massive (hundreds of thousands and quickly growing) messages queued on some Drab processes.

The only difference now was fswatch - on OSX I used kqueue, on Linux - inotify. And bingo - that was the issue. The difference is that kqueue monitors file updates only (by default) and inotify is more complex: it can monitor every access to the file (describing it as :platform_specific).

The code of the demo page is simple, it runs an infinite loop and waits for messages when fswatch find that the file was accesses:

  Sentix.start_link(:watcher, ["/var/log/nginx/access.log"], monitor: :intotify_monitor, latency: 1)
  Sentix.subscribe(:watcher)
  file_change_loop(socket, file)

  defp file_change_loop(socket, file_path) do
    receive do
      {_pid, {:fswatch, :file_event}, {^file_path, _opts}} ->
        socket |> update(:text, set: last_n_lines(file_path, 8), on: "#log_file")
    end
    file_change_loop(socket, file_path)
  end

It worked perfectly on OSX, because fswatch sent :updated message only when log file was changed. But on Linux, by default, it sents :platform_specific message also when nginx access the file without updating itā€¦

So what happened: when access log file was accesses/updated/touched, process received and message andā€¦ sent websocket message back (with socket |> update(:text, set: last_n_lines(file_path, 8), on: "#log_file")) to update #log_file div. While doing it, Nginx access the log file, which issue fsevent - and it finished with the classic positive feedback.

Solution was very simple, I just need to monitor only :update events from fswatch:

 Sentix.start_link(:watcher, [file], monitor: monitor, latency: 1, filter: [:updated])

What is very interesting in this situation is that the server survived it and was able to handle all requests. Until it crash few hours later, because of the exhausted memory :slight_smile:

5 Likes

Girls and boys,
Iā€™ve just released v0.3.0 of Drab - the addition to Phoenix to live control of browserā€™s User Interface from Elixir, from the server-side. This version comes with few API changes, new features and many bug fixes.

Waiters

First at all, 0.3.0 introduces Drab Waiter: the functionality which allows you to wait for the user input. That could be useful, when - for example - you made a changes in the database, and want to ask the user what to do: rollback or commit? (forgive the pseudocode, itā€™s just an example).

Database.sql(conn, "UPDATE users SET password='';")
waiter(socket) do
  on "#commit_button", "click", fn (sender) ->
    Database.commit(conn)
  end
  on "#rollback_button", "click", fn (sender) ->
    Database.rollback(conn)
  end
  on_timeout 5000, fn -> 
    Database.rollback(conn)
  end
end

This function will stop processing until user press one of the buttons - or until timeout.

Drab.Query.select API changed

The second major change is the Drab.Query.select API. Before, select(:html, from: "p") returned a list of all htmls found on this selector. Now, there are two versions of each jQuery method: singular and plural. Singular behaves exactly like jQuery, returning the value of the first found DOM object. It is an exact equevalent of running $("p").html() in the browser.

Drab 0.3.0 introduces plural version of methods. They return all found DOM object values. In the same example, you may use select(:htmls, from: "p") to get all ā€œpā€ elements from the DOM tree and return the value of jQuery .html() method, executed on every single object.

Plural versions of the methods return a Map of %{name|id|__undefined_[number] => value}. Keys of the map are created of DOM objectā€™s attributes: first name or, if not found: id or, if both id and name not found, system will construct a special key with incremental number. Letā€™s check it on example:

### <span name="first_span" class="qs_2 small-border">First span with class qs_2</span>
### <span id="second_span" class="qs_2 small-border">Second span with class qs_2</span>
socket |> select(:html, from: ".qs_2")
# "First span with class qs_2"
socket |> select(:htmls, from: ".qs_2")
# %{"first_span" => "First span with class qs_2", "second_span" => "Second span with class qs_2"}

Callbacks

Drab is now equipped with callbacks to be used in the Commander: before_handler and after_handler. Usage is quite obvious, it is only worth to mention that before_handler must return truly value (everything except false and nil). Otherwise, the proceeding event handler will never be processed (handy for checking the authorization etc). By the other hand, after_handler is launched every time after handler function finish, and it receives the handler function return value as an argument.

More

Please check the release notes for more details.

Plans for the future

Unfortunately, the TODO list is rather growing than shrinking. In the following weeks I would like to concentrate on building the test environment. After this, Iā€™d like to completely refactor Drab.Query and Drab.Template.

Feedback

Any feedback is strongly welcome! Most of the suggestion of users in this forum are included in this release of Drab.


One more thing: only Elixir/Phoenix can make it possible!

9 Likes

@grych: :html and :htmls ā€¦
I prefer: :element and :elements
or: shorter forms of them like :elem
What do you think?

1 Like

Well, because Drab.Query corresponds to jQuery, for me it would be better to stick with jQuery terminology. It is just easier to use for a person who already knows jQuery. And it is simpler, select(:html, from: "selector") translates directly to $("selector").html(), select(:val, from: "selector") - to $("selector").val(), etc etc

Btw you always have select(:all, from: "selector"), which returns all know jQuery methods, including html, val, text, position, etc etc

1 Like

New release 0.3.1

This release is about debugging. The most important feature is to test/debug Drab related functions directly in IEx! If you start Phoenix via REPL with iex -S mix phoenix.server and open a browser to http://localhost:4000, Drab shows this debugging information in IEX console:

[debug] 
    Started Drab for /drab, handling events in DrabPoc.PageCommander
    You may debug Drab functions in IEx by copy/paste the following:
import Drab.Core; import Drab.Query; import Drab.Modal; import Drab.Waiter
socket = GenServer.call(pid("0.666.0"), :get_socket)

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

Now you can grab this two lines with import and get_socket:

iex(1)> import Drab.Core; import Drab.Query; import Drab.Modal; import Drab.Waiter
Drab.Waiter
iex(2)> socket = GenServer.call(pid("0.666.0"), :get_socket)
%Phoenix.Socket{assigns: %{__action: :index,
   __controller: DrabPoc.PageController, __drab_pid: #PID<0.666.0>,
   ... 
   transport_pid: #PID<0.1548.0>}

And you are ready to remote control you browser from IEx. Give it a try!

Show JS alert box:

 iex(3)> socket |> execjs("alert('hello from IEx!')") 
 nil

Get all href attributes:

iex(4)> socket |> select(attrs: :href, from: "a")
%{"__undefined_0" => "http://www.phoenixframework.org/docs",
  "__undefined_1" => "https://tg.pl/drab",
  "__undefined_2" => "http://phoenixframework.org/docs/overview",
  "__undefined_3" => "https://hexdocs.pm/phoenix",
  "__undefined_4" => "https://github.com/phoenixframework/phoenix",
  "__undefined_5" => "http://groups.google.com/group/phoenix-talk",
  "__undefined_6" => "http://webchat.freenode.net/?channels=elixir-lang",
  "__undefined_7" => "https://twitter.com/elixirphoenix"}

Run pretty Bootstrap Modal (please notice that IEx waits for your response):

iex(5)> socket |> alert("Title", "Sure?", buttons: [ok: "Azaliż", cancel: "Poniechaj"])
{:ok, %{}}

There is a project with Phoenix and Drab already installed - here on Github. You may use it as a sandbox to get started with Drab.

6 Likes

@grych thanks a lot, drab looks very promising! Trying to wrap my head around it.

2 Likes

Heh, looks like a fun feature! ^.^

2 Likes

Girls and boys,
there is a new release of Drab: 0.3.2. Finally, Iā€™ve made tests for all Drab features. Because of how Drab works, mosts of the tests must be end-to-end (integration). Iā€™ve decided to use Hound and chromedriver.

In addition, there is a build-in Phoenix server. Now to play with Drab in IEx, you donā€™t need to create your own Phoenix application and add drab to it, anymore. You can just clone the github repo and run the server on it:

git clone git@github.com:grych/drab.git
cd drab
mix deps.get
npm install && node_modules/brunch/bin/brunch build
iex -S mix phoenix.server

Open the browser, navigate to http://localhost:4000 and follow the instruction in IEX:

import Drab.Core; import Drab.Query; import Drab.Modal; import Drab.Waiter
socket = GenServer.call(pid("0.xxxxx.0"), :get_socket)

Now you can remote control the browser from IEx:

iex(5)> socket |> alert("Title", "WOW!")                                          
{:ok, %{}}
iex(6)> socket |> select(:text, from: "h3")
"Drab Tests"
iex(7)> socket |> update(:text, set: "It is set from IEx, wow!", on: "h3")  
%Phoenix.Socket{assigns: %{__action: :index, .........

And, last but not least, Drab has the own logo :slight_smile:

7 Likes

Congrats - just had a play via your instructions, awesome :003:

1 Like

Whoo, Iā€™ll update as soon as I finish this horrid SQL stuff that Iā€™m neck-deep in. :slight_smile:

1 Like

Wow, just found this project. Awesome!! Iā€™m doing something similar in my team chat app, but on a much smaller scale. My app renders the initial page with a standard controller. But all requests after that are rendered server side and pushed to the client through channels.

Iā€™m going to this project a try see how it fits into my app.

Great work!

1 Like

Speaking about chats, I am thinking about adding very easy chat example to the demo/tutorial page. I didnā€™t want to do it at the first place, as the stereotype of websockets is that they are only for chats :wink:

There is another thing from my TODO list to resolve: how Drab should broadcast changes to the browsers. Drab has two version of DOM-modification commands: update and broadcast update. When you do:

def delete_button_clicked(socket, sender) do
  bookid = sender["data"]["id"]
  Books.Repo.delete(bookid)
  socket |> delete("#book_row_#{bookid}")
end

it updates only the page in the browser from which request is sent - the browser where you click the button. But if other user have the same page opened, the row will persist until they refresh. Bad.

The solution for this is to broadcast such requests to all browsers which are currently viewing the page with the book list. In Drab, all DOM modification commands have the bang version. If we change the delete line to:

socket |> delete!("#book_row_#{bookid}")

it will update all the browsers viewing this page.

Currently Drab broadcasts such modifications to the viewers of the current page only. This is obviously not enough: the simplest example is when you are doing changes on page /users/1/books you probably want to propagate it to /users/2/books etc etc.

This is what I want to enhance in Drab. Broadcasts should be configurable! My first thoughts are to allow you to broadcast to:

  • :all
  • :current_url - this is a default behaviour
  • :current_controller - all browsers viewing the page rendered it the same controller, so /books/1, /books/2 etc
  • "topic" - use your own topic, like "books" - it would broadcast to all browsers which operates on the Commander listening to this topic

What do you think?

1 Like

Do it :003:

The great thing about something becoming synonymous is that it makes for easy comparison and lets people get their bearings easily.

Iā€™m sure many people would like to see how it would be implemented with Drab :slight_smile: (me included :lol:)

2 Likes

Very happy to see this project. Love the idea writing code on the server and having it run on the client. Anything that avoids Javascript is a blessingā€¦

I would like to suggest some changes to the Drab syntax, that could improve code readability (if it is not too late in this projects lifecycle.)

Drab.Query Existing 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)

Drab.Query Proposed syntax:
return =
socket |> select(what, at: selector)
socket |> create(what, at: selector)
socket |> update(what, at: selector)
socket |> insert(what, at: selector)
socket |> delete(what, at: selector)
socket |> execute(what, at: selector)


Drab Event Handling Existing syntax:
<input id=ā€œtext_to_uppercaseā€ value=ā€œuppercase meā€>
<button drab-event=ā€œclickā€ drab-handler="uppercase>Do it </button>

Drab Event Handling Proposed syntax:
<input id=ā€œtext_to_uppercaseā€ value=ā€œuppercase meā€>
<button drab-on=ā€œclickā€ drab-do=ā€œuppercaseā€>Do it </button>


Drab Event Handling Shortcuts - Existing syntax:
<button drab-click=ā€œclicked_sleep_buttonā€ data-sleep=ā€œ1ā€> Sleep 1 second </button>
<button drab-click=ā€œclicked_sleep_buttonā€ data-sleep=ā€œ2ā€> Sleep 2 seconds </button>
<button drab-click=ā€œclicked_sleep_buttonā€ data-sleep=ā€œ3ā€> Sleep 3 seconds </button>

Drab Event Handling Shortcuts - Proposed syntax:
<button drab-on-click-do=ā€œclicked_sleep_buttonā€ use-sleep=ā€œ1ā€> Sleep 1 second </button>
<button drab-on-click-do=ā€œclicked_sleep_buttonā€ use-sleep=ā€œ2ā€> Sleep 2 seconds </button>
<button drab-on-click-do=ā€œclicked_sleep_buttonā€ use-sleep=ā€œ3ā€> Sleep 3 seconds </button>

1 Like

It is not, it is still in 0.3 version :slight_smile:

Changing all the selectors to be passed with :at option is tempting, but why not to go the step further and pass the selector just as an argument of the function? I did the similar in the first version of API, and I found itā€¦ hard to read.

In my opinion, sql-like syntax is easier to read and to remember, as being more natural. select(:html, from: selector) sounds natural, and select(:html, at: selector). We want to have html from this selector, not at it :slight_smile:

Furthermore, it is not possible to standarize it is some cases. Good example is insert, where you need to set up where you are adding your node: insert("<b>some</b>", before: selector), insert("<b>some</b>", after: selector), etc. It this case there would be need to add additional parameter to insert, like insert("<b>some</b>", at: selector, where: :before). It doesnā€™t look very bad, but still I find my solution better.

Anyway, it would be good to hear the opinion of the community. Maybe not all, but some of the command should be changed to :at? As I am not native english, I might be wrong here. What is more natural, execute on selector, or execute at selector? update on something or update at something?

This may have sense. If there is more people agreed with it, I will consider a change.

Disagree, data-* is a standard in html/js

Anyway, thanks for your post!

I agree, passing the selectors as arguments to a function is not good for readability. The syntax you have now is better.

If you take the route of using at: with all the selectors, you could split ā€˜insert-beforeā€™ and ā€˜insert-afterā€™ cases into two separate selectors:

socket |> insert(what, at: selector)
socket |> append(what, at: selector)

Let me explain my thinking in suggesting these changes:

at: in english usually denotes a point in space / time. In this case a selector helps us locate an html element within the space of a document, so at: for selectors.

on: event is a standard way of denoting the occurrence of an event in most JS libraries, etc.

do: makes it explicit that we are calling an function/action.

I am basically suggesting to reduce the keyword count in Drab by trimming it to the bare essentials. But regardless, the syntax you have now is pretty good. I am happy to use Drab as it is.

data-* is fine. I was being pedantic about readabilityā€¦:slight_smile:

P.S. I have just started learning Drab. I havenā€™t even looked at the rest of the commands. :wink:

1 Like

Hi @raza,
thanks for this, really appreciated!
I am not convinced yet, but I would like to hear the community opinion about the API syntax: what do you guys think about it?

I like the :at proposal. Consistency makes the API easier to remember.

I might suggest prepend and append. Its more consistent with jQuery and takes the ambiguity out of insert, IMHO

1 Like

It must be prepend, append, before and after, as in jQuery.

Now:

insert(something, before: selector)
insert(something, after: selector)
insert(something, prepend: selector)
insert(something, append: selector)

Proposed:

append(something, at: selector)
prepend(something, at: selector)
before(something, at: selector)
after(something, at: selector)

And to be consistent with this approach, we need to do the similar with all functions, not only insert, like:

Now:

select(:val, from: selector)
select(:html, from: selector)
select(attr: :href, from: selector)

update(:val, set: new_value, on: selector)
update(attr: :href, set: new_value, on: selecto)

Then:

get_val(at: selector)
get_html(at: selector)
get_attr(:href, at: selector)

set_val(new_value, at: selector)
set_attr(:href, new_value, at: selector)

etc. It can be done for every jQuery method, using some simple meta, but I am still not convinced :slight_smile:

1 Like

I like this approach even better than select, update, delete etc. Using get_, set_ make the syntax concise, consistent and extensible.

select becomes get_*
create becomes add_* or new_*
delete becomes rem_*
update becomes set_*
execute becomes run

1 Like