Skip to content

Commit d2a1733

Browse files
committed
LiveView counter example
1 parent ec9150a commit d2a1733

File tree

4 files changed

+204
-2
lines changed

4 files changed

+204
-2
lines changed

README.md

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ This is a quick summary of each commit. View the commit to look at code in isola
4949
Section # | Commit | Description
5050
------|------|------
5151
1 | `12eb0e4` | Ran `mix phx.new petal_stack_tutorial`
52-
1 | `TBD` | Added `.tool-versions`, wrote PETAL Stack section of README
52+
1 | `f78aefb` | Added `.tool-versions`, wrote PETAL Stack section of README
5353

5454
## Tutorial
5555

@@ -110,5 +110,157 @@ iex -S mix phx.server
110110
```
111111

112112
### Section 2: Intro to LiveView
113+
114+
We're going to implement a simple counter.
115+
116+
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.
117+
118+
```elixir
119+
live "/counter", Counter
120+
```
121+
122+
Create a new file `lib/petal_stack_tutorial_web/live/counter.ex` with the following content
123+
```elixir
124+
defmodule PetalStackTutorialWeb.Counter do
125+
use PetalStackTutorialWeb, :live_view
126+
127+
# more code here
128+
end
129+
```
130+
131+
Note: `use PetalStackTutorialWeb, :live_view` inserts `use Phoenix.LiveView` which makes this a live view.
132+
133+
We need to implement minimum 3 callbacks.
134+
- `mount/3` which is called when the websocket is mounted, and returns the inital state.
135+
- `render/1` which takes the state and returns HEEx (HTML Embedded Elixir) template.
136+
- `handle_update/3` which handles events and updates the state.
137+
138+
Implment the `mount/3` function.
139+
```elixir
140+
@impl true
141+
def mount(_params, _session, socket) do
142+
{:ok, assign(socket, counter: 0)}
143+
end
144+
```
145+
146+
Note: Add the `@impl true` to explicitly say you're implementing a callback.
147+
148+
`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.
149+
150+
Implment the `render/1` function.
151+
```elixir
152+
attr :click, :string, required: true
153+
attr :debounce, :integer, default: 20
154+
slot :inner_block
155+
156+
defp my_button(assigns) do
157+
~H"""
158+
<button phx-click={@click} phx-debounce={@debounce}>
159+
<%= render_slot(@inner_block) %>
160+
</button>
161+
"""
162+
end
163+
164+
attr :counter, :integer, required: true
165+
166+
@impl true
167+
def render(assigns) do
168+
~H"""
169+
<div>
170+
<span><%= @counter %></span>
171+
<.my_button click="inc">+</.my_button>
172+
<.my_button click="dec">-</.my_button>
173+
</div>
174+
"""
175+
end
176+
```
177+
178+
Note: The `~H` sigil is a macro that turns the string into a HEEx template.
179+
180+
Note: The `@` is a macro that accesses the assigns map. `@counter` is equivalent to `assigns[:counter]`.
181+
182+
Note: When displaying a Elixir value in a HEEx template, use `<%= ... %>` for values in HTML and `{ ... }` for values as attributes.
183+
184+
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.
185+
186+
After we implmented this function, we can start the server and open http://localhost:4000/counter in out browser.
187+
188+
We see the following logs.
189+
```
190+
[info] GET /counter
191+
[debug] Processing with PetalStackTutorialWeb.Counter.Elixir.PetalStackTutorialWeb.Counter/2
192+
Parameters: %{}
193+
Pipelines: [:browser]
194+
[info] Sent 200 in 28ms
195+
[info] CONNECTED TO Phoenix.LiveView.Socket in 12µs
196+
Transport: :websocket
197+
Serializer: Phoenix.Socket.V2.JSONSerializer
198+
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"}
199+
[debug] MOUNT PetalStackTutorialWeb.Counter
200+
Parameters: %{}
201+
Session: %{"_csrf_token" => "dqIPBnpWkJD0YmyRFvtqV55d"}
202+
[debug] Replied in 87µs
203+
```
204+
205+
Here we see the following
206+
1. The browser makes a `GET` to `/counter`
207+
2. The router routes to the `Counter` live view.
208+
3. A response is made in 28ms, which sends the LiveView response.
209+
4. That response sends another request to establish a websocket connection.
210+
5. Connect in 12µs
211+
6. The `mount/3` callback is called to initialize the state.
212+
7. The `render/1` callback is called rendering the HTML. This reply is sent in 87µs.
213+
214+
The web page displays "0+-"
215+
216+
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.
217+
218+
```
219+
[error] GenServer #PID<0.804.0> terminating
220+
** (UndefinedFunctionError) function PetalStackTutorialWeb.Counter.handle_event/3 is undefined or private
221+
```
222+
223+
What happens
224+
1. We click the +
225+
2. The "inc" event is sent via the websocket.
226+
3. We try to call `handle_event/3` and get a function not defined error.
227+
4. The Elixir process dies due to a unhandled exception.
228+
5. The process is restarted by a supervisor and reconnects to our websocket.
229+
230+
You will see a small red flash alert telling you the page loses connection for a moment, then disappear when the connection is reestablished.
231+
232+
Let's implment `handle_event/3`
233+
```elixir
234+
@impl true
235+
def handle_event("inc", _params, socket) do
236+
{:noreply, update(socket, :counter, fn x -> x+1 end)}
237+
end
238+
239+
@impl true
240+
def handle_event("dec", _params, socket) do
241+
f = fn
242+
x when x <= 0 -> 0
243+
x -> x - 1
244+
end
245+
{:noreply, update(socket, :counter, f)}
246+
end
247+
```
248+
249+
We 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.
250+
251+
Now when we click the + button we see the log and the counter update.
252+
```
253+
[debug] HANDLE EVENT "inc" in PetalStackTutorialWeb.Counter
254+
Parameters: %{"value" => ""}
255+
[debug] Replied in 413µs
256+
```
257+
258+
That is most everything you need to get started with LiveView but not all the functionality
259+
260+
- Implement `handle_info/2` to handle server-side events sent from other processes, for real-time updates to the UI.
261+
- File upload
262+
- Lazy-Evaluation style streaming values
263+
- Local events handled by `Phoenix.LiveView.JS`
264+
113265
### Section 3: Intro to Ash
114266
### Section 4: TBD

lib/petal_stack_tutorial/application.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule PetalStackTutorial.Application do
1010
children = [
1111
PetalStackTutorialWeb.Telemetry,
1212
PetalStackTutorial.Repo,
13-
{DNSCluster, query: Application.get_env(:petal_stack_tutorial, :dns_cluster_query) || :ignore},
13+
{DNSCluster,
14+
query: Application.get_env(:petal_stack_tutorial, :dns_cluster_query) || :ignore},
1415
{Phoenix.PubSub, name: PetalStackTutorial.PubSub},
1516
# Start the Finch HTTP client for sending emails
1617
{Finch, name: PetalStackTutorial.Finch},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule PetalStackTutorialWeb.Counter do
2+
use PetalStackTutorialWeb, :live_view
3+
4+
@impl true
5+
def mount(_params, _session, socket) do
6+
{:ok, assign(socket, counter: 0)}
7+
end
8+
9+
attr :click, :string, required: true
10+
attr :debounce, :integer, default: 20
11+
slot :inner_block
12+
13+
defp my_button(assigns) do
14+
~H"""
15+
<button phx-click={@click} phx-debounce={@debounce}>
16+
<%= render_slot(@inner_block) %>
17+
</button>
18+
"""
19+
end
20+
21+
attr :counter, :integer, required: true
22+
23+
@impl true
24+
def render(assigns) do
25+
~H"""
26+
<div>
27+
<span><%= @counter %></span>
28+
<.my_button click="inc">+</.my_button>
29+
<.my_button click="dec">-</.my_button>
30+
</div>
31+
"""
32+
end
33+
34+
@impl true
35+
def handle_event("inc", _params, socket) do
36+
{:noreply, update(socket, :counter, fn x -> x + 1 end)}
37+
end
38+
39+
@impl true
40+
def handle_event("dec", _params, socket) do
41+
f = fn
42+
x when x <= 0 -> 0
43+
x -> x - 1
44+
end
45+
46+
{:noreply, update(socket, :counter, f)}
47+
end
48+
end

lib/petal_stack_tutorial_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule PetalStackTutorialWeb.Router do
1818
pipe_through :browser
1919

2020
get "/", PageController, :home
21+
live "/counter", Counter
2122
end
2223

2324
# Other scopes may use custom stacks.

0 commit comments

Comments
 (0)