#179: Phoenix LiveView Tutorial Part 3
Now that we have schemas for our valid “words” and our “solves”, let’s start building the grid we’ll use to play the game.
In Part 2 we used the Phoenix LiveView generator mix task, which created a LiveView with our “Word” module. If we open our router we see we’re using the WordLive.Index LiveView as the homepage for our application. This will be the LiveView we’ll use for our gameplay.
Let’s go ahead and open up the WordLive.Index.ex LiveView. We won’t need all the boilerplate code that was generated with it, so let’s remove that. And all we’re left with is the mount callback that’s returning an :ok tuple with the socket.
lib/werdle_web/live/word_live/index.exdefmodule WerdleWeb.WordLive.Index do
use WerdleWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
end
Great, now let’s open the corresponding index.html.heex template. This is more default code that we won’t need. So let’s remove it. I’ll paste in the HTML for our initial Werdle grid.
Now let’s talk through what we’re doing here. Our Phoenix 1.7 application comes with Tailwind CSS installed, so we’re using it here to create grids to help with the layout then because each game of Wordle gets 6 guesses in a game, we’re looping through the row index 6 times. And inside of that, we’ll loop through the column index 5 times, since our word guesses are all 5 letters. Then we have a single text input that has a maxlength of 1. This is the input for each letter of the word guess.
To get a better idea of how the rows and column indexes render the grid, I’ve added a value for each input with the row index followed by the column index.
Template path: lib/werdle_web/live/word_live/index.html.heex
<div class="flex flex-wrap items-top justify-center w-screen">
<div class="grid grid-cols-1 self-start mt-5 mb-5 gap-y-1">
<div class="grid grid-cols-1 col-span-1 gap-1.5">
<%= for row_index <- 0..5 do %>
<div class="col-span-1 gap-1.5 flex justify-items-center justify-center">
<%= for column_index <- 0..4 do %>
<div class="relative sm:w-16 sm:h-16 h-14 w-14 col-span-1 pointer-events-none select-none">
<input
type="text"
value={"#{row_index} #{column_index}"}
class="w-full h-full p-3 text-slate-500 rounded-sm text-2xl text-center font-bold cursor-default"
maxlength="1" />
</div>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
Alright, let’s go ahead and see how this looks. We’ll go to the command line and start our server.
$ mix phx.server
...
With that running, let’s open our application in the browser. And perfect - our grid is rendered and we see the number of our row index, followed by the index of the column. This is a great start - we have our initial Word grid displayed on our page.
With that let’s go back to our template and break our game board here into a few different LiveView components to help keep things organized and maintain all the state we’ll need to play our game. We’ll create 3 different components. One for the full grid, one for each row in the grid, and another for each cell in a row.
Alright, let’s go to the “components” directory, and let’s put our new components into their own namespace called “game_board”, so let’s create that directory. Then inside it, we’ll create grid_component.ex, row_component.ex, and cell_component.ex. Let’s start with the grid_component.ex. We’ll create the module and because we’ll be managing state in our component we’ll want these to be LiveComponents and not function components so let’s add use WerdleWeb, :live_component.
If you’re unfamiliar with LiveView components or need a refresher, I cover LiveComponents in episode 129.
Let’s add the update callback to our component. And we’ll pattern match on a couple assigns: id, cell_backgrounds, and changeset. We’ll get into these more later, but the ID is that we’ll use to uniquely identify our component. cell_backgrounds we may use to render different color backgrounds for each cell in our grid. And then the changeset, which we’ll use the manage a player’s guesses during a game. We’ll include our socket in the update callback. Let’s mark that this function is an implementation of a callback with @impl true. Now inside our update function, we’ll assign the id, cell_backgrounds, and changeset to the socket and then return {:ok, socket}.
Then let’s add the render callback, which takes the assigns and inside it, I’ll paste in the HTML we want to render. This is mostly the same as the HTML we used for the grid in the index.html.heex template. We’re using the @id assign for the HTML id attribute. We’ve also got phx-target={@myself} which is an internal unique reference to the component instance. Just like before we’re looping with the row_index six times for the number of guesses. Then we’re rendering another LiveComponent - our RowComponent - passing in as assigns the id, cell_backgrounds, row_index, and the changeset.
Module path: lib/werdle_web/components/game_board/grid_component.ex
defmodule WerdleWeb.GameBoard.GridComponent do
use WerdleWeb, :live_component
@impl true
def update(
%{id: id, cell_backgrounds: cell_backgrounds, changeset: changeset},
socket
) do
socket =
socket
|> assign(:id, id)
|> assign(:cell_backgrounds, cell_backgrounds)
|> assign(:changeset, changeset)
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div id={@id}
phx-target={@myself}
class="grid grid-cols-1 self-start mt-5 mb-5 gap-y-1">
<div class="grid grid-cols-1 col-span-1 gap-1.5">
<%= for row_index <- 0..5 do %>
<.live_component
module={WerdleWeb.GameBoard.RowComponent}
id={"input-row-#{row_index}"}
cell_backgrounds={@cell_backgrounds}
row_index={row_index}
changeset={@changeset} />
<% end %>
</div>
</div>
"""
end
end
Let’s move on to the RowComponent. We’ll define the live component module. Then we’ll want to add the update callback to it. I’ll go ahead and paste that in. And this is missing the row_index assign, so let’s add that.
Now we’re pattern matching on all 4 assigns that we’re giving our RowComponent. Then inside the callback, we’ll assign each of them to the socket and return {:ok, socket}.
This will need the render too so let’s define that and I’ll go ahead and paste in the HTML. This is part of the same HTML we had in the index.html.heex template for rendering each of the 5 columns in a row. Just like in the GridComponent I’ve updated it to use the @id assign for the HTML id attribute. And then for each column_index we’re rendering our third component - the CellComponent passing in the id, cell_backgrounds, and changeset as assigns.
Module path: lib/werdle_web/components/game_board/row_component.ex
defmodule WerdleWeb.GameBoard.RowComponent do
use WerdleWeb, :live_component
@impl true
def update(
%{id: id, cell_backgrounds: cell_backgrounds, row_index: row_index, changeset: changeset},
socket
) do
socket =
socket
|> assign(:id, id)
|> assign(:cell_backgrounds, cell_backgrounds)
|> assign(:row_index, row_index)
|> assign(:changeset, changeset)
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div id={@id} class="col-span-1 gap-1.5 flex justify-items-center justify-center">
<%= for column_index <- 0..4 do %>
<.live_component
module={WerdleWeb.GameBoard.CellComponent}
id={"input-cell-#{@row_index}-#{column_index}"}
cell_backgrounds={@cell_backgrounds}
changeset={@changeset} />
<% end %>
</div>
"""
end
end
Now let’s move on to the cell component. We’ll define CellComponent as a LiveComponent module. Then I’ll paste in the update callback. This is just like our other two update callbacks. We’re pattern matching on the assigns we want and then assigning them to the socket. Then let’s add our render callback and I’ll paste it in the HTML. Here we’re really just rendering an input for each letter in the word guess.
Again, we’re using the @id assign for the HTML id attribute, the type “text” - no value for now - and then the Tailwind CSS classes for the input. And again the maxlength of 1 since each input should only contain 1 letter.
Module path: lib/werdle_web/components/game_board/cell_component.ex
defmodule WerdleWeb.GameBoard.CellComponent do
use WerdleWeb, :live_component
@impl true
def update(
%{id: id, cell_backgrounds: cell_backgrounds, changeset: changeset},
socket
) do
socket =
socket
|> assign(:id, id)
|> assign(:cell_backgrounds, cell_backgrounds)
|> assign(:changeset, changeset)
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div class="relative sm:w-16 sm:h-16 h-14 w-14 col-span-1 pointer-events-none select-none">
<input
id={@id}
type="text"
value=""
class={"w-full h-full p-3 text-slate-500 rounded-sm text-2xl text-center font-bold cursor-default"}
maxlength="1" />
</div>
"""
end
end
Now let’s update the WorldLive.Index live view that will render our components. We’ll assign the cell_backgrounds to the socket as an empty map and then the changeset - let’s use nil for now.
lib/werdle_web/live/word_live/index.exdefmodule WerdleWeb.WordLive.Index do
use WerdleWeb, :live_view
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:cell_backgrounds, %{})
|> assign(:changeset, nil)
{:ok, socket}
end
end
With that let’s go back to the word_live/index.html.heex template and now instead of rendering the full grid, we can update it to render the GameBoard.GridComponent passing in the assigns of an id with the value of input-grid, cell_backgrounds, and changeset.
Template path: lib/werdle_web/live/word_live/index.html.heex
<div class="flex flex-wrap items-top justify-center w-screen">
<.live_component
module={WerdleWeb.GameBoard.GridComponent}
id="input-grid"
cell_backgrounds={@cell_backgrounds}
changeset={@changeset} />
</div>
Now let’s see if we have everything set up correctly. We’ll go to the command line and when we try to start the server we get an error. It looks like we have a typo in our grid_component.ex module. Let’s open that up and we’ll fix the typo in :live_component. Now if we go to the command line and start the server it starts up. Then looking at the browser - our grid is rendering correctly with 6 rows for each guess and 5 cells for each letter in a word. Now that we have our grid, we’ll start on our keyboard in the next episode.