Lesson 12

From website to web app

10

In the previous lesson, we made it possible for the user to sign up and log in to our website. In this chapter, we want to take advantage of this new functionality to make the UI of our app more dynamic!


This is sometimes referred to as the difference between a website (which has the same static content for everyone), and a web app.


Web apps typically offer a "logged-in" experience that lets the user read and write content.


Extracting the header into a template

The first thing we want to do is to change the header depending on if the user is logged-in our logged-out. We can refer to our Sketch file to see how we intend to make it look:


The logged-out header VS the logged-in header.


Alright, let's implement that! Since we're going to add quite a bit of logic to our header now, it makes sense to extract it into its own template. We'll put that template in the layout-folder (that makes sense, because a header is part of the layout, right?):

lib/messengyr_web/templates/layout/header.html.eex

Now we can remove the header-tag from our app.html.eex template, and replace it with a simple render function:

lib/messengyr_web/templates/layout/app.html.eex

Nothing fancy here! We just specify the view (LayoutView), the template ("header.html"), and then we use assigns in order to be able to use @conn in our template (which we will need in order to retrieve the logged-in user, as you saw in the previous lesson).


If you reload the page, it should still look exactly the same, which is a good sign!


Now comes the fun part! Remember how we learned that views "transform our data to make it easy to use when rendering a template"? Well, that's exactly what we're going to do now. We want to make the logic in our template as simple as possible, by defining good functions in our Layout View.


The first thing we want to know in our template is whether the user is logged-in or not. For that, we'll use an if-statement in our header template to render different things depending on the result of the logged_in?/1-function:

lib/messengyr_web/templates/layout/header.html.eex

We obviously have to define this logged_in?/1-function now, or else we get a compilation error.


A tip when using libraries like Guardian: if you're too lazy to check the documentation (like I am sometimes), you can always use the special __info__ function to get information about Elixir modules. To get a list of all the functions that the Guardian.Plug module has for example, you simply use IO.inspect Guardian.Plug.__info__(:functions). You should then see authenticated? among the list of functions in your console, along with the number of parameters it takes. That's the function that we're going to use:

lib/messengyr_web/views/layout_view.ex


Even though it's covered by the awesome logo, you can see that the "Logged in"-text renders as we expected!


Showing a username and an avatar

Just showing "Logged in!" in the header is kind of lame, so the next step is to show the user's username and the avatar. First, we make some changes to the template and call the username/1 function inside the if-statement:

lib/messengyr_web/templates/layout/header.html.eex

Now we need to define this username/1 function in our view. Here we can just use Guardian's current_resource function that we saw earlier, and then extract the username from that using pattern matching:

lib/messengyr_web/views/layout_view.ex


Tada! And the CSS that we added earlier already has it styled for us.


Next, we want to also add an avatar. For this, we don't want to make the user upload a profile picture or anything – that's beyond the scope of this project. Instead we'll use Gravatar to fetch whatever profile picture (if they have one) that is linked to the user's email address.


If we check Gravatar's documentation for image requests, we learn that we need to hash the email address in the image URL. Let's do that in a new function!

lib/messengyr_web/views/layout_view.ex

Now we can add our image tag to the template, and call the avatar/1-function:

lib/messengyr_web/templates/layout/header.html.eex


If you have a profile picture linked to your email address on Gravatar, you should now see your picture in the Phoenix app!


Adding a "log out"-button

We're almost done with the header. The last thing we want to add is a "log out"-button, next to the username. The button will just be a link to the URL /logout which will be connected to a function in our Page Controller.


You know the drill for new routes by now! We start by adding a line to our router file:

lib/messengyr_web/router.ex

Next we add the logout/2-function to our PageController. In it, we'll just use Guardian's sign_out function, set a flash message, and redirect to the landing page:

lib/messengyr_web/controllers/page_controller.ex

And finally we add the link to /logout in our header template, before the avatar and username:

lib/messengyr_web/templates/layout/header.html.eex

Once that's done, you should be able to log in and log out as much as you want!

Fixing the landing page forms

There's a little flaw in our landing page that we've overlooked until now – the login and signup boxes don't work! Right now, we can only log in by going to the /login route, and we can only sign up by going to /signup.


These boxes are currently useless!


Thankfully, we already have all the logic ready, so we just need to connect them to the relevant actions in our Page controller.


First of all, we need a User changeset on the landing page for the signup form to work, so we need to set it in our existing index/2-function in the controller:

lib/messengyr_web/controllers/page_controller.ex

Next, we simply replace the form in the login box with the dynamic form from login.html.eex, and the form in the signup box with the one from signup.html.eex. The final index.html.eex template should look like this:

lib/messengyr_web/templates/page/index.html.eex

That's it! Now you can log in and sign up straight from the landing page!

Authenticated routes

Our final task in this lesson is to create a new route (/messages), that's only available for logged-in users. First we create the route:

lib/messengyr_web/router.ex

Since this route is for logged-in users only, it's going to work in a lightly different way than our previous routes. Therefore, we use a new controller for it – the ChatController. We need to create a file for it, and add its index/2-function:

lib/messengyr_web/controllers/chat_controller.ex

As you know, we also need a view for this controller if we want the template to render, so let's create ChatView:

lib/messengyr_web/views/chat_view.ex

Finally we need a template. By following Phoenix's conventions, we know that this needs to be created at messengyr_web/templates/chat/index.html.eex. The file can be completely empty for now.


If you followed the instructions, you should be able to see an empty page when you go to /messages.


Now we want this route to only be available to users who are logged-in. For this, we can use Guardian's EnsureAuthenticated plug in the ChatController:

lib/messengyr_web/controllers/chat_controller.ex

The plug's handler should point to a module that contains an auth_error/3 function. In that function, we specify what happens if the user is not logged-in, yet tries to hit a route connected to this controller.


Since we currently only have a single controller that requires authentication (ChatController), we can simply set this handler to __MODULE__ to tell Guardian to look for the auth_error/3-function in the current module.


Let's create this auth_error/3-function now to handle unauthenticated users:

lib/messengyr/web_controllers/chat_controller.ex

That's all! Now try to go to /messages without being logged in and you should be redirected an see a flash message. If you are logged in however, you should see the empty chat/index-template, like before.

A final note:

Guardian also has an EnsureNotAuthenticated plug that we could use to make sure that certain routes can only be accessed when the user is not logged in. This could be used for the login page for example (because why should you be able to access the login page when you're already logged in?). For now though, we'll keep things simple and let these pages be available even for authenticated users.


Congratulations! We've now gone from having a mostly static website to something that resembles a web app! In the next couple of lessons, we're going stay on the /messages route and build the interactive messaging interface!

Comments

Saw Thinkar Nay Htoo

It seems, plug Guardian.Plug.Pipeline, module: Messengyr.Guardian, error_handler: MessengyrWeb.ChatController is needed in router.ex. Otherwise, it will give error_handler not set in Guardian pipeline Error.

Eric Chua

thanks :) added within the pipeline :browser and it works

Diemesleno Souza Carvalho

Here the same, we use: MessengyrWeb.ChatView and MessengyrWeb, :view

Diemesleno Souza Carvalho

Now we use MessengyrWeb.ChatController

Diemesleno Souza Carvalho

And use MessengyrWeb, :controller

Diemesleno Souza Carvalho

Now the correct is: MessengyrWeb.LayoutView

profile/avatar/default
Braun Andreas

Thank you, works for me!

Peter Marreck

shouldn't this be <img src="<%= avatar(@conn) %>" /> (with doublequotes)?

Tristan Edwards

@peter-marreck: Indeed, that would be more correct. It seems like it works without them too though. :)

Peter Marreck

Note for Rails veterans: The = before the if, else and end is necessary for some reason (in Rails it would not be since it's just logic) otherwise nothing outputs