How is it that `render/2` is always invoked with a map?

Hi everyone. I’m trying to get more familiar with how rendering works in Phoenix. I have one question that is bugging me though.

How is it that when I invoke render/2 in a template, with a keyword list as a second argument, the invocation always seems to happen with a map as the second argument?

For example, start up a brand new Phoenix app, and follows the steps to generate a new resource:

mix phoenix.gen.html User users name:string age:integer

You end up with a new.html.eex file in the template/user directory:

<h2>New user</h2>

<%= render "form.html", changeset: @changeset,
                        action: user_path(@conn, :create) %>

<%= link "Back", to: user_path(@conn, :index) %>

That render line should be invoking HelloWorld.UserView.render/2 with a string and a keyword list. However, if you over-ride the appropriate function in the view module, it seems like my keyword list got turned into a map:

defmodule HelloWorld.UserView do
  use HelloWorld.Web, :view

  def render("form.html", assigns) do
    "is assigns a list?: " <> "#{is_list(assigns)}"
  end
end

If you refresh the new page, you’ll see that assigns is not a list. (You can make the appropriate changes to see that it is a map). There’s something going on that is messing up my mental model that templates just get turned into different clauses of render/2 functions on the view module.

Phoenix.View.render/3 does this as a convenience because writing a keyword list is nicer and in your render functions, matching on Maps is easier :slight_smile:

You can pass in both Maps and Keyword lists and Phoenix.View.render/3 will ensure that you always end up with a Map in your custom render functions.

You can easily see this in how the function is implemented:

When assigns is already a Map, it remains the same, otherwise it gets transformed into one.

1 Like

@wmnnd - Hi. Thank you. This is indeed helpful but I am still left with a gap in my mental model.

My understanding is that I am invoking HelloWorld.UserView.render/2, not Phoenix.View.render/3.

I can’t point to the sources at the moment, but more than one tutorial from individuals that I trust have mentioned something along the lines of templates just being compiled to functions on the view module. So, my view module looks something like this:

defmodule HelloWorld.UserView do
  use HelloWorld.Web, :view

  def render("form.html", assigns) do
    "this should really be the string returned by the form"
  end

  def render("new.html", assign) do
    "<p>" <> render("form.html", [just: :data]) <> "</p>"
  end
end

Maybe a more concrete question is how is render("form.html", [just: :data]) invoking Phoenix.View.render/3, if that is indeed what’s going on.

Thanks for the help!

hi @StevenXL

You are indeed invoking HelloWorld.UserView.render/2, a functions that gets injected into your view module by use Phoenix.Template. This function inturn calls Phoenix.View.render/2.

Your view uses Phoenix.View which uses Phoenix.Template: https://github.com/phoenixframework/phoenix/blob/v1.2.3/lib/phoenix/view.ex#L133

and in template.

Hope this helps.

Hi @shankardevy! Thank you very much. That’s the part that was missing. Thanks @wmnnd as well. I simply wasn’t following what you were saying. (Sometimes I think in too linearly a fashion).

When you use HelloWorld.Web, :view, some functions are defined by the __using__ macro of Phoenix.Template:

There you can see that one of the clauses of render/2 makes sure the assigns are a map and otherwise uses Enum.into(assigns, %{}) on them.

Since you call the use statement before you define your own render/2 functions (and hence the render/2 functions from Phoenix.Template are defined before your own and have precedence), this clause always matches when there is a non-Map assigns.