#180: Phoenix LiveView Tutorial Part 4

Published January 27, 2024 5m 50s

Now that we have the grid for our game, we need to create a keyboard so that players will be able to enter their word guesses. And just like we did for the gird, let’s use LiveView LiveComponents.

Let’s go back to our index.html.heex template and below our GridComponent let’s add another live_component for a new component that we’ll call KeyboardComponent. For its assigns we’ll include an id="keyboard" and let’s also add a keyboard_backgrounds assigns that we might use later on to handle the background colors of our keyboard.

Template path: lib/werdle_web/live/word_live/index.html.heex

...
<.live_component
    module={WerdleWeb.GameBoard.KeyboardComponent}
    id="keyboard"
    keyboard_backgrounds={@keyboard_backgrounds} />
...

Then let’s create a new module in the same “components/game_boards” directory called keyboard_component.ex we’ll define the :live_component module. And let’s create a @keyboard_rows module attribute that will be a list of lists, with each nested list containing the keycap values for our keyboard.

With that added let’s implement the update callback, pattern matching on the id and the keyboard_background assigns and inside the function, we’ll add the id to the socket. The keyboard_rows, and the keyboard_backgrounds again returning {:ok, socket}. Then let’s add the render callback and I’ll paste in the HTML, but let’s walk through it.

We’ve got a <div> that’s using our @id assign for the id HTML attribute. Then we’re looping over each keyboard_row in keyboard_rows. We’re taking the keyboard_row and looping through the keycap_values and rendering each one in another LiveComponent we’re calling KeycapComponent. And for the component assigns we’re using a unique ID that includes the keycap value as well as the keycap_value itself and the keyboard_backgrounds.

Module path: lib/werdle_web/components/game_board/keyboard_component.ex

defmodule WerdleWeb.GameBoard.KeyboardComponent do
  use WerdleWeb, :live_component

  @keyboard_rows [
    ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
    ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
    ["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"]
  ]

  @impl true
  def update(%{id: id, keyboard_backgrounds: keyboard_backgrounds}, socket) do
    socket =
      socket
      |> assign(:id, id)
      |> assign(:keyboard_rows, @keyboard_rows)
      |> assign(:keyboard_backgrounds, keyboard_backgrounds)

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div id={@id} class="w-full h-full">
      <%= for keyboard_row <- @keyboard_rows do %>
        <div class="flex justify-center gap-1 mb-1.5 w-full sm:h-full h-14">
        <%= for keycap_value <- keyboard_row do %>
          <.live_component
            module={WerdleWeb.GameBoard.KeycapComponent}
            id={"keycap-#{keycap_value}"}
            keycap_value={keycap_value}
            keyboard_backgrounds={@keyboard_backgrounds}
          />
        <% end %>
        </div>
      <% end %>
    </div>
    """
  end
end

Now we need to create the KeycapComponent. Let’s create the file and define the module. I’ll go ahead and paste in the update callback, but this follows the same pattern in the other components we created. We’re pattern matching on the assigns to get the id, keycap_value, and keyboard_backgrounds and then assigning them to the socket in the function.

With that let’s define the render callback and I’ll go paste in the HTML for our keycap. We’ve got the ID assigns for the HTML ID attribute and then some classes to help us style the keycap. Then we’re rendering the keycap_value in uppercase.

Module path: lib/werdle_web/components/game_board/keycap_component.ex

defmodule WerdleWeb.GameBoard.KeycapComponent do
  use WerdleWeb, :live_component

  @impl true
  def update(
    %{id: id, keycap_value: keycap_value, keyboard_backgrounds: keyboard_backgrounds},
    socket
    ) do

    socket =
      socket
      |> assign(:id, id)
      |> assign(:keycap_value, keycap_value)
      |> assign(:keyboard_backgrounds, keyboard_backgrounds)

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <kbd id={@id} class="kbd kbd-md sm:kbd-lg bg-gray-500 text-slate-200 cursor-default font-bold font-sans border-transparent rounded-md py-2 sm:py-3 px-0 w-2 sm:px-1">
      <%= String.upcase(@keycap_value) %>
    </kbd>
    """
  end

end

Let’s see how everything looks. We’ll go to the command line and start the server, but it looks like we’ve got another error - “undefined variable “socket” in our keyboard_component.ex. We’ll go back to that and in our update callback we’re missing the socket parameter so let’s add that. Below we’re pattern matching on the keyboard_backgrounds and assigning them to the socket, but I don’t think we ever added that to our WordLive.Index LiveView. Let’s go back to that and we need to add that to our socket here.

We’ll use an empty map for now, just like we’re currently doing for the cell_backgrounds.

lib/werdle_web/live/word_live/index.ex...
|> assign(:keyboard_backgrounds, %{})
...

Now let’s go back to the command line and try to start the server again.

$ mix phx.server
...

And great it starts up. So let’s check out our keyboard. Our components are rendering, but the styling is not quite what we’d expect. Let’s fix that.

To help us format our keyboard, we’ll use the great daisyUI Tailwind component library, which has a keyboard component. To install it in our application we’ll go to the command line and move into our “assets” directory. Then we’ll initialize an NPM project. And with that, we can add the daisyui NPM dependency.

$ cd assets
$ npm init -y
$ npm i -D daisyui@latest

Once it’s installed we’ll open our tailwind.config.js and add daisyui to the plugins list.

We don’t want any of DailyUI’s base styles to interfere with ours, so let’s tell DaisyUI not to apply them by adding the daisyui option of base: false.

assets/tailwind.config.js...

plugins: [
  require("@tailwindcss/forms"),
  require("daisyui"),
  ...
  ],
  daisyui: {
    base: false,
  }
}

Great, now let’s go back to the command line and move out of “/assets” directory, and start the server again.


$ cd ../
$ mix phx.server
...

And great - now when we view our application in the browser we see our keyboard is displayed for the most part. We have a little work to do with “backspace” and “enter” keys.

Let’s fix those now. We’ll go back to our keycap_component.ex and let’s use pattern matching to get the different keycap_values that we want to add additional styling to. We’ll add another render callback and pattern match on the “backspace” keycap_value. This is mostly the same, but instead of just rendering the keycap_value, we’re rendering an SVG for a backspace icon.

Then let’s add another render callback to pattern match on the “enter” keycap. Again inside the function, I’ll paste in the HTML with some updated Tailwind CSS styles for us to use. One note here - the ordering of these functions is important. Our pattern-matching render functions need to come before the more general catch-all render function that isn’t doing any pattern matching.

Module path: lib/werdle_web/components/game_board/keycap_component.ex

...

@impl true
def render(%{keycap_value: "backspace"} = assigns) do
  ~H"""
  <kbd id={@id} class="kbd kbd-lg bg-gray-500 text-slate-200 cursor-default rounded-md border-transparent">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"   stroke="currentColor" class="w-10 sm:w-7">
      <path stroke-linecap="round" stroke-linejoin="round" d="M12 9.75L14.25 12m0 0l2.25 2.25M14.25 12l2.25-2.25M14.25 12L12 14.25m-2.58 4.92l-6.375-6.375a1.125 1.125 0 010-1.59L9.42 4.83c.211-.211.498-.33.796-.33H19.5a2.25 2.25 0 012.25 2.25v10.5a2.25 2.25 0 01-2.25 2.25h-9.284c-.298 0-.585-.119-.796-.33z" />
    </svg>
  </kbd>
  """
end
def render(%{keycap_value: "enter"} = assigns) do
  ~H"""
  <kbd id={@id} class="kbd kbd-lg bg-gray-500 text-slate-200 cursor-default font-bold border-transparent rounded-md py-3 text-xs">
    <%= String.upcase(@keycap_value) %>
  </kbd>
  """
end

...

Alright, with those changes we’ll go back to the browser. And our keyboard looks great! With our grid and keyboard rendering like we expect it to, it’s time to move on to the game logic.

Ready to Learn More?

Subscribe to get access to all episodes and exclusive content.

Subscribe Now