Presto is an Elixir library for creating Elm-like or React-like single page applications (SPAs) completely in Elixir.
It was presented at ElixirConfEU 2018. You can find the slides here.
Add this to mix.exs
:
{:presto, "~> 0.1.2"}
Web development is too complciated. Front-ends, back-ends, multiple languages, markup, it's all too complicated. Things can be simpler.
We want:
- the feel and data model (mostly) of React.
- views to be a projection of the data.
- the simplicity of Elm's model/update/view functions.
- all of this in Elixir.
Model
-> Update
-> View
State
-> Message
-> Response
This is a GenServer
.
- A
GenServer
keeps the state for the user. It’s all on the server. - For a single component root, there is one
GenServer
that comes to life when it gets a message. - It receives DOM events from the browser over a
channel
, updating theGenServer
state. - UI updates are returned via the
channel
.
The GenServers
are managed by a DynamicSupervisor
.
Components are scoped to a visitor_id
, which is unique to each browser.
mix.exs
defp deps do
[
...
{:presto, "~> 0.1.2"},
...
]
end
lib/presto/single_counter.ex
defmodule PrestoDemoWeb.Presto.SingleCounter do
use Presto.Component
use Taggart.HTML
require Logger
@impl Presto.Component
def initial_model(_model) do
0
end
@impl Presto.Component
def update(message, model) do
case message do
%{"event" => "click", "id" => "inc"} ->
model + 1
%{"event" => "click", "id" => "dec"} ->
model - 1
end
end
@impl Presto.Component
def render(model) do
div do
"Counter is: #{inspect(model)}"
button(id: "inc", class: "presto-click") do
"More"
end
button(id: "dec", class: "presto-click") do
"Less"
end
end
end
end
index.html.eex
<%= Presto.render(Presto.component(PrestoDemoWeb.Presto.SingleCounter, assigns[:visitor_id])) %>
assets/package.json
...
"dependencies": {
...
"presto": "file:../deps/presto"
},
...
app.js
import {Presto} from "presto"
import unpoly from "unpoly/dist/unpoly.js"
let presto = new Presto(channel, up);
user_socker.ex
defmodule PrestoDemoWeb.UserSocket do
use Phoenix.Socket
channel("presto:*", PrestoDemoWeb.CounterChannel)
def connect(%{"token" => token} = _params, socket) do
case PrestoDemoWeb.Session.decode_socket_token(token) do
{:ok, visitor_id} ->
{:ok, assign(socket, :visitor_id, visitor_id)}
{:error, _reason} ->
:error
end
end
...
component_channel.ex
defmodule PrestoDemoWeb.CounterChannel do
...
def handle_in("presto", payload, socket) do
%{visitor_id: visitor_id} = socket.assigns
# send event to presto component
{:ok, dispatch} = Presto.dispatch(PrestoDemoWeb.Presto.SingleCounter, visitor_id, payload)
case dispatch do
[] -> nil
_ -> push(socket, "presto", dispatch)
end
{:reply, {:ok, payload}, socket}
end
...
end
router.ex
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug(PrestoDemoWeb.Plugs.VisitorIdPlug)
plug(PrestoDemoWeb.Plugs.UserTokenPlug)
end
user_token_plug.ex
defmodule PrestoDemoWeb.Plugs.UserTokenPlug do
import Plug.Conn
def init(default), do: default
def call(conn, _default) do
if visitor_id = conn.assigns[:visitor_id] do
user_token = PrestoDemoWeb.Session.encode_socket_token(visitor_id)
assign(conn, :user_token, user_token)
else
conn
end
end
end
visitor_id_plug.ex
defmodule PrestoDemoWeb.Plugs.VisitorIdPlug do
import Plug.Conn
@key :visitor_id
def init(default), do: default
def call(conn, _default) do
visitor_id = get_session(conn, @key)
if visitor_id do
assign(conn, @key, visitor_id)
else
visitor_id = Base.encode64(:crypto.strong_rand_bytes(32))
conn
|> put_session(@key, visitor_id)
|> assign(@key, visitor_id)
end
end
end
Testing is easy. It’s just a GenServer
. Spin them up, update, test the response. Done.
Use the language. Growing your app is very simple with this approach. If your render()
method gets too big, you just split it up in to helpers and modules and whatnot. If your update()
method gets too big, you just split it up in to helpers and modules and whatnot.
Here is the code for a simple counter demo
This is a real application using Presto
.
The code is here.
This is running on the West Coast of the USA:
This is running in Central Europe: