Drab: remote controlled frontend framework for Phoenix

Congratulations, you’ve found a bug!
Looks like noone used this functionality before :slight_smile: it only shows how irrele^H^H^H^H^H^Himportant was it. I will fix it in the next release.

But, it is probably because you don’t need it! In Drab, you subscribe to additional topics at the server-side. See https://hexdocs.pm/drab/Drab.Commander.html#subscribe/2

1 Like

haha, okay! at least i can retain some sanity.

Except now I realise I should be using the Commander subscribe. As this makes more sense.

1 Like

Sorry, me again. So I have decided that the broadcast_poke/4 wont work for me. Having to set all the assigns just wont work in my case, as I need to know things like the currently logged in Staff user, which changes data in the view. @OvermindDL1 is there a way you know around this? can you explain the Shared Commanders model a bit more?

@grych I looked into the Drab.Elements as you suggested, and I can see that the Element broadcast_html might work. Although I have to broadcast a list of data, each will have a link on it which currently is built using the Phoenix link helpers and routing paths. Just seems a little messy.
Am I possibly not using Drab in the best use case here? Or am I thinking about this data in the wrong way?

1 Like

I believe the best thing you can do is to split you application into smaller templates. Then you may re-render them at the server side and send changed html to the browser, with broadcast_html.

Actually, there is an example for this on Drab demo page - it is not for broadcasting, but shows the idea of using small partials.

First, render the partial

buttons = render_to_string("waiter_example.html", [])

then, send it to the browser

insert_html socket, "#waiter_example_div", :beforeend, buttons

and at the end, remove them from the browser

set_prop socket, "#waiter_example_div", innerHTML: nil

(the last line should be replaced with set_html/3, when I wrote the example it did not exist yet).

2 Likes

Re: shared commanders

It is always a good idea to split your application to chunks, which you can reuse in the future. Shared Commander can help you with this! Shared commanders are the pieces of frontend+backend code. This is a good example:

<p drab-commander="DrabPoc.Timer2Commander" drab-argument="{seconds: 10}">
  <button class="btn btn-primary" drab-click="countdown">
    Count down from 10
  </button>
  <br><span class="output">SPAN .output</span>
</p>

server side:

defhandler countdown(socket, sender, options) do
  for i <- 1..options["seconds"] do
    set_prop socket, this_commander(sender) <> " .output", innerText: options["seconds"] - i
    Process.sleep(1000)
  end
end 

What is the most important here, is to understand this_commander/1 function. It gives you an unique selector of the node you are calling the countdown function from. This is how you may put many of those in the same page.

But your case is probably even easier, as you don’t want to react on the events from the Shared Commander, but just broadcast the updated data for all the servers. Consider you have a book partial:

<tr><td>
  <span class="book-name" id="book_<%= @book_id %>"><%= @book_name %></span>
</td></tr>

and render it as a list:

<%= for {book_id, book_name} <- @books do %>
  <%= render("book.html", book_id: book_id, book_name: book_name %>
<% end %>

now, when you want to refresh the specific book name, just use it’s id!

book_name = read_from_database(book_id)
broadcast_html same_topic("books"), "#book_#{book_id}", book_name
2 Likes

Thanks so much! I had a quick go at this and it definitely work out alot cleaner!

2 Likes

Do you have any issues with Tests?

During tests my broadcast is happening. The tests pass okay, which they should since I am only unit testing. But at the end of the test I get a termination error from my GenServer process that i am running.

The error is occurs after all tests pass, during the call my Drab broadcast function, which is doing this:

conn = %Plug.Conn{private: %{:phoenix_endpoint => BonusWeb.Endpoint}}
      auction = Phoenix.View.render_to_string(BonusWeb.Staff.OfferView, "_auction.html", [active_offers: offers, conn: conn ])
      Drab.Element.broadcast_html(Drab.Core.same_topic("brandname"), "#auction", auction)

The error basically says that im passing in Nil for the active_offers assigns.

Im figuring it has something to do with the DB timing of tests maybe? Have you seen this before?

Error:

21:31:48.592 [error] GenServer :auction terminating
** (MatchError) no match of right hand side value: nil
    (bonus) lib/bonus/submissions/submissions.ex:229: Bonus.Submissions.current_winning_bid/1
    (bonus) lib/bonus_web/views/shared_helpers.ex:32: BonusWeb.SharedHelpers.winning_bid/1
    (bonus) lib/bonus_web/templates/staff/offer/_auction.html.eex:18: anonymous fn/3 in BonusWeb.Staff.OfferView."_auction.html"/1

There are a lot of problems on tests, as most of them must be done with the chromedriver, and sometimes it just fails, but I’ve never seen anything like this.

Can you check what active_offers has before you render_to_string? If it is really nil, you shall check wherever this value comes from. From the test database which is empty?

1 Like

I can inspect the result of render_to_string and it has the correct data in it.

Its weird. When I run a single test it passes. but when I run multiple tests (more than 1) it fails.
Does that mean anything to you? Im not really sure how the test environments are setup and pulled down. But somehow its breaking when running this code and multiple test:

  conn = %Plug.Conn{private: %{:phoenix_endpoint => BonusWeb.Endpoint}}
  auction = Phoenix.View.render_to_string(BonusWeb.Staff.OfferView, "_auction.html", [active_offers: offers, conn: conn ])

the render_to_string works on the first test, but then returns empty on every subsequent test. i am so confused.

After further investigation i have narrowed it down to this.
Sorry it doesnt seem to be Drab related, but ddint seem to be getting this issue before I added Drab. Which just seems odd.

But, when running more than 1 test, this code return nil
%{bids: bid} = get_offer(offer.id) |> Repo.preload([bids: order_bids_query])

If I change it to this it works:
%{bids: bid} = offer |> Repo.preload([bids: order_bids_query])

Seems that the call to get_offer fails when multiple tests run. which seems crazy, cause its pretty standard:
Repo.get(Offer, id) |> Repo.preload(:brands)

It seems that the database call must return nil, when it should return a result. then this is breaking the render_to_string call i imagine.

not sure best way to solve this. maybe i should be going to regular elixir chat support? :wink:
thanks

1 Like

I would suggest asking here on the forum with “ecto” tag, as I assume this is Ecto related question. Sorry, I won’t help, as I’ve never used it before.

1 Like

Update diffs only on the client side.

Drab.Live now sends only the changed parts of html, but it updates the whole part, regardless of the fact it was changed or not.

Shall we do the client-side diff of the htmls it is sending and update only the changed tags?

Pros: it will eliminate the issue with cleaning values of the form.

Cons: more complicated client JS.

2 Likes

These strategies aren’t mutually exclusive. You can send minimal diffs and merge them into the tags that have changed.

That way you get the best of both worlds: minimal amount id data dent over the wire and maximum flexibility in handling things like input tags whose value has changed.

But if you’re going to convert your HTML into strings to merge the whole thing, you could borrow from my PhoenixUndeadView, which I think you’ve been following. That might even simplify your architecture because you wouldn’t even need to insert dummy span tags and attributes. The cost of the simplification is to depend on a library such as morphdom, but at least it offloads the complexity to a project maintained by someone else.

EDIT: id there is a Drab.Live, why not a Drab.Undead? :laughing:

2 Likes

This is exactly what I am thinking of!

1 Like

But do you plan on merging on creating a big string representing the DOMon the client and use something like morphdom to merge ir into the DOM or do you plan on using your dummy tags and only mergung inside the dummy tags?

1 Like

No idea yet. Never did something like it before.

1 Like

If you want to build a big string and hen merge it afterwards you should definitely try my UndeadTemplates. They should be as optimized as possible without messing with the Elixir compiler. It’s completely untested, of course. I can get the changing parts in whatever format you need (probably a json array that the server can send to the browser). You might end up not using it, but I think you should try.

EDIT: at least try to get a feeling for how it’s implemented

EDIT2: we can discuss this over private message.

2 Likes

To integrate Undead Templates into Drab you need something like the following:

  1. A render function to render the initial HTML (I already have that)

  2. A function to render the top level static segments of the template; if there are static segments inside dynamic parts we can’t optimize those. This should be integrated in the function above so that it renders both the HTML and the static segments in a format such as JSON.

  3. A function to send the (rendered) dynamic segments to the browser through a channel (as JSON or some other format)

  4. Something on the browser to start channel listeners and stuff like that.

  5. Something on the browser to receive the dynamic parts, concatenate them into a big string and merge it into the DOM.

I can code solutions for 1, 2 and 3 which you can plug directly unto whatever you’re doing.

I can probably also code something ridiculously inefficient for 4 and 5 based on what Drab already does

1 Like

I will definitely take a look, but right now I am busy with the other stuff. Thanks for this anyway!

1 Like

Hey, im getting a similar error deploying to Gigalixir as @makeitrein

== Compilation error in file lib/bonus_web/views/user/offer_view.ex ==
       ** (CompileError) lib/bonus_web/templates/user/offer/index.html.drab: invalid quoted expression: %Drab.Live.Safe{partial: %Drab.Live.Partial{amperes: %{}, assigns: %{}, hash: "gezdaojsgaytamju", path: "lib/bonus_web/templates/user/offer/index.html.drab"}, safe: [{:__block__, [], [{:=, [], [{:tmp1, [], Drab.Live.EExEngine}, [{:__block__, [], [{:=, [], [{:tmp1, [], Drab.Live.EExEngine}, [["{{{{@drab-partial:gezdaojsgaytamju}}}}"], "\n\tvar countDownDate"]]}, [{:tmp1, [], Drab.Live.EExEngine}, {:case, [generated: true], [{{:., [line: 5], [{:offer, [line: 5], nil}, :id]}, [line: 5], []}, [do: [{:->, [generated: true], [[safe: {:data, [generated: true], Drab.Live.EExEngine}], {:data, [generated: true], Drab.Live.EExEngine}]}, {:->, [generated: true], [[{:when, [generated: true], [{:bin, [generated: true], Drab.Live.EExEngine}, {:is_binary, [generated: true, context: Drab.Live.EExEngine, import: Kernel], [{:bin, [generated: true], Drab.Live.EExEngine}]}]}], {{:., [generated: true], [{:__aliases__, [generated: true, alias: false], [:Plug, :HTML]}, :html_escape]}, [generated: true], [{:bin, [generated: true], Drab.Live.EExEngine}]}]}, {:->, [generated: true], [[{:other, [generated: true], Drab.Live.EExEngine}], {{:., [line: 5], [{:__aliases__, [line: 5, alias: false], [:Phoenix, :HTML, :Safe]}, :to_iodata]}, [line: 5], [{:other, [line: 5], Drab.Live.EExEngine}]}]}]]]}]]}, " = "]]}, [{:tmp1, [], Drab.Live.EExEngine}, {:case, [generated: true], [{{:., [line: 5], [{:__aliases__, [counter: 0, line: 5], [:DateTime]}, :to_unix]}, [line: 5], [{{:., [line: 5], [{:offer, [line: 5], nil}, :ending_datetime]}, [line: 5], []}, :milliseconds]}, [do: [{:->, [generated: true], [[safe: {:data, [generated: true], Drab.Live.EExEngine}], {:data, [generated: true], Drab.Live.EExEngine}]}, {:->, [generated: true], [[{:when, [generated: true], [{:bin, [generated: true], Drab.Live.EExEngine}, {:is_binary, [generated: true, context: Drab.Live.EExEngine, import: Kernel], [{:bin, [generated: true], Drab.Live.EExEngine}]}]}], {{:., [generated: true], [{:__aliases__, [generated: true, alias: false], [:Plug, :HTML]}, :html_escape]}, [generated: true], [{:bin, [generated: true], Drab.Live.EExEngine}]}]}, {:->, [generated: true], [[{:other, [generated: true], Drab.Live.EExEngine}], {{:., [line: 5], [{:__aliases__, [line: 5, alias: false], [:Phoenix, :HTML, :Safe]}, :to_iodata]}, [line: 5], [{:other, [line: 5], Drab.Live.EExEngine}]}]}]]]}]]}, "\n\t"]}
       (phoenix) /tmp/build/lib/bonus_web/views/user/offer_view.ex:1: Phoenix.Template.__before_compile__/1
remote: Command '[u'docker', u'run', u'--rm', u'-e', u'GIGALIXIR_SHOULD_CLEAN_CACHE=False', u'-v', u'/tmp/tmpVn4ilG/key-burlywood-degu:/tmp/app', u'-v', u'/tmp/gigalixir/cache/key-burlywood-degu/:/tmp/cache', u'-v', u'/tmp/tmpVn4ilG/env:/tmp/env', u'--env=USER=www-data', u'us.gcr.io/gigalixir-152404/herokuish:latest']' returned non-zero exit status 1

Do we know of any way around this? not sure how to debug this either.