Skip to content

Latest commit

 

History

History
279 lines (196 loc) · 12.3 KB

E_views.md

File metadata and controls

279 lines (196 loc) · 12.3 KB

Phoenix views have two main jobs. First and foremost, they render templates (this includes layouts). The core function involved in rendering, render/3, is defined in Phoenix itself in the Phoenix.View module. Views also provide functions which take raw data and make it easier for templates to consume. If you are familiar with decorators or the facade pattern, this is similar.

Phoenix assumes a strong naming convention from controllers to views to the templates they render. The PageController requires a PageView to render templates in the web/templates/page directory.

If we want to, we can change the directory Phoenix considers to be the template root. Phoenix provides a view/0 function in the HelloPhoenix.Web module defined in web/web.ex. The first line of view/0 allows us to change our root directory by changing the value assigned to the :root key.

A newly generated Phoenix application has three view modules - ErrorView, LayoutView, and PageView - which are all in the, web/views directory.

Let's take a quick look at the LayoutView.

defmodule HelloPhoenix.LayoutView do
  use HelloPhoenix.Web, :view
end

That's simple enough. There's only one line, use HelloPhoenix.Web, :view. This line calls the view/0 function we just saw above. Besides allowing us to change our template root, view/0 exercises the __using__ macro in the Phoenix.View module. It also handles any module imports or aliases our application's view modules might need.

At the top of this guide, we mentioned that views are a place to put functions for use in our templates. Let's experiment with that a little bit.

Let's open up our application layout template, templates/layout/app.html.eex, and change this line,

<title>Hello Phoenix!</title>

to call a title/0 function, like this.

<title><%= title %></title>

Now let's add a title/0 function to our LayoutView.

defmodule HelloPhoenix.LayoutView do
  use HelloPhoenix.Web, :view

  def title do
    "Awesome New Title!"
  end
end

When we reload the Welcome to Phoenix page, we should see our new title.

The <%= and %> are from the Elixir EEx project. They enclose executable Elixir code within a template. The = tells EEx to print the result. If the = is not there, EEx will still execute the code, but there will be no output. In our example, we are calling the title/0 function from our LayoutView and printing the output into the title tag.

Note that we didn't need to fully qualify title/0 with HelloPhoenix.LayoutView because our LayoutView actually does the rendering.

When we use HelloPhoenix.Web, :view, we get other conveniences as well. Since the view/0 function imports HelloPhoenix.Router.Helpers, we don't have to fully qualify path helpers in templates. Let's see how that works by changing the template for our Welcome to Phoenix page.

Let's open up the templates/page/index.html.eex and locate this stanza.

<div class="jumbotron">
  <h2>Welcome to Phoenix!</h2>
  <p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
</div>

Then let's add a line with a link back to the same page. (The objective is to see how path helpers respond in a template, not to add any functionality.)

<div class="jumbotron">
  <h2>Welcome to Phoenix!</h2>
  <p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
  <p><a href="<%= page_path @conn, :index %>">Link back to this page</a></p>
</div>

Now we can reload the page and view source to see what we have.

<a href="/">Link back to this page</a>

Great, page_path/2 evaluated to / as we would expect, and we didn't need to qualify it with HelloPhoenix.View.

More About Views

You might be wondering how views are able to work so closely with templates.

The Phoenix.View module gains access to template behavior via the use Phoenix.Template line in its __using__/1 macro. Phoenix.Template provides many convenience methods for working with templates - finding them, extracting their names and paths, and much more.

Let's experiment a little with one of the generated views Phoenix provides us, web/views/page_view.ex. We'll add a message/0 function to it, like this.

defmodule HelloPhoenix.PageView do
  use HelloPhoenix.Web, :view

  def message do
    "Hello from the view!"
  end
end

Now let's create a new template to play around with, web/templates/page/test.html.eex.

This is the message: <%= message %>

This doesn't correspond to any action in our controller, but we'll exercise it in an iex session. At the root of our project, we can run iex -S mix, and then explicitly render our template.

iex(1)> Phoenix.View.render(HelloPhoenix.PageView, "test.html", %{})
  {:safe, [["" | "This is the message: "] | "Hello from the view!"]}

As we can see, we're calling render/3 with the individual view responsible for our test template, the name of our test template, and an empty map representing any data we might have wanted to pass in.

The return value is a tuple beginning with the atom :safe and the resultant string of the interpolated template.

"Safe" here means that Phoenix has escaped the contents of our rendered template. Phoenix defines its own Phoenix.HTML.Safe protocol with implementations for atoms, bitstrings, lists, integers, floats, and tuples to handle this escaping for us as our templates are rendered into strings.

What happens if we assign some key value pairs to the third argument of render/3? In order to find out, we need to change the template just a bit.

I came from assigns: <%= @message %>
This is the message: <%= message %>

Note the @ in the top line. Now if we change our function call, we see a different rendering after recompiling PageView module.

iex(2)> r HelloPhoenix.PageView
web/views/page_view.ex:1: warning: redefining module HelloPhoenix.PageView
{:reloaded, HelloPhoenix.PageView, [HelloPhoenix.PageView]}

iex(3)> Phoenix.View.render(HelloPhoenix.PageView, "test.html", message: "Assigns has an @.")
{:safe,
  [[[["" | "I came from assigns: "] | "Assigns has an @."] |
  "\nThis is the message: "] | "Hello from the view!"]}

Let's test out the HTML escaping, just for fun.

iex(4)> Phoenix.View.render(HelloPhoenix.PageView, "test.html", message: "<script>badThings();</script>")
{:safe,
  [[[["" | "I came from assigns: "] |
     "&lt;script&gt;badThings();&lt;/script&gt;"] |
    "\nThis is the message: "] | "Hello from the view!"]}

If we need only the rendered string, without the whole tuple, we can use the render_to_iodata/3.

iex(5)> Phoenix.View.render_to_iodata(HelloPhoenix.PageView, "test.html", message: "Assigns has an @.")
[[[["" | "I came from assigns: "] | "Assigns has an @."] |
  "\nThis is the message: "] | "Hello from the view!"]

A Word About Layouts

Layouts are just templates. They have a view, just like other templates. In a newly generated app, this is web/views/layout_view.ex. You may be wondering how the string resulting from a rendered view ends up inside a layout. That's a great question!

If we look at web/templates/layout/app.html.eex, just about in the middle of the <body>, we will see this.

<%= render @view_module, @view_template, assigns %>

This is where the view module and its template from the controller are rendered to a string and placed in the layout.

The ErrorView

Phoenix recently added a new view to every generated application, the ErrorView which lives in web/views/error_view.ex. The purpose of the ErrorView is to handle two of the most common errors - 404 not found and 500 internal error - in a general way, from one centralized location. Let's see what it looks like.

defmodule HelloPhoenix.ErrorView do
  use HelloPhoenix.Web, :view

  def render("404.html", _assigns) do
    "Page not found"
  end

  def render("500.html", _assigns) do
    "Server internal error"
  end

  # In case no render clause matches or no
  # template is found, let's render it as 500
  def template_not_found(_template, assigns) do
    render "500.html", assigns
  end
end

Before we dive into this, let's see what the rendered 404 not found message looks like in a browser. In the development environment, Phoenix will debug errors by default, showing us a very informative debugging page. What we want here, however, is to see what page the application would serve in production. In order to do that we need to set debug_errors: false in config/dev.exs.

use Mix.Config

config :hello_phoenix, HelloPhoenix.Endpoint,
  http: [port: 4000],
  debug_errors: false,
  code_reloader: true,
  . . .

After modifying our config file, we need to restart our server in order for this change to take effect. After restarting the server, let's go to http://localhost:4000/such/a/wrong/path for a running local application and see what we get.

Ok, that's not very exciting. We get the bare string "Page not found", displayed without any markup or styling.

Let's see if we can use what we already know about views to make this a more interesting error page.

The first question is, where does that error string come from? The answer is right in the ErrorView.

def render("404.html", _assigns) do
  "Page not found"
end

Great, so we have a render/2 function that takes a template and an assigns map, which we ignore. Where is this render/2 function being called from?

The answer is the render/5 function defined in the Phoenix.Endpoint.ErrorHandler module. The whole purpose of this module is to catch errors and render them with a view, in our case, the HelloPhoenix.ErrorView.

Now that we understand how we got here, let's make a better error page.

Phoenix generates an ErrorView for us, but it doesn't give us a web/templates/error directory. Let's create one now. Inside our new directory, let's add a template, not_found.html.eex and give it some markup - a mixture of our application layout and a new div with our message to the user.

<!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="">

    <title>Welcome to Phoenix!</title>
    <link rel="stylesheet" href="/css/app.css">
  </head>

  <body>
    <div class="container">
      <div class="header">
        <ul class="nav nav-pills pull-right">
          <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
        </ul>
        <span class="logo"></span>
      </div>

      <div class="jumbotron">
        <p>Sorry, the page you are looking for does not exist.</p>
      </div>

      <div class="footer">
        <p><a href="http://phoenixframework.org">phoenixframework.org</a></p>
      </div>

    </div> <!-- /container -->
    <script src="/js/app.js"></script>
  </body>
</html>

Now we can use the render/2 function we saw above when we were experimenting with rendering in the iex session.

Our render/2 function should look like this when we've modified it.

def render("404.html", _assigns) do
  render("not_found.html", %{})
end

When we go back to http://localhost:4000/such/a/wrong/path, we should see a much nicer error page.

It is worth noting that we did not render our not_found.html.eex template through our application layout, even though we want our error page to have the look and feel of the rest of our site. The main reason is that it's easy to run into edge case issues while handling errors globally.

If we want to minimize duplication between our application layout and our not_found.html.eex template, we can implement shared templates for our header and footer. Please see the Template Guide for more information.

Of course, we can do these same steps with the def render("500.html", _assigns) do clause in our ErrorView as well.

We can also use the assigns map passed into any render/2 clause in the ErrorView, instead of discarding it, in order to display more information in our templates.