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

Link to this function

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
Link to this function

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

Link to this callback

authorize(arg0, arg1, map) (optional)
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
Link to this callback

handle_info(any, arg1) (optional)
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
Link to this callback

init(map)
init(map()) :: {:ok, Whistle.state()} | {:error, any()}

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
Link to this callback

route(list, arg1, arg2, map) (optional)
route([String.t()], Whistle.state(), Whistle.Session.t(), map()) ::
  {:ok, Whistle.state()} | {:error, any()}

Link to this callback

terminate(arg0) (optional)
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.

Link to this callback

update(arg0, arg1, arg2)
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
Link to this callback

view(arg0, arg1)
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.