Skip to content

Commit 7b6f545

Browse files
committed
Update to new Phoenix version. closes #59
1 parent 7c5afe5 commit 7b6f545

1 file changed

Lines changed: 136 additions & 121 deletions

File tree

modules/ROOT/pages/phoenix/phoenix-liveview-basics.adoc

Lines changed: 136 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
# Phoenix LiveView Basics
33
Stefan Wintermeyer <sw@wintermeyer-consulting.de>
44

5-
WARNING: This document may not be up to date with the latest Phoenix/LiveView
6-
version. I am working on an updated version.
5+
WARNING: I am currently in the process of updating this document to the latest Elixir and Phoenix version. Please help me by sending me bug reports (ideally as GitHub issues). Thank you!
76

87
LiveView, according to the
98
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html[documentation],
@@ -63,10 +62,9 @@ You can also run your app inside IEx (Interactive Elixir) as:
6362
6463
$ iex -S mix phx.server
6564
----
66-
<1> '--live' adds all the needed stuff to use LiveView out of the box. For this
67-
example, we don't need Ecto.
68-
<2> Click `Y` and, depending on your internet connection, maybe grab a cup of
69-
coffee.
65+
<1> The `--live` flag adds all the necessary components to use LiveView out of the box. For this
66+
example, we don't need Ecto, so we use `--no-ecto`. Phoenix 1.7+ comes with Tailwind CSS by default.
67+
<2> Click `Y` and, depending on your internet connection, maybe grab a cup of coffee.
7068

7169
The aim of this demo is to create a new webpage with the path `/light` which
7270
offers a status of a virtual light bulb and a switch functionality to turn that
@@ -75,7 +73,7 @@ any JavaScript.
7573

7674
First we have to add the route:
7775

78-
.lib/hello_world_web/router.ex
76+
.lib/demo_web/router.ex
7977
[source,elixir]
8078
----
8179
defmodule DemoWeb.Router do
@@ -86,15 +84,13 @@ defmodule DemoWeb.Router do
8684
scope "/", DemoWeb do
8785
pipe_through :browser
8886
89-
live "/", PageLive, :index <1>
87+
get "/", PageController, :home <1>
9088
live "/light", LightLive <2>
9189
end
9290
9391
[...]
9492
----
95-
<1> Because we created the app with the `--live` switch the default root path is
96-
already a live view. Therefore the `live` macro is used here (at the beginning
97-
of the line) instead of the traditional `get`.
93+
<1> In Phoenix 1.7+, the default root path uses a regular controller instead of a LiveView. You can change this to a LiveView if desired.
9894
<2> This is the new `light` route which leads to the `LightLive` module.
9995

10096
LiveView modules are located in the `lib/demo_web/live/` directory. There we
@@ -107,27 +103,26 @@ defmodule DemoWeb.LightLive do
107103
use DemoWeb, :live_view
108104
109105
def render(assigns) do <1>
110-
~L"""
111-
<h1>The light is off.</h1>
106+
~H"""
107+
<h1 class="text-2xl font-bold mb-4">The light is off.</h1>
112108
"""
113109
end
114110
end
115111
----
116-
<1> The `render/1` function renders the template. We use the `~L` sigil to
117-
define the template.
112+
<1> The `render/1` function renders the template. In Phoenix 1.7+, we use the `~H` sigil for HEEx templates instead of the deprecated `~L` sigil. Note the Tailwind CSS classes added to the h1 element.
118113

119114
[IMPORTANT]
120115
====
121-
In this case, we use the `~L` sigil to define the template, but for bigger
116+
In this case, we use the `~H` sigil to define the template inline, but for bigger
122117
templates, it makes more sense to create a separate template file, which would
123-
be a LiveEEx template (using the `.leex` extension) and be stored in the
118+
be a HEEx template (using the `.heex` extension) and be stored in the
124119
`lib/demo_web/live` directory. If you use a separate template file, the
125120
`render/1` function is not needed (see the Airport Code Search section for an
126121
example).
127122
128123
Have a look at https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html for more information.
129124
====
130-
indexterm:["LiveEEx Templates"]
125+
indexterm:["HEEx Templates"]
131126

132127
Now, let's open the URL `http://localhost:4000/light` in the browser.
133128

@@ -152,13 +147,13 @@ defmodule DemoWeb.LightLive do
152147
end
153148
154149
def render(assigns) do
155-
~L"""
156-
<h1>The light is <%= @light_bulb_status %>.</h1>
150+
~H"""
151+
<h1 class="text-2xl font-bold mb-4">The light is <%= @light_bulb_status %>.</h1>
157152
"""
158153
end
159154
end
160155
----
161-
<1> Out of all the posssible parameters of `mount/3` we only need the `socket`
156+
<1> Out of all the possible parameters of `mount/3` we only need the `socket`
162157
struct for our example.
163158
<2> We set the initial value of the variable `light_bulb_status` to `off`.
164159

@@ -170,14 +165,19 @@ To turn on the light bulb we need a button:
170165
[source,elixir]
171166
----
172167
def render(assigns) do
173-
~L"""
174-
<h1>The light is <%= @light_bulb_status %>.</h1>
175-
<button phx-click="on">On</button> <1>
168+
~H"""
169+
<h1 class="text-2xl font-bold mb-4">The light is <%= @light_bulb_status %>.</h1>
170+
<button
171+
type="button"
172+
phx-click="on"
173+
class="px-4 py-2 rounded bg-green-600 text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50">
174+
On
175+
</button> <1>
176176
"""
177177
end
178178
----
179179
<1> The button tag includes `phx-click="on"` which is special Phoenix code to
180-
trigger an event.
180+
trigger an event. We've added Tailwind CSS classes for styling.
181181

182182
Now we see the button on the webpage:
183183

@@ -285,49 +285,46 @@ defmodule DemoWeb.LightLive do
285285
use DemoWeb, :live_view
286286
287287
def mount(_params, _session, socket) do
288-
socket =
289-
socket
290-
|> assign(:light_bulb_status, "off")
291-
|> assign(:on_button_status, "") <1>
292-
|> assign(:off_button_status, "disabled")
293-
288+
socket = assign(socket, :light_bulb_status, "off") <1>
294289
{:ok, socket}
295290
end
296291
297292
def render(assigns) do
298-
~L"""
299-
<h1>The light is <%= @light_bulb_status %>.</h1>
300-
<button phx-click="on" <%= @on_button_status %>>On</button>
301-
<button phx-click="off" <%= @off_button_status %>>Off</button> <2>
293+
~H"""
294+
<h1 class="text-2xl font-bold mb-4">The light is <%= @light_bulb_status %>.</h1>
295+
296+
<button
297+
type="button"
298+
phx-click="on"
299+
class="px-4 py-2 rounded bg-green-600 text-white disabled:opacity-40 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
300+
disabled={@light_bulb_status == "on"}>
301+
On
302+
</button>
303+
304+
<button
305+
type="button"
306+
phx-click="off"
307+
class="px-4 py-2 rounded bg-gray-600 text-white disabled:opacity-40 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50"
308+
disabled={@light_bulb_status == "off"}>
309+
Off
310+
</button> <2>
302311
"""
303312
end
304313
305314
def handle_event("on", _value, socket) do
306-
socket =
307-
socket
308-
|> assign(:light_bulb_status, "on")
309-
|> assign(:on_button_status, "disabled") <3>
310-
|> assign(:off_button_status, "")
311-
315+
socket = assign(socket, :light_bulb_status, "on") <3>
312316
{:noreply, socket}
313317
end
314318
315319
def handle_event("off", _value, socket) do
316-
socket =
317-
socket
318-
|> assign(:light_bulb_status, "off")
319-
|> assign(:on_button_status, "")
320-
|> assign(:off_button_status, "disabled")
321-
320+
socket = assign(socket, :light_bulb_status, "off")
322321
{:noreply, socket}
323322
end
324323
end
325324
----
326-
<1> We assign a value for the `on_button_status` and `off_button_status` in
327-
order to make the on button active and the off button inactive at the start.
328-
<2> We use the `@off_button_status` to disable the off button right at the
329-
beginning.
330-
<3> We toggle the values of the buttons.
325+
<1> We only need to track the light bulb status now, as we'll use it directly with the disabled attribute
326+
<2> We use the `disabled` attribute with a conditional expression to disable buttons based on the current state
327+
<3> We simply toggle the light bulb status in each event handler
331328

332329
We are all set. The buttons work in the way a user would like them to work and
333330
all without writing a single line of JavaScript. Phoenix LiveView takes care of
@@ -603,47 +600,57 @@ end
603600
<3> We hardcode a list of German airports here. In a real application, this
604601
would include more data and probably be database driven.
605602

606-
This time we don't use the `~L` sigil in the controller but a LiveEEx Template
603+
This time we don't use the `~H` sigil directly in the controller but a separate HEEx Template
607604
file:
608605

609-
lib/travelagent_web/live/search_live.html.leex
606+
lib/travelagent_web/live/search_live.html.heex
610607
[source,html]
611608
----
612-
<form phx-submit="airport_code_search">
613-
<fieldset>
614-
<label for="nameField">Airport Code</label>
615-
<input type="text" name="airport_code" value="<%= @airport_code %>"
616-
placeholder="e.g. FRA"
617-
autofocus autocomplete="off" /> <1>
618-
<input class="button-primary" type="submit" value="Search Airport">
619-
</fieldset>
620-
</form>
621-
622-
<%= unless @airports == [] do %> <2>
623-
<h2>Search Results</h2>
624-
<table>
625-
<thead>
626-
<tr>
627-
<th>Airport Code</th>
628-
<th>Name</th>
629-
</tr>
630-
</thead>
631-
<tbody>
632-
<%= for airport <- @airports do %>
633-
<tr>
634-
<td><%= airport.code %></td>
635-
<td><%= airport.name %></td>
636-
</tr>
637-
<% end %>
638-
</tbody>
639-
</table>
640-
<% end %>
641-
----
642-
<1> I think it is always a curtesy to the user to set the first input field to
643-
`autofocus`. And we add an `autocomplete="off"` just to be sure that the browser
644-
doesn't disturb the user.
645-
<2> When the search returns a non-empty list, a table with the results will be
646-
displayed.
609+
<div class="max-w-2xl mx-auto">
610+
<form phx-submit="airport_code_search" class="mb-6">
611+
<div class="space-y-4">
612+
<label for="nameField" class="block text-sm font-medium text-gray-700">Airport Code</label>
613+
<input
614+
type="text"
615+
name="airport_code"
616+
value={@airport_code}
617+
placeholder="e.g. FRA"
618+
autofocus
619+
autocomplete="off"
620+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" /> <1>
621+
<button
622+
type="submit"
623+
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
624+
Search Airport
625+
</button>
626+
</div>
627+
</form>
628+
629+
<%= unless @airports == [] do %> <2>
630+
<h2 class="text-xl font-semibold mb-4">Search Results</h2>
631+
<div class="overflow-x-auto">
632+
<table class="min-w-full divide-y divide-gray-200">
633+
<thead class="bg-gray-50">
634+
<tr>
635+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Airport Code</th>
636+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
637+
</tr>
638+
</thead>
639+
<tbody class="bg-white divide-y divide-gray-200">
640+
<%= for airport <- @airports do %>
641+
<tr class="hover:bg-gray-50">
642+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"><%= airport.code %></td>
643+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= airport.name %></td>
644+
</tr>
645+
<% end %>
646+
</tbody>
647+
</table>
648+
</div>
649+
<% end %>
650+
</div>
651+
----
652+
<1> Setting the first input field to `autofocus` improves user experience. The `autocomplete="off"` prevents browser autocomplete from interfering with our LiveView updates.
653+
<2> When the search returns a non-empty list, a table with the results will be displayed, styled with Tailwind CSS.
647654
648655
Lastly, we need to update the TravelagentWeb.SearchLive module:
649656
@@ -700,40 +707,48 @@ codes begin with an `h`. Without having to click on the `Search Airport` button.
700707
Luckily for us, we only have to make a couple of changes in the LiveEEx Template
701708
file to achieve this.
702709
703-
lib/travelagent_web/live/search_live.html.leex
710+
lib/travelagent_web/live/search_live.html.heex
704711
[source,html]
705712
----
706-
<form phx-change="airport_code_search"> <1>
707-
<fieldset>
708-
<label for="nameField">Airport Code</label>
709-
<input type="text" name="airport_code" value="<%= @airport_code %>"
710-
placeholder="e.g. FRA"
711-
autofocus autocomplete="off" />
712-
</fieldset>
713-
</form>
714-
715-
<%= unless @airports == [] do %>
716-
<h2>Search Results</h2>
717-
<table>
718-
<thead>
719-
<tr>
720-
<th>Airport Code</th>
721-
<th>Name</th>
722-
</tr>
723-
</thead>
724-
<tbody>
725-
<%= for airport <- @airports do %>
726-
<tr>
727-
<td><%= airport.code %></td>
728-
<td><%= airport.name %></td>
729-
</tr>
730-
<% end %>
731-
</tbody>
732-
</table>
733-
<% end %>
734-
----
735-
<1> We just have to use `phx-change` for the form. This means that each change
736-
triggers `handle_event/3`.
713+
<div class="max-w-2xl mx-auto">
714+
<form phx-change="airport_code_search"> <1>
715+
<div class="space-y-4">
716+
<label for="nameField" class="block text-sm font-medium text-gray-700">Airport Code</label>
717+
<input
718+
type="text"
719+
name="airport_code"
720+
value={@airport_code}
721+
placeholder="e.g. FRA"
722+
autofocus
723+
autocomplete="off"
724+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
725+
</div>
726+
</form>
727+
728+
<%= unless @airports == [] do %>
729+
<h2 class="text-xl font-semibold mt-6 mb-4">Search Results</h2>
730+
<div class="overflow-x-auto">
731+
<table class="min-w-full divide-y divide-gray-200">
732+
<thead class="bg-gray-50">
733+
<tr>
734+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Airport Code</th>
735+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
736+
</tr>
737+
</thead>
738+
<tbody class="bg-white divide-y divide-gray-200">
739+
<%= for airport <- @airports do %>
740+
<tr class="hover:bg-gray-50">
741+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"><%= airport.code %></td>
742+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= airport.name %></td>
743+
</tr>
744+
<% end %>
745+
</tbody>
746+
</table>
747+
</div>
748+
<% end %>
749+
</div>
750+
----
751+
<1> We use `phx-change` for the form instead of `phx-submit`. This means that each keystroke triggers `handle_event/3`, providing real-time feedback.
737752
738753
Please open your browser at http://localhost:4000/search and give it a try.
739754

0 commit comments

Comments
 (0)