defmodule Phoenix.View do @moduledoc """ A module for generating `render/2` functions from templates on disk. With design patterns introduced by `Phoenix.LiveView`, this module has fallen out of fashion in favor of `Phoenix.Component`, even in non LiveView applications. See the "Replaced by `Phoenix.Component`" section below. ## Examples In Phoenix v1.6 and earlier, new Phoenix apps defined a blueprint for views at `lib/your_app_web.ex`. It generally looked like this: defmodule YourAppWeb do # ... def view do quote do use Phoenix.View, root: "lib/your_app_web/templates", namespace: YourAppWeb # Import convenience functions from controllers import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML import YourAppWeb.ErrorHelpers import YourAppWeb.Gettext end end # ... end Then you could use the definition above to define any view in your application: defmodule YourAppWeb.UserView do use YourAppWeb, :view end Because we defined the template root to be "lib/your_app_web/templates", `Phoenix.View` will automatically load all templates at "your_app_web/templates/user" and include them in the `YourApp.UserView`. For example, imagine we have the template: # your_app_web/templates/user/index.html.heex Hello <%= @name %> The `.heex` extension maps to a template engine which tells Phoenix how to compile the code in the file into Elixir source code. After it is compiled, the template can be rendered as: Phoenix.View.render_to_string(YourApp.UserView, "index.html", name: "John Doe") #=> "Hello John Doe" ## Rendering and formats `Phoenix.View` renders templates. A template has a name, which also contains a format. For example, in the previous section we have rendered the "index.html" template: Phoenix.View.render_to_string(YourApp.UserView, "index.html", name: "John Doe") #=> "Hello John Doe" While we got a string at the end, that's not actually what our templates render. Let's take a deeper look: Phoenix.View.render(YourApp.UserView, "index.html", name: "John Doe") #=> ... This inner representation allows us to separate how templates render and how they are encoded. For example, if you want to render JSON data, we could do so by adding a "show.json" entry to `render/2` in our view: defmodule YourAppWeb.UserView do use YourAppWeb, :view def render("show.json", %{user: user}) do %{name: user.name, address: user.address} end end Notice that in order to render JSON data, we don't need to explicitly return a JSON string! Instead, we just return data that is encodable to JSON. Now, when we call: Phoenix.View.render_to_string(YourApp.UserView, "user.json", user: %User{...}) Because the template has the `.json` extension, Phoenix knows how to encode the map returned for the "user.json" template into an actual JSON payload to be sent over the wire. Phoenix ships with some template engines and format encoders, which can be further configured in the Phoenix application. You can read more about format encoders in `Phoenix.Template` documentation. ## Replaced by `Phoenix.Component` In `Phoenix.LiveView`, `Phoenix.View` was replaced by `Phoenix.Component`. With Phoenix v1.7+ we can also use `Phoenix.Component` to render traditional templates as functional components, using the `embed_templates` function. For example, in Phoenix v1.7+, the `YourAppWeb.UserView` above would be written as: defmodule YourAppWeb.UserHTML do use YourAppWeb, :html embed_templates "users/*" end The benefit of `Phoenix.Component` is that it unifies the rendering of traditional request/response life cycles with the composable component model provided by LiveView. The table below summarizes how the defaults changed from Phoenix v1.6 to v1.7: | Feature | Phoenix v1.6 | Phoenix v1.7 | | -------------------------------- | --------------------------------------- | --------------------------------------------- | | `MyController.action/2` renders | `MyView.render("action.html", assigns)` | `MyHTML.action(assigns)` | | Define views at | `lib/my_app/views/my_view.ex` | `lib/my_app/controllers/my_html.ex` | | At the top of your views | `use MyAppWeb, :view` | `use MyAppWeb, :html` | | Default template language | `EEx` (`.eex` extension) | `HEEx` (`.heex` extension) | | To embed templates from disk | `use Phoenix.View` | `use Phoenix.Component` (+ `embed_templates`) | | HTML helpers (forms, links, etc) | `use Phoenix.HTML` | `use Phoenix.Component` | However, note Phoenix v1.7 is backwards compatible with v1.6 if you want to keep with the old style. The functionality in this module will be maintained in the long term though for those who cannot or prefer not to migrate. ### Migrating to Phoenix.Component Migrating your current views to components be done in a few steps. You should also be able to migrate one view at a time. > It may be helpful to generate a new project using Phoenix v1.7+ to compare > code samples during this process. The first step is to define `def html` in your `lib/my_app_web.ex` module. This function is similar to `def view`, but it replaces `use Phoenix.View` by `use Phoenix.Component` (requires LiveView 0.18.3 or later). We also recomend to add `import Phoenix.View` inside `def html` while migrating. Then, for each view, you must follow these steps (we will assume the current view is called `MyAppWeb.MyView`): 1. Replace `render_existing/3` calls by `function_exported?/3` checks, according to the `render_existing` documentation. 2. Replace `use MyApp, :view` by `use MyApp, :html` and invoke `embed_templates "../templates/my/*"`. Alternatively, you can move both the HTML file and its templates to the `controllers` directory, to align with Phoenix v1.7 conventions. 3. Your templates may now break if they are calling `render/2`. You can address this by replacing `render/2` with a function component. For instance, `render("_form.html", changeset: @changeset, user: @user)` must now be called as `<.form changeset={@changeset} user={@user} />`. If passing all assigns, `render("_form.html", assigns)` becomes `<%= _form(assigns) %>` 4. Your templates may now break if they are calling `render_layout/4`. You can address this by converting the layout into a function component that receives its contents as a slot. See `render_layout/4` docs Now you are using components! Once you convert all views, you should be able to remove `Phoenix.View` as a dependency from your project. Remove `def view` and also remove the `import Phoenix.View` from `def html` in your `lib/my_app_web.ex` module. When doing so, compilation may fail if you are using certain functions: * Replace `render/3` with a function component. For instance, `render(OtherView, "_form.html", changeset: @changeset, user: @user)` can now be called as ``. If passing all assigns, `render(OtherView, "_form.html", assigns)` becomes `<%= OtherView._form(assigns) %>`. * If you are using `Phoenix.View` for APIs, you can remove `Phoenix.View` altogether. Instead of `def render("index.html", assigns)`, use `def users(assigns)`. Instead of `def render("show.html", assigns)`, do `def user(assigns)`. Instead `render_one`/`render_many`, call the `users/1` and `user/1` functions directly. """ alias Phoenix.Template @doc """ When used, defines the current module as a main view module. ## Options * `:root` - the template root to find templates * `:path` - the optional path to search for templates within the `:root`. Defaults to the underscored view module name. A blank string may be provided to use the `:root` path directly as the template lookup path * `:namespace` - the namespace to consider when calculating view paths * `:pattern` - the wildcard pattern to apply to the root when finding templates. Default `"*"` The `:root` option is required while the `:namespace` defaults to the first nesting in the module name. For instance, both `MyApp.UserView` and `MyApp.Admin.UserView` have namespace `MyApp`. The `:namespace` and `:path` options are used to calculate template lookup paths. For example, if you are in `MyApp.UserView` and the namespace is `MyApp`, templates are expected at `Path.join(root, "user")`. On the other hand, if the view is `MyApp.Admin.UserView`, the path will be `Path.join(root, "admin/user")` and so on. For explicit root path locations, the `:path` option can be provided instead. The `:root` and `:path` are joined to form the final lookup path. A blank string may be provided to use the `:root` path directly as the template lookup path. Setting the namespace to `MyApp.Admin` in the second example will force the template to also be looked up at `Path.join(root, "user")`. """ defmacro __using__(opts) do opts = if Macro.quoted_literal?(opts) do Macro.prewalk(opts, &expand_alias(&1, __CALLER__)) else opts end quote do # Register setup first, because its before_compile # needs to run before Phoenix.Template callback. Phoenix.View.__setup__(__MODULE__, unquote(opts)) use Phoenix.Template import Phoenix.View @doc """ Callback invoked when no template is found. By default it raises but can be customized to render a particular template. """ @spec template_not_found(binary, map) :: no_return def template_not_found(template, assigns) do Phoenix.View.__not_found__!(__MODULE__, template, assigns) end defoverridable template_not_found: 2 @doc """ Renders the given template locally. """ def render(template, assigns \\ %{}) def render(module, template) when is_atom(module) do Phoenix.View.render(module, template, %{}) end def render(template, _assigns) when not is_binary(template) do raise ArgumentError, "render/2 expects template to be a string, got: #{inspect(template)}" end def render(template, assigns) when not is_map(assigns) do render(template, Enum.into(assigns, %{})) end @doc "The resource name, as an atom, for this view" def __resource__, do: @view_resource end end defp expand_alias({:__aliases__, _, _} = alias, env), do: Macro.expand(alias, %{env | function: {:init, 1}}) defp expand_alias(other, _env), do: other @doc ~S''' Renders the given layout passing the given `do/end` block as `@inner_content`. This can be useful to implement nested layouts. For example, imagine you have an application layout like this: # layout/app.html.heex Title <%= @inner_content %> This layout is used by many parts of your application. However, there is a subsection of your application that wants to also add a sidebar. Let's call it "blog.html". You can build on top of the existing layout in two steps. First, define the blog layout: # layout/blog.html.heex <%= render_layout LayoutView, "app.html", assigns do %> <%= @inner_content %> <% end %> And now you can simply use it from your controller: plug :put_layout, "blog.html" ## Alternatives `render_layout/4` is discouraged in favor of components. If you need to share functionality, you can create components with bits of functionality you want to reuse. For example, the code above could be rewritten with a layout component: def layout(assigns) do ~H"""
<%= render_slot(@sidebar) %> <%= render_slot(@inner_block) %>
""" end Which can be used as: <.layout> Main content Or: <.layout> <:sidebar>Additional sidebar content Main content The advantage of using components is that you can handle all of the sidebar markup inside the parent layout component, instead of spreading it across multiple files. ''' def render_layout(module, template, assigns, do: block) do assigns = assigns |> Map.new() |> Map.put(:inner_content, block) module.render(template, assigns) end @doc """ Renders a template. It expects the view module, the template as a string, and a set of assigns. Notice that this function returns the inner representation of a template. If you want the encoded template as a result, use `render_to_iodata/3` instead. ## Examples Phoenix.View.render(YourApp.UserView, "index.html", name: "John Doe") #=> {:safe, "Hello John Doe"} ## Assigns Assigns are meant to be user data that will be available in templates. However, there are keys under assigns that are specially handled by Phoenix, they are: * `:layout` - tells Phoenix to wrap the rendered result in the given layout. See next section ## Layouts Templates can be rendered within other templates using the `:layout` option. `:layout` accepts a tuple of the form `{LayoutModule, "template.extension"}`. To template that goes inside the layout will be placed in the `@inner_content` assign: <%= @inner_content %> """ def render(module, template, assigns) def render(module, template, assigns) do assigns |> Map.new() |> Map.pop(:layout, false) |> render_within(module, template) end defp render_within({false, assigns}, module, template) do module.render(template, assigns) end defp render_within({{layout_mod, layout_tpl}, assigns}, module, template) when is_atom(layout_mod) and is_binary(layout_tpl) do content = module.render(template, assigns) assigns = Map.put(assigns, :inner_content, content) layout_mod.render(layout_tpl, assigns) end defp render_within({layout, _assigns}, _module, _template) do raise ArgumentError, """ invalid value for reserved key :layout in View.render/3 assigns :layout accepts a tuple of the form {LayoutModule, "template.extension"} got: #{inspect(layout)} """ end @doc ~S''' Renders a template only if it exists. > Note: Using this functionality has been discouraged in > recent Phoenix versions, see the "Alternatives" section > below. This function works the same as `render/3`, but returns `nil` instead of raising. This is often used with `Phoenix.Controller.view_module/1` and `Phoenix.Controller.view_template/1`, which must be imported into your views. See the "Examples" section below. ## Alternatives This function is discouraged. If you need to render something conditionally, the simplest way is to check for an optional function in your views. Consider the case where the application has a sidebar in its layout and it wants certain views to render additional buttons in the sidebar. Inside your sidebar, you could do: If you are using Phoenix.LiveView, you could do similar by accessing the view under `@socket`: Then, in your view or live view, you do: def sidebar_additions(assigns) do ~H\""" ...my additional buttons... \""" ## Using render_existing Consider the case where the application wants to allow entries to be added to a sidebar. This feature could be achieved with: <%= render_existing view_module(@conn), "sidebar_additions.html", assigns %> Then the module under `view_module(@conn)` can decide to provide scripts with either a precompiled template, or by implementing the function directly, ie: def render("sidebar_additions.html", _assigns) do ~H""" ...my additional buttons... """ end To use a precompiled template, create a `scripts.html.eex` file in the `templates` directory for the corresponding view you want it to render for. For example, for the `UserView`, create the `scripts.html.eex` file at `your_app_web/templates/user/`. ''' def render_existing(module, template, assigns \\ []) do assigns = assigns |> Map.new() |> Map.put(:__phx_render_existing__, {module, template}) render(module, template, assigns) end @doc """ Renders a collection. It receives a collection as an enumerable of structs and returns the rendered collection in a list. This is typically used to render a collection as structured data. For example, to render a list of users to json: render_many(users, UserView, "show.json") which is roughly equivalent to: Enum.map(users, fn user -> render(UserView, "show.json", user: user) end) The underlying user is passed to the view and template as `:user`, which is inferred from the view name. The name of the key in assigns can be customized with the `:as` option: render_many(users, UserView, "show.json", as: :data) is roughly equivalent to: Enum.map(users, fn user -> render(UserView, "show.json", data: user) end) """ def render_many(collection, view, template, assigns \\ %{}) do assigns = Map.new(assigns) resource_name = get_resource_name(assigns, view) Enum.map(collection, fn resource -> render(view, template, Map.put(assigns, resource_name, resource)) end) end @doc """ Renders a single item if not nil. The following: render_one(user, UserView, "show.json") is roughly equivalent to: if user != nil do render(UserView, "show.json", user: user) end The underlying user is passed to the view and template as `:user`, which is inflected from the view name. The name of the key in assigns can be customized with the `:as` option: render_one(user, UserView, "show.json", as: :data) is roughly equivalent to: if user != nil do render(UserView, "show.json", data: user) end """ def render_one(resource, view, template, assigns \\ %{}) def render_one(nil, _view, _template, _assigns), do: nil def render_one(resource, view, template, assigns) do assigns = Map.new(assigns) render(view, template, assign_resource(assigns, view, resource)) end @compile {:inline, [get_resource_name: 2]} defp get_resource_name(assigns, view) do case assigns do %{as: as} -> as _ -> view.__resource__ end end defp assign_resource(assigns, view, resource) do Map.put(assigns, get_resource_name(assigns, view), resource) end @doc """ Renders the template and returns iodata. """ def render_to_iodata(module, template, assign) do render(module, template, assign) |> encode(template) end @doc """ Renders the template and returns a string. """ def render_to_string(module, template, assign) do render_to_iodata(module, template, assign) |> IO.iodata_to_binary() end defp encode(content, template) do "." <> format = Path.extname(template) if encoder = Template.format_encoder(format) do encoder.encode_to_iodata!(content) else content end end @doc """ Converts the template path into the template name. ## Examples iex> Phoenix.View.template_path_to_name( ...> "lib/templates/admin/users/show.html.eex", ...> "lib/templates" ...> ) "admin/users/show.html" """ @spec template_path_to_name(Path.t(), Path.t()) :: Path.t() def template_path_to_name(path, root) do path |> Path.rootname() |> Path.relative_to(root) end @doc """ Converts a module, without the suffix, to a template root. ## Examples iex> Phoenix.View.module_to_template_root(MyApp.UserView, MyApp, "View") "user" iex> Phoenix.View.module_to_template_root(MyApp.Admin.User, MyApp, "View") "admin/user" iex> Phoenix.View.module_to_template_root(MyApp.Admin.User, MyApp.Admin, "View") "user" iex> Phoenix.View.module_to_template_root(MyApp.View, MyApp, "View") "" iex> Phoenix.View.module_to_template_root(MyApp.View, MyApp.View, "View") "" """ def module_to_template_root(module, base, suffix) do module |> unsuffix(suffix) |> Module.split() |> Enum.drop(length(Module.split(base))) |> Enum.map(&Macro.underscore/1) |> join_paths() end defp join_paths([]), do: "" defp join_paths(paths), do: Path.join(paths) defp unsuffix(value, suffix) do string = to_string(value) suffix_size = byte_size(suffix) prefix_size = byte_size(string) - suffix_size case string do <> -> prefix _ -> string end end ## Exceptions # Defined on Phoenix.Template for backwards compatibility defmodule Elixir.Phoenix.Template.UndefinedError do @moduledoc """ Exception raised when a template cannot be found. """ defexception [:available, :template, :module, :root, :assigns, :pattern] def message(exception) do "Could not render #{inspect(exception.template)} for #{inspect(exception.module)}, " <> "please define a matching clause for render/2 or define a template at " <> "#{inspect(Path.join(Path.relative_to_cwd(exception.root), exception.pattern))}. " <> available_templates(exception.available) <> "\nAssigns:\n\n" <> inspect(exception.assigns) <> "\n\nAssigned keys: #{inspect(Map.keys(exception.assigns))}\n" end defp available_templates([]), do: "No templates were compiled for this module." defp available_templates(available) do "The following templates were compiled:\n\n" <> Enum.map_join(available, "\n", &"* #{&1}") <> "\n" end end @private_assigns [:__phx_template_not_found__] @doc false def __not_found__!(view_module, template, assigns) do {root, pattern, names} = view_module.__templates__() raise Template.UndefinedError, assigns: Map.drop(assigns, @private_assigns), available: names, template: template, root: root, pattern: pattern, module: view_module end ## On use callbacks @doc false def __setup__(module, opts) do if Module.get_attribute(module, :view_resource) do raise ArgumentError, "use Phoenix.View is being called twice in the module #{module}. " <> "Make sure to call it only once per module" else view_resource = String.to_atom(resource_name(module, "View")) Module.put_attribute(module, :view_resource, view_resource) end Module.put_attribute(module, :before_compile, Phoenix.View) root = opts[:root] || raise(ArgumentError, "expected :root to be given as an option") path = opts[:path] namespace = if given = opts[:namespace] do given else module |> Module.split() |> Enum.take(1) |> Module.concat() end root = Path.join(root, path || module_to_template_root(module, namespace, "View")) Module.put_attribute(module, :phoenix_root, Path.relative_to_cwd(root)) Module.put_attribute(module, :phoenix_pattern, Keyword.get(opts, :pattern, "*")) engines = Enum.into(Keyword.get(opts, :template_engines, []), Phoenix.Template.engines()) Module.put_attribute(module, :phoenix_engines, engines) end @doc false defmacro __before_compile__(_env) do quote generated: true, unquote: false do require Phoenix.Template names = for {name, _path} <- Phoenix.Template.compile_all( &Phoenix.View.template_path_to_name(&1, @phoenix_root), @phoenix_root, @phoenix_pattern, @phoenix_engines ) do defp render_template(unquote(name), assigns) do unquote(String.to_atom(name))(assigns) end name end # Catch-all clause for template rendering. defp render_template(template, %{__phx_render_existing__: {__MODULE__, template}}) do nil end defp render_template(template, %{__phx_template_not_found__: __MODULE__} = assigns) do Phoenix.View.__not_found__!(__MODULE__, template, assigns) end defp render_template(template, assigns) do template_not_found(template, Map.put(assigns, :__phx_template_not_found__, __MODULE__)) end # Catch-all clause for rendering. def render(template, assigns) do render_template(template, assigns) end @doc false def __templates__ do {@phoenix_root, @phoenix_pattern, unquote(names)} end end end defp resource_name(alias, suffix) do alias |> to_string() |> Module.split() |> List.last() |> unsuffix(suffix) |> Macro.underscore() end end