Let's move to some more complicated functionality. Assume having a process which is doing some stuff on the server side and we want to report back to the user after each completed step. We will use the progress bar controlled from the server.
Client side is very simple, again. Please notice the drab-click
attribute - this time it will run perform_long_process
function on the server side.
<div class="progress">
<div class="progress-bar" role="progressbar" style="width:0%">
</div>
</div>
<button drab-click="perform_long_process">Click me to start processing ...</button>
On the server we simulate a long processing by sleeping some random time in each step. We will run
start_background_process
function. Main loop runs random number of steps, and each step just sleeps for a random time. After the nap,
update_bar
function in launched and sets the
css("width")
of the
progress-bar
to
XX%
and then, the html of the progress bar node to
"XX%"
. We keep it doing it until it reaches 100%, then we
insert(class: "progress-bar-success", into: ".progress-bar")
. Lets repeat this code in English:
insert the
class progress-bar-success into the
.progress-bar selector. This is why I prefer the pipe syntax - Drab functions are more readable with it.
Notice that updates can be stacked, because all Drab setter functions return
socket
.
defhandler perform_long_process(socket, dom_sender) do
socket
|> execute(:hide, on: this(dom_sender))
|> insert(cancel_button(socket, self()), after: "[drab-click=perform_long_process]")
start_background_process(socket)
end
defp start_background_process(socket) do
socket |> delete(class: "progress-bar-success", from: ".progress-bar")
steps = :rand.uniform(100)
step(socket, steps, 0)
end
defp step(socket, last_step, last_step) do
# last step, when number of steps == current step
update_bar(socket, last_step, last_step)
socket |> insert(class: "progress-bar-success", into: ".progress-bar")
case socket |> alert("Finished!", "Do you want to retry?", buttons: [ok: "Yes", cancel: "No!"]) do
{:ok, _} -> start_background_process(socket)
{:cancel, _} -> clean_up(socket)
end
end
defp step(socket, steps, i) do
:timer.sleep(:rand.uniform(500)) # simulate real work
update_bar(socket, steps, i)
step(socket, steps, i + 1)
end
defp update_bar(socket, steps, i) do
socket
|> update(css: "width", set: "#{i * 100 / steps}%", on: ".progress-bar")
|> update(:html, set: "#{Float.round(i * 100 / steps, 2)}%", on: ".progress-bar")
end
Just before running the background process, the event handler (
perform_long_process/2
function) hides the main button (
execute(:hide, on: this(dom_sender)
) and then adds the Cancel button to the DOM tree with
insert("button html", after: "[drab-click=perform_long_process])"
. Translating it to English again, it means
insert button html after all objects having drab-click=perform_long_process properties. This shows the Cancel button, defined here:
defp cancel_button(socket, pid) do
"""
<button class="btn btn-danger"
drab-click="cancel_long_process"
data-pid="#{Drab.tokenize_pid(socket, pid)}">
Cancel
</button>
"""
end
This function builds the simple button html to be inserted into the DOM tree. What is interesting here, is that button has its event handler (function
cancel_long_process/2
) and
data-pid
property, containing the PID of the Elixir process which runs the steps loop (in this case it is
self()
). This is because we need to know which process to cancel!
Tokenizing the PID prevents data tampering (and also translates Elixir PID to String, so it could be inserted into the DOM tree). Remember that Tokens are signed, but not encrypted, so do not store any sensitive data in this way.
To be able to cancel the process we need to modify step/3
to receive notifications from the outside world. In our case, in each step system checks if there is a message to cancel process. In this case it cleans the stuff up, and finish. If there is no message like this, immediatel (after 0
) continues the loop.
defp step(socket, steps, i) do
:timer.sleep(:rand.uniform(500)) # simulate real work
update_bar(socket, steps, i)
# wait for a cancel message
receive do
:cancel_processing ->
clean_up(socket)
after 0 ->
step(socket, steps, i + 1)
end
end
defhandler cancel_long_process(socket, dom_sender) do
pid = Drab.detokenize_pid(socket, dom_sender["data"]["pid"])
send(pid, :cancel_processing)
end
In the cancel button handler, you just need to get the tokenized pid from
dom_sender["data"]["pid"]
and decrypt it with
Drab.detokenize_pid/2
function. Then send it a gentle message asking for not to continue.
Now take a look on Drab.Modal.alert/4
- the function which shows the bootstrap modal window on the browser and waits for the user input. Please notice that nothing is updated until you close the alert box. And because you can put your own form in the alert box, this is the easiest way to get the user input. Alert boxes return not only the name of the clicked button, but as well the names and values of all inputs in the modal window.