Rendering info from controller in template file

Hey all, sorry if this is a dumb question but I am new to elixir and unsure of how to do something pretty simple. I have this function in my page_controller.ex file that returns a user’s name:

  def get_user_name(conn) do
    first_name = Pow.Plug.current_user(conn).first_name
    last_name = Pow.Plug.current_user(conn).last_name
    user_full_name = first_name <> last_name
    IO.inspect(user_full_name, label: "user_full_name")
    render(conn, user_full_name: user_full_name)
  end

And I am trying to render that user’s name in my app.html.leex file. How can I get that value in my template file? I’m not sure if I can import other files into template files, so this doesn’t work:

<span><%= get_user_name(@conn) %></span>

because get_user_name is undefined in the template file…not sure how you go about doing this…

Thanks so much for any help!!

In your html you can call any of the assigns you passed using @assign like such:

<%= @user_full_name %>
1 Like

thanks for the help!! when I do that though, I get this error: assign @user_full_name not available in eex template. Am I not setting the assign properly?

Try using render/3 and passing the template name, like:

render(conn, "index.html", user_full_name: user_full_name)

assuming the HTML template is called index.html.eex

1 Like

The name of the template file is app.html.leex, but when I try

render(conn, "app.html", user_full_name: user_full_name)

it still gives me the same error: assign @user_full_name not available in eex template.

Is it in the folder lib/app_name_web/templates/page? I think you might have the HTML in the wrong place.

1 Like

ah, that’s true, I have it in the folder lib/app_name_web/templates/layout. Thank you so much!!!

You don’t need to move the file, just copy and paste your code from layout/app.html.eex to page/index.html.eex. The former is specifically for the layout.

1 Like

Gotcha. I need to keep this code in layout/app.html.eex though, bcs it is rendering the top bar component of the layout for the whole app, so I tried making a layout_controller.ex file in the controller folder and putting the get_user_name function in there, but it’s still giving me that same assign @user_full_name not available in eex template. error.

Just to have it all in one place, here’s what I have:
Here’s the layout_controller.ex file

defmodule ProjectNameWeb.LayoutController do
  use ProjectNameWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html")
  end

  @spec get_user_name(Plug.Conn.t(), any) :: Plug.Conn.t()
  def get_user_name(conn, _) do
    first_name = Pow.Plug.current_user(conn).first_name
    last_name = Pow.Plug.current_user(conn).last_name
    user_full_name = first_name <> last_name
    render(conn, "app.html", user_full_name: user_full_name)
  end
end

And then in app.html.leex, I’m trying to just render:

   <%= @user_full_name %>

If you really want to keep it in the layout, you can. The page index.html.eex is actually inside the layout app.html.eex as @inner_content. So the assign @user_full_name is available in both places (though I would recommend not putting anything in the layout unless it’s necessary). But you should still be using

render(conn, "index.html", user_full_name: user_full_name)

in your controller.

1 Like

Here I can finally see the problem: You define get_user_name/2, but you don’t actually use it in your index/2. You probably want to do something like this:

def index(conn, _params) do
  user_full_name = get_user_name(conn)
  render(conn, "index.html", user_full_name: user_full_name)
end

def get_user_name(conn) do
  user = Pow.Plug.current_user(conn)
  "#{user.first_name} #{user.last_name}"
end
2 Likes

Thanks!! That looks a lot cleaner and makes sense. For some reason it’s still not working though…still getting that assign @user_full_name not available in eex template. error.

Hmmm…I’m really not sure why it’s not working, I’m wondering if it has something to do with what’s in my template file? Here’s what the app.html.leex file looks like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <%= live_title_tag assigns[:page_title] || gettext("ProjectName") %>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>">
    <%= csrf_meta_tag() %>
  </head>

  <body>
    <div class="rev-TopBar rev-TopBar--left">
      <nav role="navigation">
        <div class="rev-Brand">
          <div>IRR</div>
          <%= @user_full_name %>
        </div>
        <select class="rev-Select" name="select">
          <option value="">Name</option>
          <option value="page">Account</option>
          <option value="all">Settings</option>
          <option value="all">Logout</option>
        </select>
      </nav>
    </div>
      <main role="main" class="rev-Content">
        <%= unless is_nil(get_flash(@conn, :info)) do %>
          <div class="Flash Flash--success">
            <span><%= get_flash(@conn, :info) %></span>
            <%= link to: "#", class: "rev-Close" do %>
              <i data-feather="x"></i>
            <% end %>
          </div>
        <% end %>
        <%= unless is_nil(get_flash(@conn, :error)) do %>
          <div class="Flash Flash--alert">
            <span><%= get_flash(@conn, :error) %></span>
            <%= link to: "#", class: "rev-Close" do %>
              <i data-feather="x"></i>
            <% end %>
          </div>
        <% end %>
        <main role="main">
          <%= @inner_content %>
        </main>
    <script src="<%= Routes.static_path(@conn, "/js/app.js") %>" phx-track-static></script>
  </body>
</html>

Ok, so I’m assuming that in your router.ex file you have a route that looks like this:

live "/", PageController, :index

This means that when a user requests the route "/", it calls the function index/2. Any other functions in the module, such as get_user_name will not be called. So you need to think of get_user_name as just a “helper” function, not the action which will render your page. index/2 will render your page, but all get_user_name should do is fetch the current_user and build the user_full_name String. Then it passes that string back to index/2 where it is rendered. See the code in my previous post again for example.

Now, if your router.ex instead says

live "/", PageController, :get_user_name

then this is a different scenario, where you actually DO want to use get_user_name/2 as the rendering action, and ignore index/2. If this is the case, then it should work fine, maybe just change “app.html” to “index.html”. But I don’t think this is the right way.

1 Like

In such a simple case like this, I would probably even do away with the helper function altogether.

def index(conn, _params) do
  user = Pow.Plug.current_user(conn)
  render(conn, "index.html", user_full_name: "#{user.first_name} #{user.last_name}")
end

Again, this is under the assumption that your router is calling the :index action.

1 Like

I can’t thank you enough for this explanation!! I truly appreciate it.

So, I see the issue here, I didn’t have a route for LayoutController , in router.ex, just this route:

  scope "/", ProjectNameWeb do
    pipe_through [:browser]
    get "/", PageController, :index
  end

And that doesn’t work since I changed the function to be in LayoutController .
However, when I try to add a route for LayoutController , I get the error this clause cannot match because a previous clause at line 71 always matches (referring to the PageController route). Also, when I use live instead of get, I get this error: (ArgumentError) could not infer :as option because a live action was given and the LiveView does not have a "Live" suffix. Please pass :as explicitly or make sure your LiveView is named like "FooLive" or "FooLive.Index"

Can I not have 2 controllers for the "/" route?

I think maybe I should just move the code to the PageController and template file in the page folder, everything works great when I do that.

Quick question: would it be preferable to build the top bar component that will be on every page of your app in the page template file or the template one? I’m still figuring out best practices and how to set up everything correctly

Oh! I meant to use get, not live in the router. I was in a bit of a rush when I posted, my apologies. live is just for routes which spawn a LiveView process.

As for two controllers for the same route, how would the app be able to tell which one you wanted? You have to have some difference between the routes, otherwise you will get the error which you posted.

And for the design, use the Layout if you want all the pages to have the same menu bar. Personally I use LiveView for all my projects now and rely heavily on the Live Components feature for nesting re-usable templates.

2 Likes

Thank you so much!! That makes sense. One last question: If I want to put it in the Layout since every page will use it, and the "/" route is used by the PageController, how would that work? I want the top bar component I’m creating to work on the home page as well as all pages, so it seems like I would have to put it in the PageController?

The layout will appear on EVERY page by default. That’s why I was warning you earlier about putting specific page content in there. If you have a page where you DON’T want to use the layout, you can disable it by adding put_layout/2 to the controller action as such:

def index(conn, _params) do
  conn
  |> put_layout(false)
  |> render("index.html", assign_1: "foo", assign_2: "bar")
end

and if you end up making an alternative layout (default is app.html.eex, let’s say you make another one called admin.html.eex for only admin pages), you can also use the same put_layout/2 function:

def index(conn, _params) do
  conn
  |> put_layout("admin.html")
  |> render("index.html", assign_1: "foo", assign_2: "bar")
end

For more info see Controllers — Phoenix v1.5.9

1 Like

Ah I see. Once again, thank you so much for the help and great explanation!!!

However, I’m still unsure of how to proceed here, since I want to keep this code in LayoutController bcs it’s for the topbar that will be on all pages, I need to make a route for LayoutController in router.ex, but PageController already uses the default "/" route:

scope "/", ProjectNameWeb do
    pipe_through [:browser]
    get "/", PageController, :index
  end

I wouldn’t want to create a specific route just for the topbar component right? So I’m unsure of how to keep this topbar code (that requires the user_full_name) in the LayoutController .

You don’t need to do anything with LayoutController at all. It will always load the layout unless you have disabled it in the PageController as shown in my previous post. Your router looks fine right now. Have you tried running the server locally and seeing if everything works ok now?

1 Like