Where does phoenix look for views?

Watching a video now of someone making a rule_controller.ex
Then for the view they make a file in the same folder called rule_html.ex
And somehow that file gets used by the controller. I’m not sure why. Is there some documentation on where phoenix will look for the view?

I also have seen in other people’s projects there is a controller but no view in the same directory. So not sure where it looks for it.

app/lib/app_web/controllers

It’s in the controllers folder.

In Phoenix, views are different. The views are broken down into two things:

  1. The view_html.ex
  2. and the folder.

Example:

So I have artist_controller.ex but for the view I have artist_html.ex and the folder artist_html.

Both of these, the artist_html.ex and the folder artist_html are in the controllers folder. That’s just how phoenix defaults it.

Within the folder artist_html is corresponding files that map to route like show and index.

defmodule GiraWeb.ArtistHTML do
  @moduledoc """
  This module contains pages rendered by ArtistController.

  See the `artist_html` directory for all templates available.
  """
  use GiraWeb, :html

  embed_templates "artist_html/*"

The artist_html.ex also have a line to refer to the folder. GiraWeb is my app name btw.


Router:

defmodule GiraWeb.Router do
  use GiraWeb, :router

...

  scope "/", GiraWeb do
    pipe_through :browser

    get "/artists", ArtistController, :index
    get "/artists/:page", ArtistController, :index
    get "/artist/:artist/:id", ArtistController, :show

...

controller:

defmodule GiraWeb.ArtistController do
  use GiraWeb, :controller

  alias Gira.Novel.Contexts.Novels.NovelContext
  alias Gira.Novel.Contexts.Artists.ArtistContext

  def index(conn, params) do
    artists = ArtistContext.list_artists_paginate(50, params)
    page_title = "Artist Listing"
    render(conn, :index, artists: artists, page_title: page_title)
  end

  def show(conn, %{"id" => id}) do
    artist = id |> ArtistContext.read_artist!()
    novels = id |> NovelContext.list_novels_by_artist_id()
    page_title = "Artist: " <> artist.artist_name
    render(conn, :show, artist: artist, novels: novels, page_title: page_title)
  end
end

artist_html.ex

defmodule GiraWeb.ArtistHTML do
  @moduledoc """
  This module contains pages rendered by ArtistController.

  See the `artist_html` directory for all templates available.
  """
  use GiraWeb, :html

  embed_templates "artist_html/*"

  def table_artist(assigns) do
    ~H"""
            <div class="relative overflow-x-auto shadow-md sm:rounded-lg">
                <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
                    <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                        <tr class="bg-gray-200">
                            <th scope="col" class="px-6 py-3">
                                <div class="flex items-center">
                                   Artist
                                </div>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr class="hover:bg-gray-100 odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 cursor-pointer" :for={art <- @artists}>
                            <td style="scroll-margin-top: 69px" class="px-6 py-4" id={to_string(art.artist_name)}>
                                <.link class="hover:text-violet-200 hover:font-medium hover:underline" href={~p"/artist/#{art.artist_name}/#{art.id}"} title={art.artist_name}>
                                    <%= art.artist_name %>
                                </.link>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
    """
  end
end


artist_html/index.html.heex

<div class="flex flex-row flex-wrap overflow-x-auto">
    <div id="side_1" class="w-1/5 pl-5">
    </div>

    <div id="main" class="w-3/5 pl-5">
        <.table_artist artists={@artists} />
        <.paginate paginate={@artists} route="artists" />
    </div>

    <div id="side_2" class="w-1/5 pr-5">
    </div>
</div>

artist_html/show.html.heex

<div class="flex flex-row flex-wrap overflow-x-auto">
    <div id="side_1" class="w-1/5 pl-5">
    </div>

    <div id="main" class="w-3/5 pl-5">
      <h1 class="text-2xl">
        <div class="py-5">Artist Name: <%= @artist.artist_name %></div>
      </h1>
      <.table_novel novels={@novels} />
    </div>

    <div id="side_2" class="w-1/5 pr-5">
    </div>
</div>

Greetings Joshua.

Of course there is. Basically, Phoenix has two ways of handling views: Function Components and Template Files. Seems like you view samples of both ways.

If you check the A new view part of the Phoenix tutorial creating the Hello App, you will find how both ways work.

Best regards and happy hunting!!!

1 Like

As an example in this project:

There are a bunch of controllers but no view files in the same folder. So where is phoenix looking for them?

There’s two things to understand:

  1. Phoenix.Controller.render/2,3

This function uses a “view” to translate optional assigns into an encodable response value (see format encoders). It does that using two pieces of information – the view module name and the function name to call on it. Arity is fixed to always be 1, where assigns are passed.

The view module is either inferred from the controller module name, stripping any optional Controller suffix and replacing it with the uppercase format type (see accepts). So MyAppWeb.BlogController for a website becomes MyAppWeb.BlogHTML. This inferrence is part of use Phoenix.Controller and only happens if there’s no view module defined at the time the controller is called. You can define a view module using the put_view plug.

The function name is either the atom passed directly to render as a parameter or if defaults to the action name the controller was called with, so usually :index, :show, …

Combined this means it might call MyAppWeb.BlogHTML.index(assigns). That’s all what happens at runtime.

  1. Phoenix.Template.embed_templates/2 (Phoenix.Component.embed_templates/2 does the same)

This macro informs a view module of template files at a certain place. At compile time those template files are compiled and be transformed into functions on the module itself. At runtime those functions are indistunguishable from functions coded within the module instead of being the result of a template. The template files themselfes are no longer needed after compilation.


So while there is a bunch of convention within that setup there is no piece of it you couldn’t change. As shown above you can change thow the module name and function is gathered. Modules in elixir can be anywhere within lib/ by default (and elsewhere if you configure it). embed_templates is explicitly called with a path you can customize.

And for the curious: While the details are different in places today the approach has been quite the same even compared to the past where phoenix used phoenix_view with it’s render/3 api on view modules.

5 Likes