Discover Elixir & Phoenix

Back to All Courses

Lesson 6

Templates, views and controllers

In this lesson, we'll go through the basic steps that Phoenix follows in order to present your HTML. Let's start by making sure that we understand how our current dummy HTML page (the one that says "Welcome to Phoenix") is being generated.

The layout template

By default, all pages created in Phoenix will be rendered in a pre-defined App layout template. You can find this template in lib/messengyr_web/templates/layout/app.html.eex:

The thing to remember about this template is that it should be kept very simple. Think of it as a very thin wrapper that has mostly static content. Good things to put here are your app's headerfooter and content container.

In this pre-generated Pheonix layout, there's a header, some alert-tags for error messages, and a main-tag that the rest of the page is rendered inside.

Let's try to modify this template a bit! We remove the header and type "Hello World" in an h1-tag. Keep the rest as it is!

<!-- lib/messengyr_web/templates/layout/app.html.eex -->

<body>
  <div class="container">

    <!-- Remove the whole <header>-part -->
    <!-- And add this instead: -->
    <h1>Hello world!</h1>

    <!-- Keep the rest as it is: -->
    <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
    <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

    <!-- ... -->

  </div>
</body>

If you go back to the browser, you should see your new text. Phoenix comes with live reloading bundled in, so any changes that you make to your files are visible almost instantly!

As you might have noticed, our layout template also contains this peculiar line of code:

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

This is where all the other view templates are rendered. To understand how these are rendered, we first need to understand the router.

The router

The router is essentially what connects a URL to a certain page. You can find it at lib/messengyr_web/router.ex

The file might look daunting at first, but don't worry, it's actually quite simple!

The first thing you see when you open the router-file is this weird line: use MessengyrWeb, :router.

use is an Elixir macro used to bring in **external modules into the current context **(you might know this as "mixins" in other programming languages), so that we get access to functions like plug and pipeline. You can see exactly what this line imports by opening the file lib/messengyr_web.ex and checking the router-function.

We'll go through the pipeline and plug functions in an upcoming chapter, but for now you should concentrate on the scope-function further down. You'll notice that the first parameter of this function is a URL path, represented as a string ("/").

scope "/", MessengyrWeb do
 pipe_through :browser

The pipe_through-part tells Phoenix that all paths that start with "/" (which is all URLs), must go through a series of functions from a specific pipeline before rendering something. In this case, the :browser-pipeline (defined at the top of the file) makes sure that what we render is valid HTML, and also adds some **extra security features **(like CSRF tokens).

Then we have get "/", which specifies exactly which path we want to hit (in this case, still just "/"), and what action to perform. This get-function creates what's called a route, and in this particular route, we want to perform the :index-action, defined in the PageController.

get "/", PageController, :index

If you now go to a URL that is not defined in any route (for example /test), you'll see this error message:

Let's create our own route to handle this URL, so that we can understand the flow of an HTTP request in Phoenix!

Routes and Controllers

Since the route /test isn't defined, we'll create it!

# lib/messengyr_web/router.ex

scope "/", Messengyr.Web do
  pipe_through :browser

  get "/", PageController, :index
  get "/test", PageController, :say_hello # Add this line!
end

Now, if we go to http://localhost:4000/test, our route will invoke the say_hello/2-action on the PageController. However, since that action hasn't been defined yet, you'll see this error message:

Controllers are responsible for gathering data, usually from a database, and then forwarding it to a view. For our say_hello/2-function though, we'll keep things simple, so we won't need any database.

Let's open the file messengyr_web/controllers/page_controller.ex. You'll see that the index/2-function is already defined, and that it's calling a render-function. render always returns one of our HTML-templates, but we actually don't have to return HTML -- we can return anything!

Just for fun, let's try to return "Hello!" in plain text in our /test-route using the text/2-function:

# lib/messengyr_web/controllers/page_controller.ex

#...

  def say_hello(conn, _params) do
    text conn, "Hello!"
  end
end

It works!

The only requirement for a controller action is that you must always return the conn struct, no matter what type of content you choose to render.

Views and templates

Let's take a closer look at the render-function in the controller this time. This is a function provided by Phoenix that invokes a view before rendering a certain template. The name of the view must match the controller name, so when the index-action in the *Page-*Controller calls render, it's going to invoke the *Page-*View. Let's open the PageView module and take a look!

# lib/messengyr_web/views/page_view.ex

defmodule MessengyrWeb.PageView do
  use MessengyrWeb, :view
end

As you can see, there's nothing happening here yet. We just have a use macro that imports some functions for us.

The second parameter of the render-function ("index.html") defines the name of the template. Here, Phoenix again has some convenient naming conventions. Since we're in the PageController, it's going to look for the template-file in the folder web/templates/page. If we had invoked render from a controller called HelloController, it would look for the file in web/templates/hello instead.

Open the file messengyr_web/templates/page/index.html.eex and you'll see the rest of the dummy template that's generated when we visit http://localhost:4000:

Let's remove all this content and just replace it with "Hello again!", wrapped in a p-tag:

<!-- lib/messengyr_web/templates/page/index.html.eex -->

<p>Hello again!</p>

We now have the most minimalistic website in the world!

The purpose of views is to take the data given to it by the controller, and transform it into something that's easily consumed by the template. As an example, we can define a my_name/0-function in our PageView that simply returns our name:

# lib/messengyr_web/views/page_view.ex

defmodule MessengyrWeb.PageView do
  use MessengyrWeb, :view

  def my_name do
    "Tristan"
  end
end

Now, we can display this variable in the template, by using the simple EEx templating system:

<!-- lib/messengyr_web/templates/page/index.html.eex -->

<p>Hello again <%= my_name %>!</p>

In EEx, the &lt;%-tags run the content as Elixir code, and the "=" at the beginning prints the result.

Voila!

To sum things up...

We've covered a lot of concepts in this chapter so make sure you understand them well before moving on. Here are the main takeaways:

  1. The router connects an endpoint with a controller function through a route.

  2. The controller defines the functions, and gathers data in them that it passes to a view.

  3. The view transforms this data to make it easy to consume before rendering a template.

  4. The template contains HTML code (and eventually some EEx).

Now that you have a grip on these concepts, we're going to start building our real application in the next chapter!