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
GenServerkeeps the state for the user. It’s all on the server. - For a single component root, there is one
GenServerthat comes to life when it gets a message. - It receives DOM events from the browser over a
channel, updating theGenServerstate. - 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"},
...
]
endlib/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
endindex.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
...
endrouter.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)
enduser_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
endvisitor_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
endTesting 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: