We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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