whistle v0.1.2 Whistle.Program behaviour
Link to this section Summary
Functions
Use embed/4
to embed a Program in a view. It will render the view in plain HTML. When the Javscript library executes, it will automatically connect to the Program and become interactive
A fullscreen Whistle.Program
renders the whole HTML document, this is useful if you want to also handle navigation in your program through the Whistle.Program.route/4
callback
Callbacks
The authorize callback will be called on a running program when a client tries to access it
handle_info/2
is similar to how GenServer.handle_info/2
works, it will receive a message and the current state, and it expects a new updated state returned. This callback can be triggered by sending Erlang messages to the program instance
Receives parameters from the route, it should return the initial state or an error
The terminate callback will be called when the program instance shuts down, it will receive the state
The update callback is called everytime an event handler is triggered, it will receive the message, the current state and the session of the client who triggered it
The view receives the programs state and the session of the client we are rendering the view for
Link to this section Functions
embed(conn, router, program_name, params \\ %{})
Use embed/4
to embed a Program in a view. It will render the view in plain HTML. When the Javscript library executes, it will automatically connect to the Program and become interactive.
In a Phoenix template:
<!-- lib/my_app_web/templates/page/index.html.eex -->
<div>
<%= embed(conn, MyProgramRouter, "counter") |> raw %>
</div>
In a Plug
or a Phoenix.Controller
action:
def index(conn, _opts) do
resp = embed(conn, MyProgramRouter, "counter")
conn
|> Plug.Conn.put_resp_content_type("text/html")
|> Plug.Conn.send_resp(200, resp)
end
fullscreen(conn, router, program_name, params \\ %{})
A fullscreen Whistle.Program
renders the whole HTML document, this is useful if you want to also handle navigation in your program through the Whistle.Program.route/4
callback.
When the Javscript library executes, it will automatically connect to the Program and become interactive, giving you both a static HTTP page and an interactive web page for free.
Remember to include the Javascript library via a <script>
tag or module import.
Call in a Plug
or a Phoenix.Controller
action:
def index(conn, _opts) do
fullscreen(conn, MyProgramRouter, "counter")
end
Example of a program:
def route(["user", user_id], _state, session, _query_params) do
{:ok, %{session | route: {:user, user_id}}}
end
def view(state, %{route: {:user, user_id}}) do
view_document("You're viewing user ##{user_id}")
end
def view(state, session) do
view_document("It Works!")
end
defp view_document(body) do
~H"""
<html>
<head>
<title>My Whistle App</title>
<script src="/js/whistle.js"></script>
</head>
<body>
<h1>It works!<h1>
</body>
</html>
"""
end
Link to this section Callbacks
authorize(arg0, arg1, map)
(optional)
authorize(Whistle.state(), Whistle.Socket.t(), map()) ::
{:ok, Whistle.Socket.t(), Whistle.Session.t()} | {:error, any()}
authorize(Whistle.state(), Whistle.Socket.t(), map()) :: {:ok, Whistle.Socket.t(), Whistle.Session.t()} | {:error, any()}
The authorize callback will be called on a running program when a client tries to access it.
It receives the current state, the client's socket and the clients params. And must return an updated socket, an initial session or an error with a reason.
You cloud send a bearer token and verify it here to authorize a client.
def authorize(state, socket, %{"token" => token}) do
case MyApp.Guardian.decode_and_verify(token) do
{:ok, claims} ->
{:ok, socket, claims}
{:error, reason} ->
{:error, reason}
end
end
handle_info(any, arg1)
(optional)
handle_info(any(), Whistle.state()) :: {:ok, Whistle.state()}
handle_info(any(), Whistle.state()) :: {:ok, Whistle.state()}
handle_info/2
is similar to how GenServer.handle_info/2
works, it will receive a message and the current state, and it expects a new updated state returned. This callback can be triggered by sending Erlang messages to the program instance.
defmodule TimeProgram do
use Program
def init(_args) do
Process.send_after(self(), :tick, 1_000)
{:ok, DateTime.utc_now()}
end
def handle_info(:tick, state) do
Process.send_after(self(), :tick, 1_000)
{:ok, DateTime.utc_now()}
end
def view(time, session) do
Html.p([], DateTime.to_string(time))
end
end
init(map)
Receives parameters from the route, it should return the initial state or an error.
The parameters are taken from the program route:
defmodule Router do
use Whistle.Router, path: "/ws"
match("chat:*room", ChatProgram, %{"other" => true})
end
defmodule ChatProgram do
use Program
# when joining `chat:1`
def init(%{"room" => "1", "other" => true}) do
{:ok, %{}}
end
end
route(list, arg1, arg2, map)
(optional)
route([String.t()], Whistle.state(), Whistle.Session.t(), map()) ::
{:ok, Whistle.state()} | {:error, any()}
route([String.t()], Whistle.state(), Whistle.Session.t(), map()) :: {:ok, Whistle.state()} | {:error, any()}
terminate(arg0)
(optional)
terminate(Whistle.state()) :: any()
terminate(Whistle.state()) :: any()
The terminate callback will be called when the program instance shuts down, it will receive the state.
Remember that Programs will be automatically respawned if they crash, so there is no need to try restart it yourself. This callback could be useful to serialize the state and then load it later in the init/1
callback.
update(arg0, arg1, arg2)
update(Whistle.message(), Whistle.state(), Whistle.Socket.Session.t()) ::
{:ok, Whistle.state(), Whistle.Session.t()}
update(Whistle.message(), Whistle.state(), Whistle.Socket.Session.t()) :: {:ok, Whistle.state(), Whistle.Session.t()}
The update callback is called everytime an event handler is triggered, it will receive the message, the current state and the session of the client who triggered it.
defmodule CounterProgram do
use Program
def init(_args) do
{:ok, 0}
end
def update(:increase, state, session) do
{:ok, state + 1, session}
end
def view(state, session) do
Html.div([], [
Html.p([], to_string(state)),
Html.button([on: [click: :increase]], "Increase")
])
end
end
view(arg0, arg1)
view(Whistle.state(), Whistle.Session.t()) :: Whistle.Html.Dom.t()
view(Whistle.state(), Whistle.Session.t()) :: Whistle.Html.Dom.t()
The view receives the programs state and the session of the client we are rendering the view for.
It must return a Dom tree, which looks like this:
# {key, {tag, {attributes, children}}}
{0, {"div", {[class: "red"], [
{0, {"p", {[], ["some text"]}}
]}}}
You can use the Whistle.Html
helpers to generate this tree:
Html.div([class: "red"], [
Html.p([], "some text")
])
Or the Whistle.Html.Parser.sigil_H/2
if you want to write plain HTML:
text = "some text"
~H"""
<div class="red">
<p>{{ text }}</p>
</div>
"""
Both the HTML helpers and the sigil will expand to a DOM tree at compile time.