Home Posts Tags Post Search Tag Search

Post 89

LiveView 04 - Chapter 2 Your Turn

Published on: 2025-12-02 Tags: elixir, Blog, Side Project, LiveView, Ecto, Html/CSS, Phoenix
Your Turn
        Let's try to add in these features to the site:
            Add in real email service to the site, (SendGrid or Mailgun)
                Okay so for this one Ill go over the way to use SendGrid, for this you will need to have a SendGrid account and the API key. Once you have that you will need to head to config/dev.exs and config/runtime.exs there you will add theses lines
                    config :pento, Pento.Mailer,
                        adapter: Swoosh.Adapters.Sendgrid,
                        api_key: System.get_env("SENDGRID_API_KEY")

                This will set up the config for the dev env and the runtime env so that it can use the SendGrip API. Once that is set you can then set the API key with an env variable
                    export SENDGRID_API_KEY="your_api_key_here"
                And then test to be sure it is in the system with
                    echo $SENDGRID_API_KEY

                Now this all assumes that you have emails that you can send with the company have verified the email domain and that you have set the correct info in the user_notifier like I will show below
                    # Delivers the email using the application mailer.
                    defp deliver(recipient, subject, body) do
                        email =
                        new()
                        |> to(recipient)
                        |> from({"Pento", "contact@example.com"})
                        |> subject(subject)
                        |> text_body(body)

                        with {:ok, _metadata} <- Mailer.deliver(email) do
                        IO.puts("Email sent to #{recipient} with subject #{subject}")
                        {:ok, email}
                        end
                    end
                
                Replace the contact@example.com with the domain that you have set for yourself. With that all set you shouldn't need to do anything else.

            Add in a new migration field to add a username, do you replace the email as the username? Can you still use the magic_link?
                For this I will need to do a few things. In this case I will need to add in a new ecto migration with the following command:
                    mix phx.gen.migration add_username_to_users

                This will create a new file that will be responsible for adding a new column to the users table. head to priv/repo/migrations/***_add_username_to_users
                    defmodule Pento.Repo.Migrations.AddUsernameToUsers do
                        use Ecto.Migration

                        def change do
                            alter table(:users) do
                                add(:username, :string, unique: true)
                            end

                            create(unique_index(:users, [:username]))
                        end
                    end

                This is an alter table change you want to use this as it will be taking a table that already exists and changing it. We can see that we are adding in a username column, we are making sure that the database is requiring that the username is unique. With this done we can migrate the database and add in the new column.
                    mix ecto.migrate

                Okay that is now out of the way we now need to head to the schema for that table and add in some functionality for the new column. lib/pento/accounts/user.ex
                    schema "users" do
                        field :username, :string
                        # ...
                    end

                Now let's deal with the changeset for the username we will follow the same process for the email and set a changeset for the username and then validate for the same process.
                    @doc """
                    A user changeset for changing the username.

                    It requires the username to change otherwise an error is added.
                    """

                    def username_changeset(user, attrs) do
                        user
                        |> cast(attrs, [:username])
                        |> validate_required([:username])
                        |> validate_length(:username, min: 3, max: 50)
                        |> unique_constraint(:username)
                    end

                    def validate_username_changed(changeset) do
                        if get_field(changeset, :username) && get_change(changeset, :username) == nil do
                            add_error(changeset, :username, "did not change")
                        else
                            changeset
                        end
                    end

                We also need to affect the Accounts file as well as that will be what we use to update the users table. lib/pento/accounts.ex
                    @doc """
                    Returns an `%Ecto.Changeset{}` for changing the username.

                    ## Examples

                        iex> change_username(user)
                        %Ecto.Changeset{data: %User{}}
                    """
                    def change_username(user, attrs \\ %{}) do
                        User.username_changeset(user, attrs)
                    end

                    @doc """
                    Updates the username of the user.
                    ## Examples

                        iex> update_username(user, %{username: ...})
                        {:ok, %User{}}
                        iex> update_username(user, %{username: "ab"})
                        {:error, %Ecto.Changeset{}}
                    """
                    def update_username(user, attrs) do
                        user
                        |> User.username_changeset(attrs)
                        |> User.validate_username_changed()
                        |> Repo.update()
                    end

                This function builds and validates the username changeset during form updates. The update_username/2 function is responsible for actually writing the new value to the database using Repo.update/1. The LiveView settings UI uses change_username/2 for validation during typing and update_username/2 when the user submits the form.

                Okay that is now out of the way we need to be sure that we are using this whenever we are running the settings changes. Let's head to the settings file to add in the new event handlers and add in the rest of the markdown. lib/pento_web/live/user_live/settings.ex
                    def render(assigns) do
                        ~H"""
                        <Layouts.app flash={@flash} current_scope={@current_scope}>
                        <div class="text-center">
                            <.header>
                            Account Settings
                            <:subtitle>Manage your account email address and password settings</:subtitle>
                            </.header>
                        </div>

                        ...
                        <div class="divider" />

                        <.form for={@username_form} id="username_form" phx-submit="update_username" phx-change="validate_username">
                            <.input
                            field={@username_form[:username]}
                            type="text"
                            label="Username"
                            autocomplete="username"
                            required
                            />
                            <.button variant="primary" phx-disable-with="Changing...">Change Username</.button>
                        </.form>
                        </Layouts.app>
                        """
                    end

                    def handle_event("validate_username", params, socket) do
                        %{"user" => user_params} = params

                        username_form =
                        socket.assigns.current_scope.user
                        |> Accounts.change_username(user_params)
                        |> Map.put(:action, :validate)
                        |> to_form()

                        {:noreply, assign(socket, username_form: username_form)}
                    end

                    def handle_event("update_username", %{"user" => attrs}, socket) do
                        user = socket.assigns.current_scope.user
                        true = Accounts.sudo_mode?(user)

                        case Accounts.update_username(user, attrs) do
                        {:ok, user} ->
                            {:noreply,
                            socket
                            |> put_flash(:info, "Username updated")
                            |> assign(username_form: to_form(Accounts.change_username(user)))}

                        {:error, changeset} ->
                            {:noreply, assign(socket, username_form: to_form(changeset, action: :insert))}
                        end
                    end

            Make a user auto redirect to the /guess route
                This is pretty simple for the scope of the project you need to head to the lib/pento_web/user_auth.ex and then head to the log_in_user function. Once the session is created you will have the redirect function. This is there to take a user from the login page to a page there where once at or a page of your choosing.
                    @doc """
                    Logs the user in.

                    Redirects to the session's `:user_return_to` path
                    or falls back to the `signed_in_path/1`.
                    """
                    def log_in_user(conn, user, params \\ %{}) do
                        user_return_to = get_session(conn, :user_return_to)

                        conn
                        |> create_or_extend_session(user, params)
                        |> redirect(to: user_return_to || ~p"/guess")
                    end

                As you can see there is a normal path of singed_in_path(conn)

            Add in more information into the scopes and think about displaying it on the page or the default layout.
                For this at the moment Ill just talk about what we could put into the scope. As of right now we only have the user and all its information within the scope. We just recently added in the username to the user database so I'll just be sure to add in the user name into the display at the top. Let's head to the pento/lib/pento_web/components/layouts/root.html.heex
                    <body>
                        <ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
                        <%= if @current_scope do %>
                            <li>
                            # This line here
                            {@current_scope.user.email}(<%= @current_scope.user.username || "no username" %>)
                            </li>
                            <li>
                            <.link href={~p"/users/settings"}>Settings</.link>
                            </li>
                            <li>
                            <.link href={~p"/users/log-out"} method="delete">Log out</.link>
                            </li>
                        <% else %>
                            <li>
                            <.link href={~p"/users/register"}>Register</.link>
                            </li>
                            <li>
                            <.link href={~p"/users/log-in"}>Log in</.link>
                            </li>
                        <% end %>
                        </ul>
                        {@inner_content}
                    </body>

                Now going to the scopes page let's talk about what we can add to the scope. So for this simple example I want to add in a flag for whether or not a username has been set.
                    defmodule Pento.Accounts.Scope do
                        @moduledoc """
                    
                        alias Pento.Accounts.User

                        defstruct user: nil,
                                    user_name_set?: false

                        @doc """
                        Creates a scope for the given user.

                        Returns nil if no user is given.
                        """
                        def for_user(%User{} = user) do
                            %__MODULE__{
                            user: user,
                            user_name_set?: user.username != nil && user.username != ""
                            }
                        end

                        def for_user(nil), do: nil
                    end
                    
                We now can have a check for whether or not they have a username and be sure to have them set it if we want. There is also a setting that we could have for whether or not they have confirmed their email and so much more.