We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
LiveView 16 - Chapter 6: Build the Survey LiveView
Published on: 2025-12-30
Tags:
elixir, Blog, LiveView, Html/CSS, Phoenix
Build the Survey LiveView (195)
We will build a menu and a route to fit inside it and then the mount and the render.
Establish a Menu in a Layout
For most applications there will be some elements that are shared throughout the entire app. That can be within the root layout. That is defined within lib/pento_web/components/layouts/root.html.heex, we then can leverage that with the pipeline to make sure that it is part of the browser. lib/pento_web/router.ex and look for this line
...
plug(:put_root_layout, html: {PentoWeb.Layouts, :root})
...
end
Here is where the book wants us to add in a navigation links to the products and the Survey but I added it into the Layout.app part of the site. This use the Layout.app wrapper and places the nav if we use the wrapper. Ill add it also to the roots to see how it might look there.
<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>
<.link href={~p"/products"}>Products</.link>
</li>
<li>
<.link href={~p"/survey"}>Survey</.link>
</li>
...
<% end %>
</ ul>
Also be sure to look at the <% @inner_content %> that is were the current page will be rendered. Head to lib/pento_web/product_live/index.ex and you will see a use directive.
defmodule PentoWeb.ProductLive.Index do
use PentoWeb, :live_view
# Now you can see what that is calling with the file pento_web.ex
def live_view do
quote do
use Phoenix.LiveView
# This is missing from the latest version.
# layout: {PentoWeb.Layouts, :app}
# I believe that this is needed to be added to very file
# manually
unquote(html_helpers())
end
end
Now we can get into lib/pento_web/components/layouts.ex which is where I put the directory. When we first get the generated code from the generator there is a lot of Phoenix branding. Ill put the code from the book here as you will want to get rid of most of it but keep the dark/light switch
# lib/pento_web/components/layouts.ex
def app(assigns) do
~H"""
<header class="navbar px-4 sm:px-6 lg:px-8">
<div class="flex-1">
<a href="/" class="flex-1 flex items-center gap-2">
<span class="text-xl font-bold">Pento</span>
</a>
</div>
<div class="flex-none">
<ul class="flex flex-column px-1 space-x-4 items-center">
<li>
<.link
navigate={~p"/products"}
class="btn btn-ghost"
>
Products
</.link>
</li>
<li>
<.link navigate={~p"/survey"} class="btn btn-ghost">Survey</.link>
</li>
<li>
<.theme_toggle />
</li>
</ul>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-4">
{render_slot(@inner_block)}
</div>
</main>
<.flash_group flash={@flash} />
"""
end
Define the Survey Route
We will now define the route for the /survey route. pento/lib/pento_web/router.ex
live("/survey", SurveyLive, :index)
Make sure that it is part of the scope that has [:browser, :require_authenticated_user] so that a user will need to be logged in as well as has all the plugs we want the scope to have.
Mount the Survey Live View
Okay now we need to create the file pento/lib/pento_web/live/survey_live.ex. We will need to have the user in the socket to use later, UserAuth.on_mount/4 will take care of that for us. We will also want to have all the products that the user will be able to see, as well as the demographic for that user. Knowing that a user will need to be logged in we can assume that there will be a socket.assigns.current_user
defmodule PentoWeb.SurveyLive do
use PentoWeb, :live_view
alias Pento.{Survey, Catalog}
alias Pento.DemographicLive.Show
alias __MODULE__.Component
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign_demographic()
|> assign_products()
{:ok, socket}
end
defp assign_demographic(socket) do
demographic =
Survey.get_demographic_by_user(socket.assigns.current_scope)
assign(
socket,
:demographic,
demographic
)
end
defp assign_products(socket) do
products = Catalog.list_products(socket.assigns.current_scope)
assign(socket, :products, products)
end
end
Render the Template
The book wants us to leverage a survey_live.html.heex but again the new way of doing things is a render, so while staying on that file let's add in the render it will be simple to start.
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<section>
<h2> Survey </h2>
</section>
</Layouts.app>
"""
end
Build a Simple Function Component
So now we get to build a Component that can be leveraged anywhere in the app that we build. Here is some basic syntax for an entire component module
defmodule ComponentModule do
use Phoenix.Component
attr :attr1, :string, default: nil
attr :attr2, :integer
slot :inner_block, required: true
def function_component(assigns) do
~H"""
<p><%= render_slot(@inner_block) %></p>
"""
end
end
First we have the attr macro that defines attributes and the slot macro to define the the space between the tags. This is used to have compile time warnings about the components that we use and create.
We can use the inner_block to tell the component what we put between the wrapper will be displayed there.
Moving on when we call the component like this,
<ComponentModule.function_component attr1="value1" attr2={1+2} >
<!-- ... -->
</ComponentModule.function_component>
We are saying that %{attr1: "value1", attr2: 3} we can use this to make sure that we have values that we want and we will know that when we setup the component that we will be able to have data that we need. There is even ways to assure that certain attrs are needed.
Define a Function Component
Now let's build on of Our Own. Remember that when you are building a Module every . can be thought of as as folder to traverse through. Looking at the Module name and the alias
PentoWeb.SurveyLive and alias __MODULE__.Component
Let's make this file pento/lib/pento_web/live/survey_live/component.ex
defmodule PentoWeb.SurveyLive.Component do
use Phoenix.Component
attr(:content, :string, required: true)
slot(:inner_block, required: true)
def hero(assigns) do
~H"""
<div class="hero bg-gradient-to-r from-blue-500 to-purple-600 text-white">
<div class="hero-content text-center py-16">
<div class="max-w-md">
<h1 class="mb-5 text-5xl font-bold">{@content}</h1>
<div class="mb-5 text-lg">
{render_slot(@inner_block)}
</div>
</div>
</div>
</div>
"""
end
end
We will be able to call this with Component.hero
Use the Component
Head back to the survey_live.ex and add in this link to the render, or the html.heex
<Component.hero content="Survey">
Please fill out the survey.
</Component.hero>
That worked fine... Wonderful
Let's take a few minutes to see what happens with the compile time errors if we try and use incorrect data for the attrs.
so if content=123
** (Phoenix.LiveView.Tokenizer.ParseError) lib/pento_web/live/survey_live.ex:11:33: invalid attribute
Or with no inner block
key :inner_block not found in:
We can now take a closer look into what data is present within the assigns with this block of code at the bottom of the hero component
<pre>
<%= inspect(assigns, pretty: true) %>
<% %{ inner_block: [%{inner_block: block_fn}]} = assigns %>
<%= inspect(@inner_block, pretty: true) %>
</pre>
This will show you all the information that is within the block and the assigns for the component. Again this is just taking the values that are called within the component we made.
Build the Demographic Show Function Component
Okay so what do we want to show at the end of this? We want to show the demographics for the user and then the ratings for the games the user should be able to see. First let's start off by creating a new file. pento/lib/pento_web/live/demographic_live/show.ex
defmodule PentoWeb.DemographicLive.Show do
use Phoenix.Component
alias PentoWeb.CoreComponents
end
# This is the start of a new component so we need add in the details that we will show to the user.
attr(:demographic, :map, required: true)
def details(assigns) do
~H"""
<h2>Demographics ✅</h2>
<CoreComponents.table id="demographics" rows={[@demographic]}>
<:col :let={demographic} label="Gender">
<%= demographic.gender %>
</:col>
<:col :let={demographic} label="Year of Birth">
<%= demographic.year_of_birth %>
</:col>
</CoreComponents.table>
"""
end
We have the attrs set and then a list for the gender and the year of birth. We will flesh this out more but again we can see what happens when we don't pass the right information to the component while invoking it.
Okay now let's invoke it within the show.ex
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<Component.hero content="Survey">
Please fill out the survey.
</Component.hero>
<Show.details demographic={@demographic} />
</Layouts.app>
"""
end
This would be a simple way to show the demographic however if a user doesn't have a demo done it will show an error. We need some logic around the Show.details.
<div class="container mx-auto px-4 py-8 max-w-4xl">
<%= if @demographic do %>
<Show.details demographic={@demographic} />
<% else %>
<h3>Demographic form coming soon...</h3>
<% end %>
</div>
Let's take a second here to make a demo for a logged in user and then see if the site changes for that user.
iex -S mix
iex> alias Pento.Accounts
iex> alias Pento.Survey
iex> email = "your_logged_in_email" # use logged in user's email here
iex> user = Accounts.get_user_by_email(email)
iex> attrs = %{gender: "male", year_of_birth: 2020, user_id: user.id}
iex> scope = Accounts.get_scope_for_user(user)
iex> Survey.create_demographic(scope, attrs)
It should now look like the user has a demographic.