#188: Phoenix LiveView Tutorial Part 12

Published January 27, 2024 4m 47s

In the last episode, we removed the cell_backgrounds since we decided to update their backgrounds with a JavaScript Hook.

Let’s use the same method to update the backgrounds of the keycaps on our keyboard. We can remove the keyboard_backgrounds assign from the mount callback here since we won’t need it. Then we’ll open the index.html.heex template and remove the keyboard_backgrounds assign. Then we’ll open the KeyboardComponent and remove keyboard_backgrounds and then we can remove it from the KeycapComponent.

With those deleted, let’s open our WorldLive.Index LiveView, and in the last episode, we added the create_guess_response function to push events to the client. The “guess-reveal-animation” event is what kicks off the letter guess-reveal animations. Let’s update the hook that handles this event to also push events to the KeycapComponent with updated background colors.

In the last episode, we used the status of each letter and matched it with the index of each cell in the row grid. For the keycaps, we’ll need to send over both the “letter” and the status to target the correct keycap. The comparison_results variable is a list of tuples, so let’s use that. But we’ll want to convert it to a map first, which we can do with Map.new. One note: when creating a map, the order of elements is not preserved. This is why we just included it in the letter statuses above. But for the comparison_results, we don’t need things to be order-specific so this will work.

lib/werdle_web/live/word_live/index.exdefmodule WerdleWeb.WordLive.Index do
  ...

  defp create_guess_response(socket, message) do
    guess_row = socket.assigns.current_guess
    comparison_results = Game.compare_guess(socket.assigns.changeset, guess_row, socket.assigns.solve.name)
    letter_statuses = Enum.map(comparison_results, fn {_letter, letter_status} -> letter_status end)

    socket
    |> maybe_push_event("guess-validation-text", message)
    |> push_event("guess-reveal-animation", %{
        guess_row: guess_row,
        letter_statuses: letter_statuses,
        comparison_results: Map.new(comparison_results)
    })
  end

  ...
end

With that updated let’s go to our guess_reveal_animation.js hook and add a new variable totalDelay that will be the length of the input cells times 500. Then let’s add another setTimeout to handle our keycap background updates, and we’ll give it the totalDelay, since we want this to happen just after our cell row has been revealed.

I’ll paste in the code for our keycap updates, but let’s walk through this. We’re iterating over each key/value in data.comparison_results. Then we’re building the ID of the keycap component and then getting the appropriate background color based on the result of the guess. And then just like we did above in the last episode, we’re using pushEventTo to push an event to the keycapComponentID, and we’re calling the event keycap_background and then including the background in the params.

assets/js/hooks/guess_reveal_animation.js...

this.handleEvent('guess-reveal-animation', data => {
  ...
  const totalDelay = inputCells.length * 500;
  ...
  setTimeout(() => {
    Object.entries(data.comparison_results).forEach(([letter, guessResult]) => {
    let keycapComponentID =`#keycap-${letter}`;
    let backgroundColor = classMappings[guessResult];
    this.pushEventTo(keycapComponentID, "keycap_background", { background: backgroundColor });
    });
  }, totalDelay);
});

...

Now let’s update our KeycapComponent to handle this event we’ll add a handle_event callback, pattern matching on the "keycap_background" event. We’ll also pattern match to get the background that’s being sent. And we’ll need to accept the socket too.

When we’re applying backgrounds to our keycaps, we may not want to update the color if it was previously guessed correctly. For example, if a letter was used in a word at position 3 and it was a match, and then in the next guess it was used at position 2, which is a partial match, we don’t want to update the background color for that keycap. To help us determine whether to update a background, let’s create a @background_hierarchy which will be a map of the background colors for keys and then a priority as the value.

Then in our handle_event we’ll get the priority for the background color that exists in our assigns. And the priority for the new background we receive from the hook. With these, we can determine what the new background will be. If the new_priority is greater than the current_priority then we’ll return the background from the hook. Otherwise, we’ll use the background from our assigns. Then let’s assign the background to the socket and return {:noreply, socket}.

Now we’ll need to go to the render callback that handles our letters and let’s remove the current background and replace it with the @background assign. And we’ll need to update the syntax to get the background to render correctly. Finally, let’s go to the update callback…and add the background assign, with the value from the assigns if it exists, otherwise, we’ll use the original background color as the default.

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

defmodule WerdleWeb.GameBoard.KeycapComponent do
  ...

  @background_hierarchy %{
    "bg-green-600" => 4,
    "bg-yellow-600" => 3,
    "bg-red-600" => 2,
    "bg-gray-500" => 1
  }

  ...

  @impl true
  def update(...) do
    ...
    |> assign(:background, socket.assigns[:background] || "bg-gray-500")
    ...
  end

  ...

  @impl true
  def handle_event("keycap_background", %{"background" => background}, socket) do
    current_priority = Map.get(@background_hierarchy, socket.assigns.background, 0)
    new_priority = Map.get(@background_hierarchy, background, 0)
    new_background =
      if new_priority > current_priority, do: background, else: socket.assigns.background
    socket = assign(socket, :background, new_background)
    {:noreply, socket}
  end

  ...

  def render(assigns) do
    ~H"""
    <kbd id={@id}
      phx-click="letter"
      phx-value-key={@keycap_value}
      class={"#{@background} kbd kbd-md sm:kbd-lg 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"}>

  ...
  end

...
end

With our component updated, let’s see how our game looks. We’ll go to the command line and start the server.

$ mix phx.server
...

And now when we submit a guess our guess row flips to reveal the result of the guess. Then the keycaps in our keyboard are updated with the corresponding background colors.

This is great! Our game is working, letting users submit guesses and then providing feedback for each guess.

Ready to Learn More?

Subscribe to get access to all episodes and exclusive content.

Subscribe Now