#181: Phoenix LiveView Tutorial Part 5
In our game, we’ll need a way to store a player’s guesses. When we created our Word and Solve modules we used Ecto to create a schema module that’s backed by a corresponding database table. To manage a player’s guesses we don’t really need them to be persisted in the database. So instead of using a more conventional Ecto schema - let’s use a Schemaless Ecto Changeset to manage guesses.
Because this will be the core data structure for the game, we’ll organize this into a new directory in “lib/werdle” called “game” and then we’ll create a new module inside of “game” called guesses.ex. We’ll define the module and since this will have an Ecto schema we’ll include use Ecto.Schema. As well as import Ecto.Changeset so that we can easily use the Ecto.Changeset functions.
Now for our Guesses, it will be temporary game state data that doesn’t need to live long-term. For that let’s use Ecto’s embedded_schema function. This will allow us to leverage Ecto’s powerful schema and changeset features for data validation and casting, without having to persist data to the database. Let’s define the embedded_schema and inside it, we’ll create a “guess” field for each of the 6 guesses a player gets in our game. Our field will be an array of strings with each character in the array mapping to a letter in a word guess. Then let’s define a changeset function that will take some attributes.
Now we want our changeset function to validate that the length of each guess is 5 characters long. Let’s create a module attribute called @guess_fields that contains a list of all our fields. Then let’s go back to our changeset function and use the __MODULE__ Elixir macro that expands to the current module’s name to return a Guesses struct. Then we’ll cast our attributes with the cast function passing in any attributes and the @guess_fields to return a changeset.
We also want to validate that each guess has 5 characters. To do that let’s create a private function called validate_guess_fields that will take our Guesses changeset. And inside it, we’ll call Enum.reduce passing in the enumerable of our @guess_fields and an accumulator which will be our changeset and then a function, which will receive a field from our @guess_fields and then our accumulator changeset. In each function we’ll call Ecto.Changeset’s validate_length function, passing in our changeset, the field and that that field is 5 characters. Then we’ll add validate_guess_fields to our changeset function.
lib/werdle/game/guesses.exdefmodule Werdle.Game.Guesses do
use Ecto.Schema
import Ecto.Changeset
@guess_fields [:guess_0, :guess_1, :guess_2, :guess_3, :guess_4, :guess_5]
embedded_schema do
field :guess_0, {:array, :string}, default: []
field :guess_1, {:array, :string}, default: []
field :guess_2, {:array, :string}, default: []
field :guess_3, {:array, :string}, default: []
field :guess_4, {:array, :string}, default: []
field :guess_5, {:array, :string}, default: []
end
def changeset(attrs \\ %{}) do
%__MODULE__{}
|> cast(attrs, @guess_fields)
|> validate_guess_fields()
end
defp validate_guess_fields(changeset) do
Enum.reduce(@guess_fields, changeset, fn field, acc_changeset ->
validate_length(acc_changeset, field, is: 5, message: "should be five characters")
end)
end
end
Great, now that we have our Guesses module, let’s create another module that we can use to encapsulate our game logic. Let’s call it game.ex. We’ll define the module and then let’s create a function to return a Guesses changeset. We’ll call it change_guesses and it will accept attributes with a default of an empty map.
Then we’ll call Guesses.changeset passing in any attributes. Let’s also alias the Guesses module so we can call it without the prefix.
lib/werdle/game.exdefmodule Werdle.Game do
alias Werdle.Game.Guesses
def change_guesses(attrs \\ %{}) do
Guesses.changeset(attrs)
end
end
Now let’s test our new Guesses changeset to ensure that it’s working how we expect it to.
We’ll go to the command line and call iex -S mix to start an IEx session with our application. Then let’s call Werdle.Game.change_guesses passing in a map with the guess_0 field and then a list that only has 4 characters. And we see that the changeset is not valid.
Now let’s try it again, but with a guess that’s 5 characters. Great, this time it’s a valid changeset.
$ iex -S mix
> Werdle.Game.change_guesses(%{guess_0: ["a", "p", "p", "l"]})
...
> Werdle.Game.change_guesses(%{guess_0: ["a", "p", "p", "l", "e"]})
...
Now that we have a way to return a Guesses changeset let’s go back to our WordLive.Index LiveView module and we’ll update our changeset assign to use the actual Guesses changeset. We’ll include an alias for the Game module so we can call it without the prefix.
lib/werdle_web/live/word_live/index.ex...
alias Werdle.Game
...
@impl true
def mount(_params, _session, socket) do
...
|> assign(:changeset, Game.change_guesses())
...
end
...
Now our live view will start with an initial Guesses changeset we can use to manage the state of our game.