Drab: remote controlled frontend framework for Phoenix

WHOOO! Been awaiting that! :slight_smile:

Nice QoL. :slight_smile:

I especially quite like this syntax. Itā€™d be nice if you could support this too:

<input drab="focus:input_focus(42) blur:input_blur()" />

So 42 would be appended to the end of the argument list of input_focus (so it should be the normal callback arity + 1) as thatā€™d allow us to easily pass in context sensitive information per call in addition to the event and such as well. Plus I think requiring () on event callbacks are all around better as that leaves it open to not using () meaning that it is a reference to something else for example. :slight_smile:

1 Like

+1
I like the idea, but I will leave parenthesis optional, but enforce using it in examples and docs.

1 Like

Hi @grych, thank you for the nice package!

There is an idea in meteor framework called latency compensation. I like that idea a lot.

Following is the difference between the old-school ajax way and the meteorā€™s way (latency compensation) for a request-response-update cycle (copied from the linked post).

This method is also great for chat systems, where a user experiencing latency (of a second or so) because of her/his slow connection may be an unpleasant experience which can be eliminated by the usage of latency compensation.

I think copying the latency compensation from meteor will be very easy for Drab which already uses local-storage and session-storage.

Hey, thanks for pointing this!

Anyway, I donā€™t think it could work with Drab.

All the work is done on the server side. So there is no way that Drab client side JS can simulate the result of any action, as it could literally be anything.

We could allow user to register some JS callbacks launched at the event, so you could develop your own simulation and set up some value immediately, before server returns the real value. I just donā€™t like the idea: the whole concept with Drab was to get rid of javascript.

But it is true, the latency is an issue from UX point of view. This is why click event senders are disabled by default until the response comes back. Just a one trick, but makes a difference.

3 Likes

I think the way it stands you can implement optimistic updates (the actual name for this behavior as far as I know) on a case by case basis considering most of the work is in replicating the data model on the client and the server. Making a library that is supposed to sit alongside Drab and that could facilitate that would probably be the way to go. Itā€™ll allow Drab to do what it does and focus on what itā€™s made for while also possibly allowing for extending the use case for Drab in the future if this other library advances enough.

2 Likes

###What if we could write the optimistic updates on the server side, using Elixir and Drab.Element-like DSL?

optimistic_before :save_data do
  set_prop "#name", value: "...processing...", disabled: true
end

def save_data(socket, _sender) do
  set_prop socket, "#name", value: Database.get_name()
end

optimistic_after :save_data do
  set_prop "#name", disabled: false
end

optimistic_before must compile to javascript, which is not so complicated, as set_prop/2 is simply translated to JS "Drab.set_prop(#{encode_js(selector)}, #{encode_js(properties)})". This JS would be sent to the browser on connect, and register to run before and after the event.

1 Like

You could also use that one Elixir->Javascript library too to compile the inner AST to javascript for more unknown expressions too.

1 Like

Yes, Iā€™ve already been thinking about integration of ElixirScript. Is there any other transpiler than Elixir Script?

Eh I made a mini-one before but other than that I donā€™t think so?

Looks like my dream (of having latency compensation in Phoenix apps) through Drab is possible! :slight_smile:

1 Like

For those interested:

example of drab under and umbrella with a non-drab app routing non-wss traffic by host

Please comment if there is a better way to do this and if there are any more maintainable ways to route sockets around.

2 Likes

Iā€™m unable to get unpoly up-modal working with drab 0.7 As far as I can remember including <%= Drab.Client.run(@conn) %> in your fragment used to work, but it appears there is some check to see if drab is already running, in any case the fragment is not updated. I can call Drab.exec_elixir from the fragment which may be a workaround.

What is the correct way to have drab evaluate an html fragment pulled into a page? I really only want to slick a submit button and parse the textfield values in the unpoly up-modal fragment.

On another note, Iā€™m trying to get the shared commanders working but they dontā€™ seem to be getting the :page_loaded callbacks which makes grabbing session stuff much harder.

They donā€™t work well ā€˜togetherā€™ (unless you are very careful). Generally if you have Drab on a page you usually donā€™t need unpoly. Unpoly is useful for fast multi-page switching and such, Drab is better for single pages. It would be nice if they cooperated well (or Drab subsumed Unpoly). ^.^;

1 Like

Is it fixable? If so what would fix it?

It is, you just have to call some drab hooks manually via the unpoly registry to get it all hooked up again. Might ask @grych for the specific commands necessary to tear down and fully rebuilt the drab state on page changes. ^.^;

Yes, this callbacks are only working with the ā€œmainā€ commander. Noticed to mention about this in docs. If you think all the callbacks from all commanders should be launched, create an issue on github, I will think about it.

I am not sure I understood the issue correctly, but you are putting the fragment of html, with drab-events, and want Drab to set up its events, yes?

If you are putting html fragments using Drab (with poke or set_prop innerHTML: ...), it is done automatically. If you are doing it on the client side, you need to evaluate this fragment to set up Drab events (drab-click etc) with JS method:

Drab.enable_drab_on(node_or_selector)

I did not mention about it before in docs, treating it as a kind of private function, but I am going to add it.

Ha!
Everyone is very welcome to create something like Drab.Unpoly. There is a Drab.Module behaviour to obey, it is not documented yet, but I will do it if someone is interested in adding Drab modules.
As I decided before, I, personally, will rather stick with the drab core. The last js-framework-related module I wrote was Drab.Query :wink:

1 Like

I am currently working on it. The optional argument will be evaluated on the client, so you could give any valid JS expression.

Plus, what if we could set this optional argument for a dom tree? So this:

<div drab-commander='My.Commander' drab-argument='{datasource: "Repo.User"}'>
  <button drab-click="add_user">Add User</button>
  <button drab-click="My.Other.Commander.add_property('forty-two')">Add something else<button>
</div>

would be equivalent of:

<button drab-click='My.Commander.add_user.add_user({datasource: "Repo.User"})'>Add User</button>
<button drab-click="My.Other.Commander.add_property('forty-two')">Add something else<button>

(notice that exsting properties are not overwritten).

1 Like

Maybe I donā€™t understand quite whoā€™s able to change whatā€¦

I wanted to be able to grab my current_user id from guardian and use put_store(socket, :current_user). From whatā€™ Iā€™ve read this is using local storage and it is signed and should be tamper proof right? If that is true I should be able to store it and retrieve it from a shared commander and I donā€™t need the page_loaded callback.

If not then I suppose I need to do something with connect in the UserSocketā€¦

I am afraid that get_session/2 will not work on the shared commander, unless you whitelist it with access_session macro in the main commander. Now I am thinking that it should be changed, all functionality of the ā€œnormalā€ commander should work the same on the shared one.

About the user ID, how do you do socket authentication? If in the connect callback, you may just inject userid to the socket assigns and have an access to it directly.

1 Like

v0.7.1

This version is a step forward for creating component-like pieces of code with Drab, with enhanced Shared Commanders and possibility to pass additional argument to the handler function.

Finally, most functions got their own @spec and Drab is now dialyzable.

Warning!

Drab.Live cache DETS has changed, please ensure your "*.drab templates are recompiled after the upgrade.

New Features

Define Shared Commander with drab-commander 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>

Additional argument for handlers

Since this version you may create handler with arity of 3, and pass the additional parameter using parenthesis after the handler name in drab attribute:

<button drab-click='button_clicked(42)'>

This will run button_clicked/3 instead of button_clicked/2 in your Commander:

def button_clicked(socket, sender, the_answer_for_the_ultimate_question)

The attribute 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)'>

drab-argument

Analogically to drab-commander attribute, there is a drab-argument to set this argument for more nodes. 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)'>

Client-side errors now appears in the application log

For developer happines, all client-side errors are now displayed both on JS console and on the Phoenix side.

Example:

<button drab=":wrong">

generates:

[error] Browser reports: Drab attribute value ':wrong' is incorrect.

Bugfixes

  • Parent/child expression case in Drab.Live (#71) solved
  • Updated floki to 0.20; fixed #76
  • Special case for outerHTML in Drab.Element.set_prop, fixed #80
  • Special case for HTMLOptionsCollection; fixed #75