Home Posts Tags Post Search Tag Search

Post 94

LiveView 09 - Chapter 4 Generators: Your Turn

Published on: 2025-12-12 Tags: elixir, Generators , Blog, Side Project, Libraries, LiveView, Ecto, Html/CSS, Phoenix
Your Turn
        The route is the place where we can start to debug all the files and modules that we are using throughout the page. 
        Everything that we see is written somewhere in the code. If you see something like <.blah> it is going to be a core component. 
        There are a few things that might be within a layout but that is usually things that will be wrapped around your current page. 
        The general series of events will be: Mount -> Render -> Handle_* -> Render
        There will be different routes that you will need to use for all the CRUD actions.

    Give it a Try
        Trace Through a Live View
            Start from the Index page’s implementation of the link to the product show page and work your way through the route, mount/3, handle_params/3, and render/1 lifecycle. Answer these questions:
            • Which route gets invoked when you click the link on the Index page to view a given product?
                live "/products/:id
            • What data does ProductLive.Show.mount/3 add to the socket?
                New page title and the product as long as you are allowed to see it.
            • How does the ProductLive.Show live view use the handle_params/3 callback?
                In my case it doesn't use the handle_params, but in this case it would deal with the form needing to be rendered so that a user can update a product
            • How does the ProductLive.Show template render the Product Edit form and what events does that form support?
                In this context the form will only support the edit event. As you are accessing the form from the show page and that will only use the current product.
            
        When you’re done, display your own message on the page by adding some content to the ProductLive.Show live view’s socket.assigns and then rendering it in the template.
            I went ahead and added in the timex lib to the project and then added a line to the header that shows when the product was created
                # pento/mix.exs
                {:timex, "~> 3.7"}

                # Linux command line
                mix deps.get

                #pento/lib/pento_web/live/product_live/show.ex
                <.header>
                    Product {@product.id}
                    <:subtitle>This is a product record from your database.<br /></:subtitle>
                    <:subtitle>
                    Created at {Timex.format!(@product.inserted_at, "{Mfull} {D}, {YYYY} at {h12}:{m} {AM}")}
                    </:subtitle>
                    ...
                </.header>

        Generate Your Own Live View
            This one will be a bit more involved but you will want to create your own generator command that will add in a FAQ for your page. It will be a new db that will have the following columns:
                question
                answers
                vote count

            This will involve some relationships but you can make is so there is no need to assign a question or answer to a user. Maybe just be sure that they are logged in to see the FAQ.
                I will do this as the books wants me to but a better way would be like a post/comment relationship. Where any user can post a question and any user can post an answer, that way you can have some better interface for the project. But right now the plan is to create a single database that will house the question, answer and the vote count. 

                    mix phx.gen.live Faq Question questions question:string answer:string vote_count:integer

                    mix ecto.migrate

                Now you need to start to edit all the files to get the right functionality

                    # pento/lib/pento_web/router.ex
                    live("/questions", QuestionLive.Index, :index)
                    live("/questions/new", QuestionLive.Form, :new)
                    live("/questions/:id", QuestionLive.Show, :show)
                    live("/questions/:id/edit", QuestionLive.Form, :edit)
                    live("/questions/:id/answer", QuestionLive.AnswerForm, :edit)

                Let's head into the files and change a few things as I only want you to have to be logged in to see and add questions. I don't want you only to be able to see the questions that you make.
                    # lib/pento/faq/question.ex
                    @doc false
                    def changeset_answer(question, attrs) do
                        question
                        |> cast(attrs, [:answer])
                        |> validate_required([:answer])
                    end

                    schema "questions" do
                        field :question, :string
                        field :answer, :string
                        field :vote_count, :integer, default: 1
                        field :user_id, :id

                        timestamps(type: :utc_datetime)
                    end

                I want to be able to have an other form that will just add in a question.
                    # pento/lib/pento/faq.ex
                    @doc """
                    Returns the list of all questions.

                    ## Examples

                        iex> list_all_questions()
                        [%Question{}, ...]
                    """
                    def list_all_questions do
                        Repo.all(Question)
                    end

                    @doc """
                    Gets a single question. Returns nil if the Question does not exist.

                    ## Examples

                        iex> get_question(123)
                        %Question{}

                        iex> get_question(456)
                        nil
                    """
                    def get_question(id) do
                        Repo.get(Question, id)
                    end

                    @doc """
                    Increases the vote count of a question by 1.

                    ## Examples

                        iex> increase_vote_count(question)
                        {:ok, %Question{}}
                    """
                    def increase_vote_count(%Question{} = question) do
                        question
                        |> Ecto.Changeset.change()
                        |> Ecto.Changeset.increment(:vote_count, 1)
                        |> Repo.update()
                    end

                    @doc """
                    Returns an `%Ecto.Changeset{}` for tracking question answer changes.

                    ## Examples

                        iex> change_question_answer(question)
                        %Ecto.Changeset{data: %Question{}}
                    """
                    def change_question_answer(%Question{} = question, attrs \\ %{}) do
                        Question.changeset_answer(question, attrs)
                    end

                    @doc """
                    Increases the vote count of a question by 1.

                    ## Examples

                        iex> increase_vote_count(question)
                        {:ok, %Question{}}
                    """
                    def increase_vote_count(%Question{} = question) do
                        question
                        |> Ecto.Changeset.change()
                        |> Ecto.Changeset.increment(:vote_count, 1)
                        |> Repo.update()
                    end

                    @doc """
                    Updates a question's answer.

                    ## Examples

                        iex> update_question_answer(question, %{answer: new_answer})
                        {:ok, %Question{}}

                        iex> update_question_answer(question, %{answer: bad_value})
                        {:error, %Ecto.Changeset{}}

                    """
                    def update_question_answer(%Question{} = question, attrs) do
                        with {:ok, question = %Question{}} <-
                            question
                            |> Question.changeset_answer(attrs)
                            |> Repo.update() do
                        {:ok, question}
                        end
                    end

                There is a lot here that I added as there will need to be a lot of functionality to deal with the new table and migration. Once this is all set the dev should have the tools needed to upvote, answer a question, and then update those answers if they own the answer. Now again keep in mind that this is not the best approach for this but Ill make it work.

                Okay so now we need to work on the pages to get the right logic. First let's deal with the index.
                    @impl true
                    def render(assigns) do
                        ~H"""
                        <Layouts.app flash={@flash} current_scope={@current_scope}>
                        <.header>
                            Listing Questions
                            <:actions>
                            <.button variant="primary" navigate={~p"/questions/new"}>
                                <.icon name="hero-plus" /> New Question
                            </.button>
                            </:actions>
                        </.header>

                        <.table
                            id="questions"
                            rows={@streams.questions}
                            row_click={fn {_id, question} -> JS.navigate(~p"/questions/#{question}") end}
                        >
                            <:col :let={{_id, question}} label="Question">{question.question}</:col>
                            <:col :let={{_id, question}} label="Answer">{question.answer}</:col>
                            <:col :let={{_id, question}} label="Vote count">{question.vote_count}</:col>
                            <:action :let={{_id, question}}>
                            <% if @current_scope.user && @current_scope.user.id == question.user_id do %>
                                <div class="sr-only">
                                <.link navigate={~p"/questions/#{question}"}>Show</.link>
                                </div>
                                <.link navigate={~p"/questions/#{question}/edit"}>Edit</.link>
                            <% end %>
                            </:action>

                            <:action :let={{id, question}}>
                            <% if @current_scope.user && @current_scope.user.id == question.user_id do %>
                                <.link
                                phx-click={JS.push("delete", value: %{id: question.id}) |> hide("##{id}")}
                                data-confirm="Are you sure?"
                                >
                                Delete
                                </.link>
                            <% end %>
                            </:action>
                        </.table>
                        </Layouts.app>
                        """
                    end

                    @impl true
                    def mount(_params, _session, socket) do
                        if connected?(socket) do
                        Faq.subscribe_questions(socket.assigns.current_scope)
                        end

                        {:ok,
                        socket
                        |> assign(:page_title, "Listing Questions")
                        |> stream(:questions, list_all_questions())}
                    end

                    @impl true
                    def handle_event("delete", %{"id" => id}, socket) do
                        question = Faq.get_question!(socket.assigns.current_scope, id)
                        {:ok, _} = Faq.delete_question(socket.assigns.current_scope, question)

                        {:noreply, stream_delete(socket, :questions, question)}
                    end

                    @impl true
                    def handle_info({type, %Pento.Faq.Question{}}, socket)
                        when type in [:created, :updated, :deleted] do
                        {:noreply, stream(socket, :questions, list_all_questions(), reset: true)}
                    end

                    defp list_questions(current_scope) do
                        Faq.list_questions(current_scope)
                    end

                    defp list_all_questions do
                        Faq.list_all_questions()
                    end

                I had to add in some functionality for the buttons so you can only edit the questions that you make. 

                Now to the show.ex
                    @impl true
                    def render(assigns) do
                        ~H"""
                        <Layouts.app flash={@flash} current_scope={@current_scope}>
                        <.header>
                            Question {@question.id}
                            <:subtitle>This is a question record from your database.</:subtitle>
                            <:actions>
                            <.button navigate={~p"/questions"}>
                                <.icon name="hero-arrow-left" />
                            </.button>
                            <%= if @current_scope.user && @current_scope.user.id == @question.user_id do %>
                                <.button variant="primary" navigate={~p"/questions/#{@question}/edit?return_to=show"}>
                                <.icon name="hero-pencil-square" /> Edit question
                                </.button>
                            <% end %>
                            <.button variant="primary" navigate={~p"/questions/#{@question}/answer?return_to=show"}>
                                <.icon name="hero-question-mark-circle" /> Answer question
                            </.button>
                            </:actions>
                        </.header>
                        <.list>
                            <:item title="Question">{@question.question}</:item>
                            <:item title="Answer">{@question.answer}</:item>
                            <:item title="Vote count">{@question.vote_count}</:item>
                        </.list>
                        <%= if @upvoted == false do %>
                            <.button phx-click="increase_vote_count" variant="primary">
                            <.icon name="hero-arrow-up" /> Upvote
                            </.button>
                        <% end %>
                        </Layouts.app>
                        """
                    end

                    @impl true
                    def mount(%{"id" => id}, _session, socket) do
                        if connected?(socket) do
                        Faq.subscribe_questions(socket.assigns.current_scope)
                        end

                        {:ok,
                        socket
                        |> assign(:page_title, "Show Question")
                        |> assign(:question, Faq.get_question(id))
                        |> assign(:upvoted, false)}
                    end

                    @impl true
                    def handle_event("increase_vote_count", _value, socket) do
                        {:ok, question} = Faq.increase_vote_count(socket.assigns.question)

                        {:noreply,
                        socket
                        |> assign(:question, question)
                        |> assign(:upvoted, true)}
                    end

                More needed info to deal with the logic for when you are a user that has made the question the other part was I wanted to add in a route for answering a question. This will be editable for everyone. Also I wanted to only be able to upvote once per page refresh, I know it's not optimal but I would need more databases for this to work right.

                Now on to the first form.
                    @impl true
                    def render(assigns) do
                        ~H"""
                        <Layouts.app flash={@flash} current_scope={@current_scope}>
                        <.header>
                            {@page_title}
                            <:subtitle>Use this form to manage question records in your database.</:subtitle>
                        </.header>

                        <.form for={@form} id="question-form" phx-change="validate" phx-submit="save">
                            <.input field={@form[:question]} type="text" label="Question" />
                            <footer>
                            <.button phx-disable-with="Saving..." variant="primary">Save Question</.button>
                            <.button navigate={return_path(@current_scope, @return_to, @question)}>Cancel</.button>
                            </footer>
                        </.form>
                        </Layouts.app>
                        """
                    end

                With the defaults for a question and the values all saved for a when you load in a question to edit there wasn't much that needed to be changed here I just needed to remove the ability to edit the vote and the answer.
                    defmodule PentoWeb.QuestionLive.AnswerForm do
                        use PentoWeb, :live_view

                        alias Pento.Faq
                        alias Pento.Faq.Question

                        @impl true
                        def render(assigns) do
                            ~H"""
                            <Layouts.app flash={@flash} current_scope={@current_scope}>
                            <.header>
                                {@page_title}
                                <:subtitle>Use this form to manage question answers in your database.</:subtitle>
                            </.header>

                            <.form for={@form} id="question-form" phx-change="validate" phx-submit="save">
                                <.input field={@form[:question]} type="text" label="Question" disabled/>
                                <.input field={@form[:answer]} type="text" label="Answer" />
                                <footer>
                                <.button phx-disable-with="Saving..." variant="primary">Save Answer</.button>
                                <.button navigate={return_path(@current_scope, @return_to, @question)}>Cancel</.button>
                                </footer>
                            </.form>
                            </Layouts.app>
                            """
                        end

                        @impl true
                        def mount(params, _session, socket) do
                            {:ok,
                            socket
                            |> assign(:return_to, return_to(params["return_to"]))
                            |> apply_action(socket.assigns.live_action, params)}
                        end

                        defp return_to("show"), do: "show"
                        defp return_to(_), do: "index"

                        defp apply_action(socket, :edit, %{"id" => id}) do
                            question = Faq.get_question(id)

                            socket
                            |> assign(:page_title, "Edit Question")
                            |> assign(:question, question)
                            |> assign(:form, to_form(Faq.change_question_answer(question)))
                        end

                        @impl true
                        def handle_event("validate", %{"question" => question_params}, socket) do
                            changeset =
                            Faq.change_question_answer(socket.assigns.question, question_params)

                            {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
                        end

                        def handle_event("save", %{"question" => question_params}, socket) do
                            save_question(socket, socket.assigns.live_action, question_params)
                        end

                        defp save_question(socket, :edit, question_params) do
                            case Faq.update_question_answer(
                                socket.assigns.question,
                                question_params
                                ) do
                            {:ok, question} ->
                                {:noreply,
                                socket
                                |> put_flash(:info, "Answer updated successfully")
                                |> push_navigate(
                                to: return_path(socket.assigns.current_scope, socket.assigns.return_to, question)
                                )}

                            {:error, %Ecto.Changeset{} = changeset} ->
                                {:noreply, assign(socket, form: to_form(changeset))}
                            end
                        end

                        defp return_path(_scope, "index", _question), do: ~p"/questions"
                        defp return_path(_scope, "show", question), do: ~p"/questions/#{question}"
                    end

                This one took a lot as I needed to leverage all the functions that I made to be able to edit the question entry but just the needed parts.

                Last thing that I did was add a new plug that will redirect the user to the /guess route if they are logged in an on the home page.
                    # pento/lib/pento_web/plugs/redirect_if_logged_in.ex
                    defmodule PentoWeb.Plugs.RedirectIfLoggedIn do
                        import Plug.Conn
                        import Phoenix.Controller

                        def init(opts), do: opts

                        def call(%Plug.Conn{assigns: %{current_scope: %{user: user}}} = conn, _opts)
                            when not is_nil(user) do
                            conn
                            |> redirect(to: "/guess")
                            |> halt()
                        end

                        def call(conn, _opts), do: conn
                    end

                Then we need to add that plug to a pipe line then add the pipeline to the scope for the / homepage
                    # pento/lib/pento_web/router.ex
                    pipeline :redirect_if_logged_in do
                        plug(PentoWeb.Plugs.RedirectIfLoggedIn)
                    end

                    scope "/", PentoWeb do
                        pipe_through([:browser, :redirect_if_logged_in])

                        get("/", PageController, :home)
                    end