This is tutorial repo for the learning the PETAL stack.
Disclaimer: This stack does not use Alpine.js. The A in the PETAL stack usually refers to Alpine.js however Alpine.js is typically no longer used in Phoenix applications because of additional functionallity added to LiveView, specifically in the Phoenix.LiveView.JS module. This stack adds the use of the Ash framework, as it helps declare models quickly and derive functionality with minimal code. And because it starts with an A.
| Letter | Name/Link | Description |
|---|---|---|
| P | Phoenix | Server-sided web framework built in Elixir. |
| E | Elixir | Functional, concurrent, high-level programming language that runs on the Erlang (BEAM) virtual machine. |
| T | Tailwind | Utility-first CSS framework. |
| A | Ash | Declarative model framework for Elixir. |
| L | LiveView | Library for creating UIs in declarative HTML and updated via websocket events. |
The PETAL stack is scalable and productive.
Elixir runs on the Erlang VM which has many examples of scalability
- The company that created Erlang boasts only 5.2 minutes of downtime a year in their systems running Erlang.
- Whatsup was able to maintain 2 million TCP connections on a single server using 40% CPU of a server from 2012 using Erlang. Source
- A developer rewrote an AWS microservice application in Elixir. The resulting application was faster and cost less to run in the cloud. Source
- Discord as many examples of using Elixir as thier primay language to scale to a large amount of users. Source
- The only dependancy is Elixir. Phoenix is built on Elixir, Phoenix comes with a utility to run Tailwind by default, and the other parts of the stack are Elixir packages.
- Elixir features a macro system so that users can do more with less code.
- Phoenix is a batteries included framework that's designed to get started as fast as possible.
- Including Tailwind means your project starts with over 37,000 CSS classes already defined.
- LiveView handles the connection to between backend and frontend so you don't need to maintain a backend REST API or a frontend Javascript client for your frontend.
- With LiveView developers can write reactive single-page frontends in HTML and Elixir. (No Javascript)
- Ash lets you define a model and then with minimal code derive database persistance, code wrappers, REST API, GraphQL API, and more.
Either read the full tutorial section below or look at the code commits in isolation as examples.
This is a quick summary of each commit. View the commit to look at code in isolation. Each commit will be assigned a section below to go into more detail.
| Section # | Commit | Description |
|---|---|---|
| 1 | 12eb0e4 | Ran mix phx.new petal_stack_tutorial |
| 1 | f78aefb | Added .tool-versions, wrote PETAL Stack section of README |
Install Elixir
# install asdf (a tool for managing runtimes) # Getting started: https://asdf-vm.com/guide/getting-started.html # install erlang and elixir plugins asdf plugin add erlang asdf plugin add elixir # install specific version for plugin asdf install $PLUGIN $VERSION # or if you have a .tool-versions file # install from .tool-versions file asdf installCreate a new Phoenix Project
# install Hex (Elixir package manager, like npm or pip) mix local.hex # update hex and install Phoenix creation script mix archive.install hex phx_new # create a new phoenix app mix phx.new my_app # set elixir versions, this will create a .tool-versions file asdf local erlang 26.2.4 asdf local elixir 1.16.2-otp-26Run the server
# run Postgres via Docker docker run -d \ --name phx_db \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_HOST_AUTH_METHOD=trust \ -p 5432:5432 \ postgres # install dependencies, run migrations mix setup # run the server mix phx.server # run the server and a console iex -S mix phx.serverWe're going to implement a simple counter.
The file lib/petal_stack_tutorial_web/router.ex contains the routing logic to associate controllers with verb-path pairs. Add the following line to add a new route that will point to our live view.
live "/counter", CounterCreate a new file lib/petal_stack_tutorial_web/live/counter.ex with the following content
defmodule PetalStackTutorialWeb.Counter do use PetalStackTutorialWeb, :live_view # more code here endNote: use PetalStackTutorialWeb, :live_view inserts use Phoenix.LiveView which makes this a live view.
We need to implement minimum 3 callbacks.
mount/3which is called when the websocket is mounted, and returns the inital state.render/1which takes the state and returns HEEx (HTML Embedded Elixir) template.handle_update/3which handles events and updates the state.
Implment the mount/3 function.
@impl true def mount(_params, _session, socket) do {:ok, assign(socket, counter: 0)} endNote: Add the @impl true to explicitly say you're implementing a callback.
mount/3 must return a tuple with :ok being first and the socket object being the second. Inside the socket object is a hashmap called assigns which we can add state to. The helper function assign takes a socket and a key-value and returns a socket object with the updated state. There is other information about the websocket connection in the socket object but we will only care about our own custom state for this exercise.
Implment the render/1 function.
attr :click, :string, required: true attr :debounce, :integer, default: 20 slot :inner_block defp my_button(assigns) do ~H""" <button phx-click={@click} phx-debounce={@debounce}> <%= render_slot(@inner_block) %> </button> """ end attr :counter, :integer, required: true @impl true def render(assigns) do ~H""" <div> <span><%= @counter %></span> <.my_button click="inc">+</.my_button> <.my_button click="dec">-</.my_button> </div> """ endNote: The ~H sigil is a macro that turns the string into a HEEx template.
Note: The @ is a macro that accesses the assigns map. @counter is equivalent to assigns[:counter].
Note: When displaying a Elixir value in a HEEx template, use <%= ... %> for values in HTML and { ... } for values as attributes.
We implement the render/1 function that take the assigns map in the socket object. It returns an HEEx template. The template consists of a span to display the value of the counter, and two buttons to increment and decrement the counter. The two buttons have some similar functionality so we can implment another function that is private and returns the button. The attr and slot macros are helpers for declaring values in the assigns map. A attr can be a value that is required or have a default value. You will get a compiler error if a required attr is not given. A slot is nested HTML.
After we implmented this function, we can start the server and open http://localhost:4000/counter in out browser.
We see the following logs.
[info] GET /counter [debug] Processing with PetalStackTutorialWeb.Counter.Elixir.PetalStackTutorialWeb.Counter/2 Parameters: %{} Pipelines: [:browser] [info] Sent 200 in 28ms [info] CONNECTED TO Phoenix.LiveView.Socket in 12µs Transport: :websocket Serializer: Phoenix.Socket.V2.JSONSerializer Parameters: %{"_csrf_token" => "Bwg5ATcfGAQMIHJEPC5OGzYDIUYwcHlXcypQuqhSgj6teC7IpuU7fEL3", "_live_referer" => "undefined", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"} [debug] MOUNT PetalStackTutorialWeb.Counter Parameters: %{} Session: %{"_csrf_token" => "dqIPBnpWkJD0YmyRFvtqV55d"} [debug] Replied in 87µs Here we see the following
- The browser makes a
GETto/counter - The router routes to the
Counterlive view. - A response is made in 28ms, which sends the LiveView response.
- That response sends another request to establish a websocket connection.
- Connect in 12µs
- The
mount/3callback is called to initialize the state. - The
render/1callback is called rendering the HTML. This reply is sent in 87µs.
The web page displays "0+-"
Recall we set the phx-click and phx-debounce attributes on the buttons, which are specific Phoenix LiveView attributes. phx-click for the + button is set to the string "inc", which means when that element is clicked that string will be sent via the websocket as an event. The phx-debounce attribute is set to 20, which is how many milliseconds all events for this element are debounced for. When we click the + we see the following error.
[error] GenServer #PID<0.804.0> terminating ** (UndefinedFunctionError) function PetalStackTutorialWeb.Counter.handle_event/3 is undefined or private What happens
- We click the +
- The "inc" event is sent via the websocket.
- We try to call
handle_event/3and get a function not defined error. - The Elixir process dies due to a unhandled exception.
- The process is restarted by a supervisor and reconnects to our websocket.
You will see a small red flash alert telling you the page loses connection for a moment, then disappear when the connection is reestablished.
Let's implment handle_event/3
@impl true def handle_event("inc", _params, socket) do {:noreply, update(socket, :counter, fn x -> x+1 end)} end @impl true def handle_event("dec", _params, socket) do f = fn x when x <= 0 -> 0 x -> x - 1 end {:noreply, update(socket, :counter, f)} endWe use pattern matching to implment the function across 2 seperate clauses. Here if we were to emit an event besides "inc" or "dec" from our frontend we'd get a function undefined exception. Optionally we could add a thrid catch-all clause to log the result. In both callbacks we use the update helper function to take the socket, key of value we want to change, and a function that will transform the value. We use pattern matching to implment the callback so that it can't decrement below 0.
Now when we click the + button we see the log and the counter update.
[debug] HANDLE EVENT "inc" in PetalStackTutorialWeb.Counter Parameters: %{"value" => ""} [debug] Replied in 413µs That is most everything you need to get started with LiveView but not all the functionality
- Implement
handle_info/2to handle server-side events sent from other processes, for real-time updates to the UI. - File upload
- Lazy-Evaluation style streaming values
- Local events handled by
Phoenix.LiveView.JS