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 -> ViewState -> 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
defpdepsdo[...{:presto,"~> 0.1.2"},...]endlib/presto/single_counter.ex
defmodulePrestoDemoWeb.Presto.SingleCounterdousePresto.ComponentuseTaggart.HTMLrequireLogger@implPresto.Componentdefinitial_model(_model)do0end@implPresto.Componentdefupdate(message,model)docasemessagedo%{"event"=>"click","id"=>"inc"}->model+1%{"event"=>"click","id"=>"dec"}->model-1endend@implPresto.Componentdefrender(model)dodivdo"Counter is: #{inspect(model)}"button(id: "inc",class: "presto-click")do"More"endbutton(id: "dec",class: "presto-click")do"Less"endendendendindex.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"importunpolyfrom"unpoly/dist/unpoly.js"letpresto=newPresto(channel,up);user_socker.ex
defmodulePrestoDemoWeb.UserSocketdousePhoenix.Socketchannel("presto:*",PrestoDemoWeb.CounterChannel)defconnect(%{"token"=>token}=_params,socket)docasePrestoDemoWeb.Session.decode_socket_token(token)do{:ok,visitor_id}->{:ok,assign(socket,:visitor_id,visitor_id)}{:error,_reason}->:errorendend...component_channel.ex
defmodulePrestoDemoWeb.CounterChanneldo...defhandle_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)casedispatchdo[]->nil_->push(socket,"presto",dispatch)end{:reply,{:ok,payload},socket}end...endrouter.ex
pipeline:browserdoplug(: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
defmodulePrestoDemoWeb.Plugs.UserTokenPlugdoimportPlug.Conndefinit(default),do: defaultdefcall(conn,_default)doifvisitor_id=conn.assigns[:visitor_id]douser_token=PrestoDemoWeb.Session.encode_socket_token(visitor_id)assign(conn,:user_token,user_token)elseconnendendendvisitor_id_plug.ex
defmodulePrestoDemoWeb.Plugs.VisitorIdPlugdoimportPlug.Conn@key:visitor_iddefinit(default),do: defaultdefcall(conn,_default)dovisitor_id=get_session(conn,@key)ifvisitor_iddoassign(conn,@key,visitor_id)elsevisitor_id=Base.encode64(:crypto.strong_rand_bytes(32))conn|>put_session(@key,visitor_id)|>assign(@key,visitor_id)endendendTesting 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: