Home Posts Tags Post Search Tag Search

Post 93

LiveView 08 - Chapter 4 Generators: Live View and Templates

Published on: 2025-12-10 Tags: elixir, Blog, Side Project, LiveView, Ecto, Html/CSS, Phoenix
Chapter 4 Generators: Live View and Templates
    Here is the plan for this chapter:
        Routes - use them to understand views that our generator has made. 
        Inventory - see what has been generated
        Architecture - see how everything is organized. 
        Forms - how form components takes care of all teh creation and editing of pages

    Application Inventory
        Head to pento/lib/pento_web/router.ex this will show us a lot about all the files that we are using when we build a webpage. Let's start by looking at the routes that the generator asked us to add to the scope that we want them in. 
            live("/products", ProductLive.Index, :index)
            live("/products/new", ProductLive.Form, :new)
            live("/products/:id", ProductLive.Show, :show)
            live("/products/:id/edit", ProductLive.Form, :edit)

        The live macro is installed with the use PentoWeb, :router we can head to that file now pento/lib/pento_web.ex
            def router do
                quote do
                use Phoenix.Router, helpers: false

                # Import common connection and controller functions to use in pipelines
                import Plug.Conn
                import Phoenix.Controller
                import Phoenix.LiveView.Router
                end
            end

        This to dumb it down ties a URL patter to a LiveView module. Let's talk about the different params that the live has:
            1. URL Pattern, any : implies that it is a variable and can have different values. Think products/7, this will also map the :id to something like %{"id" => "7"} withing the assigns. 
            2. LiveView Module, Form-Show-Index are the normal types of modules that we will use here from CRUD. They will allow us to build forms, show a single product, or show a list of products.
            3. Live Action, this will let the LiveView know which action to perform.

        Explore the Generated Files
            Running the generator created a lot of files here is a snippet of some of the files that it created.
                * creating lib/pento_web/live/product_live/show.ex (with embedded render/1)
                * creating lib/pento_web/live/product_live/show.html.heex (fallback template)
                * creating lib/pento_web/live/product_live/index.ex (with embedded render/1)
                * creating lib/pento_web/live/product_live/index.html.heex (fallback template)
                * creating lib/pento_web/live/product_live/form.ex (with embedded render/1)
                * creating lib/pento_web/live/product_live/form.html.heex (fallback template)
                * creating test/pento_web/live/product_live_test.exs

            Quick side note while in the past the html.heex was needed to display a render if there is a render in any of the plain, form.ex etc that will take precedent.

            Mount will load all the needed elements and all the static ones and then the render will display the page, there are functions like handle_param that can deal with some changes but most of the time we are dealing with render and handle_event to change things on the page.

    Mount and Render the Product Index
        Now that we have the idea of what is being called and what functions are taking care of the initial page and the subsequent loads we can dive into what the functions actually are doing. 
        
        Remember that the mount is there to load all the needed info off the bat, so head to pento/lib/pento_web/live/product_live/index.ex and take a look at the mount.
            alias Pento.Catalog

            @imp true
            def mount(_params, _session, socket) do
                {:ok, stream(socket, :products, Catalog.list_products())}
            end

            # pento/lib/pento/catalog.ex add this function to get all the products if you don't have it
                @doc """
                Returns the list of all products.
                """
                def list_products() do
                    Repo.all(Product)
                end

        Now this is not what the current page might look like but you can see this would be a very simple way to build the initial set of products. Let's look into the stream part of the mount. This is useful when you have a massive list of products and you don't want to have all the products at any given time within the socket. When you build the list a new key is added to the socket assigns which can be referenced with @streams.products. Once this is built it will be passed to the client then using any delete or add will then ask the client to update their copy of the stream.

        Now staying in the same file let's add in some extra keys like so. I would add them to both the basic function we created (if the generator didn't have them) and the more robust.
            {:ok,
                socket
                |> assign(:page_title, "Listing Products")
                |> assign(:greeting, "Welcome to Pento!")
                |> stream(:products, Catalog.list_products())}
        
        The assign/3 is built-in and will add a key/value pair to the socket within :assigns this can also be used to update a already existing key/value pair. Once the pair is with the socket you can access the value with @key where keys is the atom for the key we just made.

        Let's head to the render part of the code and add in the greeting.
            <h1 class="text-3xl font-bold">{@greeting}</h1>

        You can now start up the server and see what you did, make sure to head to /products
            mix phx.server

        Understanding LiveView Behavior
            So let's take some time and understand the behavior of the live views yes they are called behaviors. Think of them like plugs, the behavior runs a specified application and calls your code according to a contract. The contract specifies which callback functions are available (mount/3, render/1, handle_*)

            So yes mount happens before render but mount doesn't call render the behavior calls mount then render. Think of it like below but understand that they are not just passing info there is layers of elixir code that will transform the data so that the next callback can deal with the information. 
                socket |> mount() |> render() |> handle_event() |> render()
            
        Route to the Product Index
            So looking back at the router.ex we see that the /products route will make the pattern to the code that will execute the request. The first job is the mount/3 the next is the render/1
                browser calls the get /products
                liveview mounts the products
                liveview renders the page
                browser renders the html
            
            Keep in mind that the simple action of establishing the connection will build a few things within the socket to keep the connection alive, once that is done we can utilize the assigns portion of the socket.

        Establish Product Index State
            Okay so we know that the mount is the first thing that is called but you may not have noticed the @impl true. This is to denotes the behavior that we are jacking to implement a behavior. The true thing that we are replace is @impl Phoenix.LiveView so that can be used instead to be more clear. Let's now use the built in mount/3 that the generator created. 
                @impl true
                def mount(_params, _session, socket) do
                    if connected?(socket) do
                        Catalog.subscribe_products(socket.assigns.current_scope)
                    end

                    {:ok,
                    socket
                    |> assign(:page_title, "Listing Products")
                    |> assign(:greeting, "Welcome to Pento!")
                    |> stream(:products, list_products(socket.assigns.current_scope))}
                end

            So now that we have all the products in the stream let's look at the assigns. Add this to the index.ex 
                <pre><%= inspect assigns, pretty: true %></pre>
            
            It will show something like this
                streams: %{
                    __changed__: MapSet.new([:products]),
                    __configured__: %{},
                    __ref__: 1,
                    products: %Phoenix.LiveView.LiveStream{
                    name: :products,
                    dom_id: #Function<4.74013028/1 in Phoenix.LiveView.LiveStream.new/4>,
                    ref: "0",
                    inserts: [
                        {"products-4", -1,
                        %Pento.Catalog.Product{
                        __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
                        id: 4,
                        name: "Backgammon",
                        description: "An ancient strategy game",
                        unit_price: 15.0,
                        sku: 9876543,
                        user_id: 3,
                        inserted_at: ~U[2025-12-09 20:17:15Z],
                        updated_at: ~U[2025-12-09 20:17:15Z]
                        }, nil, nil},
                        {"products-3", -1,
                        %Pento.Catalog.Product{
                        __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
                        id: 3,
                        name: "Checkers",
                        description: "A classic board game",
                        unit_price: 8.0,
                        sku: 1234567,
                        user_id: 3,
                        inserted_at: ~U[2025-12-09 20:17:15Z],
                        updated_at: ~U[2025-12-09 20:17:15Z]
                        }, nil, nil},
                        {"products-2", -1,
                        %Pento.Catalog.Product{
                        __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
                        id: 2,
                        name: "Chess",
                        description: "The classic strategy game",
                        unit_price: 10.0,
                        sku: 5678910,
                        user_id: 3,
                        inserted_at: ~U[2025-12-09 20:17:15Z],
                        updated_at: ~U[2025-12-09 20:17:15Z]
                        }, nil, nil},
                        {"products-1", -1,
                        %Pento.Catalog.Product{
                        __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
                        id: 1,
                        name: "Pentominoes",
                        description: "A super fun game!",
                        unit_price: 5.0,
                        sku: 123456,
                        user_id: nil,
                        inserted_at: ~U[2025-12-09 00:58:12Z],
                        updated_at: ~U[2025-12-09 00:58:12Z]
                        }, nil, nil}
                    ],
                    deletes: [],
                    reset?: false,
                    consumable?: false
                    }
                },
                live_action: :index,
                greeting: "Welcome to Pento!"

            This is the assigns part of the socket. notice the that the streams is part of it and that the products reflect a LiveStream, also notice the action at the bottom and then notice the inserts part of the stream, this is saying to add them to the list, there might be times that you will need to remove things from the list.

            Again you don't have to tell it how to do a thing just what you want it to do. Let's move onto the render

        Render Product Index State
            Within my code there wasn't a generated html.heex, but let's take a second to go over the need for it (in the past). So let's talk about the heex and the eex, so the heex is designed to render as little as possible in the page. Once the initial page is rendered there will be static things an then there will be things that might need to be rendered after a change has taken place. 

            Think about it like all the plain text is rendered once and all the @tags and injected code <%= %> will be dynamic and the heex will deal with those. 

            The LiveView makes all the needed data stored within the socket.assigns available so that we can dynamically change those values. Then once those values change the they are stored so the heex can re-render the page. 

            Now let's dive in the render and the form of the page the general sections will be
                ...heading...
                ...product list...
                ...modal form...

    Use Components to Render HTML
        Components implement each section of the page. Phoenix Components are meant to work well with markup. Let's stay in the file pento/lib/pento_web/live/product_live/index.ex and look at the header
            <.header>
                Listing Products
                <:actions>
                <.button variant="primary" navigate={~p"/products/new"}>
                    <.icon name="hero-plus" /> New Product
                </.button>
                </:actions>
            </.header>

        <.header> is syntax to call the header Component. This will be responsible for some of the style and then the :actions is an other Component for navigation but it can be other actions.

        Examine the Generated Code
            Let's head to the Components deps/phoenix/priv/templates/phx.gen.live/core_components.ex this is the generator that created the components that we will use, there is a file that had all the core components that we are currently using but we can look at that later.
                defmodule <%= @web_namespace %>.CoreComponents do
                    ...
                    def header(assigns) do
                        ~H"""
                        <header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
                        <div>
                            <h1 class="text-lg font-semibold leading-8">
                            {render_slot(@inner_block)}
                            </h1>
                            <p :if={@subtitle != []} class="text-sm text-base-content/70">
                            {render_slot(@subtitle)}
                            </p>
                        </div>
                        <div class="flex-none">{render_slot(@actions)}</div>
                        </header>
                        """
                    end
            
            This takes our project name and creates a set of core components for our project. That file is stored at pento/lib/pento_web/components/core_components.ex you can head there to check out the style for the header.
                def header(assigns) do
                    ~H"""
                    <header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
                    <div>
                        <h1 class="text-lg font-semibold leading-8">
                        {render_slot(@inner_block)}
                        </h1>
                        <p :if={@subtitle != []} class="text-sm text-base-content/70">
                        {render_slot(@subtitle)}
                        </p>
                    </div>
                    <div class="flex-none">{render_slot(@actions)}</div>
                    </header>
                    """
                end

            This is why we see the format we do for the header and even the action. So the header is a function that takes some assigns and returns some heex markup.  In this case the header doesn't have any arguments so the assigns is empty. We could even pass in some of the vales with
                <.header class="bold" >
                    Listing Products
                    ...
                </header>

            Then use the passed param with
                def header(assigns) do
                    ~H"""
                        <%= @class %>
                    """
                end
            
            Now within the header there is 3 sections for rendering custom content
                inner_block - anything that isn't the below and within the <.header> #here </.header>
                subtitle -  in our current example this is not needed. But if it were used it would be a smaller set of text below the title.
                actions - this is where we can add in an action that we want to take place. In this case we want to leverage a new route to create a new product.
                    <:actions>
                        <.button variant="primary" navigate={~p"/products/new"}>
                            <.icon name="hero-plus" /> New Product
                        </.button>
                    </:actions>

                Within the components if you want to interpolate use the {} instead of the traditional <%= %>. Last bit is if you look at the data above the def header(assigns) you cans see some information about what is involved and what might be needed.
                    attr :class, :string, default: nil
                    
                    slot :inner_block, required: true
                    slot :subtitle
                    slot :actions

        Render the Header Component
            Just to be sure that we are on the same page looking back on the <.header> </.header> part of the render anything is the inner_block besides parts that are denoted with :subtitle or :action

        Render a List of Products as a Table
            Now this next bit might feel like a lot but its still using a core Component and there are parts for all the sections of the code. Let's look at the <.table> </.table> part of the code. 
                <.table
                    id="products"
                    rows={@streams.products}
                    row_click={fn {_id, product} -> JS.navigate(~p"/products/#{product}") end}
                >
                    <:col :let={{_id, product}} label="Name">{product.name}</:col>
                    <:col :let={{_id, product}} label="Description">{product.description}</:col>
                    <:col :let={{_id, product}} label="Unit price">{product.unit_price}</:col>
                    <:col :let={{_id, product}} label="Sku">{product.sku}</:col>
                    <:action :let={{_id, product}}>
                    <div class="sr-only">
                        <.link navigate={~p"/products/#{product}"}>Show</.link>
                    </div>
                    <.link navigate={~p"/products/#{product}/edit"}>Edit</.link>
                    </:action>
                    <:action :let={{id, product}}>
                    <.link
                        phx-click={JS.push("delete", value: %{id: product.id}) |> hide("##{id}")}
                        data-confirm="Are you sure?"
                    >
                        Delete
                    </.link>
                    </:action>
                </.table>

            Looking with out going to the core_components you cans see attributes for :id, :rows, :row_click. You can see slots for :col, :action. rows={@streams.products} tells us what will be populating the rows.

            To implement a column you will want to have a :col section for it (order matters), you give it a label, and then tell it where to look for the value. Action can be an other column but those require come phx-click and maybe a link this can get a little bigger.

        Navigation to Dedicated Form Pages
            So if you want to create a new product you head to the route for /products/new which uses the ProductLive.Form and the action :new, the route for /products/:id/edit uses the same ProductLive.Form but with :edit. This helps to make it:
                • Better accessibility: Screen readers and keyboard navigation work more naturally with dedicated pages
                • Bookmarkable URLs: Users can bookmark or share direct links to form pages
                • Cleaner state management: Each page has its own focused responsibility
                • Easier testing: Each page can be tested independently
                • Simplified code: No need to manage modal visibility state or complex JavaScript interactions

            Last things for this is that remember that once the page is rendered the static text will only get rendered once and only the rows or values with some dynamic values will change like only a product that changes will change. 

    Handle Change for the Product Edit
        Now we can talk about the edit. Again this is a new route so the page will need to be mounted and rendered, this way makes it so we can deal with more than one CRUD functionality.

        Route to the Product Edit
            This is the route for an edit (/products/:id/edit) so in the books case they are talking about using the module for ProductLive.Index, :edit that will show a form on the top of the page. 

            The way the series of event would work in that case is this.
                Browser runs the get for the index
                LiveView mounts the products
                LiveView gets the :edit action and handle_params will deal with the changes
                LiveView Renders the new elements
                Browser renders the html

        Navigate with a Live Patch
            Now this is for using the <.link patch={~p"/products/#{product}/edit"}>Edit</.link> that will use the handle_params/3 and try to only load what needs to be changed. With Phoenix 1.8 we go back to the brand new page to load the forms and use the ProductLive.Forms to deal with an edit or a new action.

        ProductLive.Form: Dedicated Form Handling
            Now we should talk about how we are setting it up with the latest version of Phoenix. Heading to pento/lib/pento_web/live/product_live/form.ex we can see there is an apply_action function for :new and :edit.
                defp apply_action(socket, :edit, %{"id" => id}) do
                    product = Catalog.get_product!(socket.assigns.current_scope, id)

                    socket
                    |> assign(:page_title, "Edit Product")
                    |> assign(:product, product)
                    |> assign(:form, to_form(Catalog.change_product(socket.assigns.current_scope, product)))
                end

            Instead of the handle_params/3 lifecycle, 1.8 uses the mount -> apply_action to deal with the different events that need to take place. The apply_helper will take the event the spawned it or the route that triggered it and send it to the apply_action within the mount
                @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
             
            This does make it different but there are some clear positives. 
                • Clearer mental model: Each page has one clear purpose
                • Simpler state management: No need to track which “mode” a view is in
                • Better URL structure: Each operation has its own clean, bookmarkable URL
                • Easier testing: Each LiveView can be tested independently
                • Improved accessibility: Screen readers and keyboard navigation work naturally

            Okay so we have the 2 ways of doing a form let's move onto the streams so we can deal with more CRUD

    Manage Data with Streams
        So looking at a portion of the stream element here we can see that the delete event has a JS.push event that will leverage the stream.
            <.link
                phx-click={JS.push("delete", value: %{id: product.id}) |> hide("##{id}")}
                data-confirm="Are you sure?"
            >
                Delete
            </.link>

        This will pass the product id to the server and trigger the :delete event
            @impl true
            def handle_event("delete", %{"id" => id}, socket) do
                product = Catalog.get_product!(socket.assigns.current_scope, id)
                {:ok, _} = Catalog.delete_product(socket.assigns.current_scope, product)

                {:noreply, stream_delete(socket, :products, product)}
            end

        We fetch the product from the DB destroy it and then we stream_delete/3 to be sure that the page gets the right information. This will result in the socket having something that looks like this:
            streams: %{
            __changed__: MapSet.new([:products]), ...
            ...
                products: %Phoenix.LiveView.LiveStream{
                    name: :products,
                    dom_id: #Function<3.76265957/1 in Phoenix.LiveView.LiveStream.new/4>,
                    ref: "0",
                    inserts: [],
                    deletes: [
                        {"products-1", -1,%...Product{...name: "Pentominoes",...}, nil}
                    ],
                    reset?: false,
                    consumable?: false
                }
                ...other keys...
                }

        As you can see there is a deletes section that has the product and the needed information to make sure that we get rid of the product on the page.

        To add to this there is an API that provides similar functionality for :add or :update
            @impl true
            def handle_info({type, %Pento.Catalog.Product{}}, socket)
                when type in [:created, :updated, :deleted] do
                {:noreply,
                stream(socket, :products, list_products(socket.assigns.current_scope), reset: true)}
            end

        Render Product Edit State
            Let's stay in the index.ex and look at how the index renders the template with :live_action and @streams.products, we are referring to the <.table></.table>

            You can see that the user will go to a brand new page and avoid having to deal with how they got there or the current state. This helps with:
                • Modal state management within the index view
                • Complex conditional rendering based on live actions
                • JavaScript-based modal show/hide logic
                • Nested live components within the same view

        Manage JavaScript in the Component
            This next bit is about the modal dialogue box that shows up to display messages. It uses JavaScript to take care of the work. If you want to dive deeper head to pento/lib/pento_web/components/core_components.ex

            I'm not seeing this in the core_components.ex I do see show/2 and hide/2

    Phoenix 1.8's Dedicated Form LiveView
        This is the last part to finally go over the form part of the products route.

        ProductLive.Form Structure
            So for this form to work it determines it behavior based on teh live action and the URL parameters. Then the apply_action/3 sends it to the right event. 
                • For :new action: Creates a fresh product struct with the current user’s ID
                • For :edit action: Loads the existing product from the database using the ID parameter

            Both will create an ecto changeset and then validate based off of the inputs. 

        Phoenix 1.8 Form Rendering
            In the 1.8 environment it will just is the standard .form component. Let's look at how the form is constructed.
                1. Form Component: Uses the .form function component with phx-change="validate" and phx-submit="save" events
                2. Input Components: Each field uses the .input component with automatic error handling and DaisyUI styling
                3. Event Handling: Events are sent directly to the ProductLive.Form LiveView (no need for phx-target or @myself)
                4. Cancel Navigation: Provides clean navigation back to the appropriate page using navigate attributes
        
            The Phoenix 1.8 approach eliminates several complexities from the old system:
                • No phx-target complexity: Events go directly to the current LiveView
                • No @myself references: Simplified event routing
                • Better styling: DaisyUI integration provides professional appearance
                • Cleaner templates: More readable HEEx markup without nested component complexity

        ProductLive.Form Event Handling
            The event handling is more straight forward now, really only 2 events.
                1. "validate": Triggered by phx-change when users type in form fields
                2. "save": Triggered by phx-submit when users submit the form            

            Pattern matching will determine which event is is trying to do. We can look at the :edit in this case.
                defp save_product(socket, :edit, product_params) do
                    case Catalog.update_product(
                        socket.assigns.current_scope,
                        socket.assigns.product,
                        product_params
                        ) do
                    {:ok, product} ->
                        {:noreply,
                        socket
                        |> put_flash(:info, "Product updated successfully")
                        |> push_navigate(
                        to: return_path(socket.assigns.current_scope, socket.assigns.return_to, product)
                        )}

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

            This one will update a product but there is a new feature that we haven't talked about yet and that is the flash message. The standard way to add in a flash message is with this syntax
                {:noreply,
                    socket
                    |> put_flash(:info, "Product updated successfully")
                    |> push_patch(to: socket.assigns.patch)}

            No with all this said the event that triggers one of the events for handle_info [:created, :updated, :deleted] it triggers a PubSub event to let everyone that is needing the info. The handle_info is waiting for changes that it needs to know.

            Here are some of the benefits of this approach:
                • Decoupled architecture: LiveViews don’t directly reference each other
                • Automatic updates: Index pages update automatically when forms save
                • Multi-user support: All connected users see updates in real-time
                • Simpler testing: Each LiveView can be tested independently

        Live Navigation with Patching
            This is more of the older what of doing thing but if you have the html.heex template you might see something like,
                <.live_component
                    module={PentoWeb.ProductLive.FormComponent}
                    id={@product.id || :new}
                    title={@page_title}
                    action={@live_action}
                    product={@product}
                    patch={~p"/products"}
                />

            Remember a patch will use the same mount and WebSocket and try to reload as little as possible. It instead uses the handle_params/3 function to deal with any new information.