Introduction & Project Setup
Hi guys, welcome to the Phoenix tutorial, part one. My name is Tensor from the Tensor Programming blog. So today, we're going to take our first look at the Phoenix framework for Elixir, and we're going to use it to build a real-time chat application, or at least we're going to use it to start building a real-time chat application. So Phoenix is sort of like the Rails of Elixir, but at the same time it also has a lot of really cool features that Rails just simply doesn't have.
All right so let's build our project. To make a project with Phoenix you just type in mix phoenix.new and then you type in the project name. In this case, we're going to call it chatter because it's going to be a chat application. Now, this will create our directory as you can see, as well as a lot of files here. And now we can fetch and install the dependencies. This will run mix deps.get and it will run npm install and set up Brunch for us.
For those of you who want to follow along, I'll be sure to leave a link to Phoenix in the description so you can install it in the mix. All right, so our dependencies are finished, and as you can see, we get a nice little message here that says that we're all set.
Phoenix Application Overview
And we have two options. So we can run our Phoenix application on a server as well as open up the interactive REPL, or we can just run the server itself. And we can also create our database using mix ecto.create. So before we actually spin up the server, that's what we're going to have to do is set up our database.
All right, so here's what our directory looks like. As you can see, there are a bunch of different folders in here as well as different files. We have our mix.ex file here. Here we have a package.json file. And as you can see, we have a bunch of dependencies. We have Babel, Brunch, clean CSS, CSS Brunch, JavaScript Brunch, and Uglify JS, as well as Phoenix and Phoenix HTML. We also have this web folder. So this is where most of the Phoenix framework exists, is inside of this web folder. You can see we have a views folder, we have a templates folder which has all of our HTML, we have a static folder which has our CSS and JavaScript, and we can put of course our assets such as images and stuff in there.
Exploring the Project Structure
We have a models folder which is currently empty because we have no models. We have a controllers folder which has a single controller in it right now. And we have a channels folder, and this is where we're going to do most of our development for this application. We also have a test folder. This allows us to build tests for each of our components. We have a priv folder. Now this has all of our static information in it, like our static CSS and JavaScript, the stuff that we really just don't want to touch. Then we have our node modules. We also have our lib folder. With like with any Elixir project, this is where our main Elixir application resides. And then we have our Elixir dependencies, our config folder, and a build folder.
Database Configuration and Creation
So to set up our database, we want to go into config and dev.exs, and we want to go all the way down to the bottom here. This is configuring your database, and by default, Elixir uses Postgres, and it puts in the default username and password here, which is postgres and postgres. Now my Postgres is set up to use this username and password, so I do not need to make any changes here. So after you set up your development file, you can run mix ecto.create and this will create your database in your PostgreSQL server. So running mix ecto.create will actually compile your entire project, as you can see here, it compiled the Chatter app, and then it will create the database.
The application that we're going to be building in this tutorial is going to be a chat application.
Application Goals & Initial Server Run
There are two major features that we want to implement. The first one is a list of all the users who are currently connected to our application as well as when they signed on to our application, so we want to add a timestamp. And then of course, we want the ability for the users online to post messages to each other, which each will see in real time.
So before we do all that, I'm just going to run the server so that you can take a look and see what a clean Phoenix app looks like. We do this just by running mix phoenix.server, and by default, this will put us out on localhost:4000. Here is our default Phoenix application. As you can see here there's not really much going on. Our chat application is actually going to come in right here where you see this Jumbotron. We will later, we will actually remove all this other stuff here like the Phoenix framework logo. For now, we're just going to be editing this area of our application.
Building the HTML User Interface
So we want to edit our template page/index.html.eex file. It has a bunch of HTML in it as well as embedded Elixir. Right here is some embedded Elixir for example, and we can just take all of this and delete it. If we save it, all of that information disappeared from our application. Phoenix has hot reloading, which is pretty nice for development.
We want to create a div that surrounds everything. So we're going to say div class row, and then we're going to have a div inside of it. And inside of this div is where we're going to have our username. So when the user comes in, we want them to be able to see their name. So we'll put a span in here, and the span will have an ID of user. This will be important because we'll be using JavaScript to get this user later. Then we need to use some embedded Elixir to get the username. So to do this, we use a lesser than sign, then we use a percent sign, then we use an equal sign to open up our embedded Elixir, and then to close it, we use a percent sign and a greater than sign. We're going to get our username just by looking at our connection and then looking specifically at the parameters and trying to find the map of name: user. And we'll say something like "Hello there" and then the username. So if the username is say Tensor, it'll say "Hello there, Tensor".
All right, so now we're going to make another div, and then inside of this div we will have our messages. And this messages will basically just be an unordered list. And the way it will work is every single time somebody types a new message inside of the input below it and hits enter, it will create a new list item underneath of our messages. With our input and everything, we will have a list of our online users. And again, we're going to use an unordered list to generate this, and we'll just generate list items with the usernames inside of them.
Passing User Info & Generating Presence
All right, so that's all we really need for our HTML. Let's take a look at what this looks like on our server. As you can see, we've got a text box here. This will say "Hello there" and then the name of the user that is logged in. And we can actually log in as a user right now by typing in question mark user= and then our name. And as you can see, it says "Hello there, Tensor", but of course it doesn't show Tensor as being online and it doesn't show me anything else, and that's because we haven't created the logic in our backend yet.
All right, so now we want to bring a new module into our Phoenix application. This module is called Presence. So we're going to use Mix to do this. We're going to say mix phoenix.gen.presence. And as you can see, this comes up with a nice little message. It says, "Add your new module to your supervision tree," and then it gives us the code that we need. So we can just copy this.
Integrating the Presence Supervisor
And if we go up to our lib folder and we go inside of our Chatter application here, you can see we have our supervision tree right here. So we can put our Presence supervisor right here to wire it up. So what our Presence supervisor is going to allow us to do is to actually track the users as they log in and out of our server. So to get this process actually started we need to stop our server real quick and then restart it. So for the most part, you don't need to stop your Elixir servers but in this case, because we've added a new module, we need to terminate it and then restart it to spin up that process.
Configuring the User WebSocket
All right, so the next thing we want to do is to actually create a web socket inside of our Phoenix application. So if you don't know what websockets are, let me explain real quick. A socket is an open connection between a user's client and our application. So the client and the server, with a typical web app, the client makes an HTTP request to the server and then the server returns a response. However with a socket, we can have a connection that remains open between the client and the server, which allows us to exchange messages back and forth in real time.
So to actually enable our socket here, the first thing we want to do is come into our web/channels/user_socket.ex file. We want to uncomment line five, so this channel "room:*", Chatter.RoomChannel and then we want to come all the way down to this connect function and we want to modify it. So this params variable, we want to actually set a specific variable for it. So we want to deal with our users. Specifically our user is a map currently. So we're going to put that map in here and we're going to say "user" corresponds with the user. And then we're going to remove this socket part here and we're going to put a assign in here. And the assign takes in our socket and then it takes in the key and the value. In this case, it's going to take in a key of :user and then take in the value of user.
Creating the Room Channel
All right, so this is basically allowing us to create our web socket, but it's also letting us take use of what are called channels. So Phoenix channels are sort of like, in this case they're sort of like web rooms. If we wanted our web app to have multiple rooms, then we could create multiple Phoenix channels to account for this. Now in this current tutorial, we're not going to have multiple channels, but we may in the future.
So to handle the one channel that we're going to use, we need to create a new file in this channels folder. Our file is going to be called room_channel.ex. We also want to use Chatter.Web, :channel inside of this. So this will specify that we want this to be a channel, so to handle the channel logic of our application. And then we also want to alias Chatter.Presence so that we have easy access to the presence module inside of this module. Next, we want to create a join function here. And the join function will take in this string that says "room:lobby". Then it will take in an anonymous variable and then the socket. And then we will send self(), as in the user, and then we will correspond this with a callback function that refers to this atom :after_join.
So here is our callback function, this handle_info function. And as you can see, the two atoms are corresponding, and we're passing the socket through here as well. And we're saying, okay, Presence.track, and we want to track at the socket, and then we want to use socket.assigns.user. And we're going to map the user, and we're going to find out when the user logged into the socket, so when the user first came onto the socket. And this will allow us to create a timestamp so that we know when a user logs in. Also the nice thing is this socket.assigns.user is actually where our username is. For example, currently Tensor is logged into our application, so this will be replaced with tensor. The field online_at will be populated by this system time, which will be based in milliseconds.
Client-Side Socket Connection
Now we're going to use JavaScript to make this into more readable information. And then we push all this back to the socket, and then we update the list. So our list is our list of users on the socket. So now let's go into the JavaScript layer and handle all of our web socket information. So to go into our socket layer, we just go down to this here, the JavaScript inside of this static folder and we want to go into this app.js.
As you can see, all that's happening in here is that it's importing Phoenix HTML. We want to import a few other things and then we want to actually add some logic in here. We want to import our Socket and Presence from our Phoenix application. Then we want to define a user variable. So as you can see here, we're getting the element by ID user. So if we go back into our template, our user is inside of this span. So we're getting our username from this conn.params. Finally, we want to create a new socket. So we're creating a new socket on this /socket route. So Elixir automatically puts the web socket on /socket. And if we want to actually see this happening, if we go into our actual Chatter application in the endpoint.ex file, you'll see here it says socket "/socket" and then it assigns Chatter.UserSocket to this socket. So we're pulling in that socket from our backend, and then we're bringing it into our JavaScript, and then we're connecting to it through this function here socket.connect().
Handling Presence in JavaScript
So now we need to handle the presence so that we'll be able to see the users as they are logging in. So we need to create a new JavaScript object. so we're going to say let presence = {} and then our object. Then we want to create an anonymous function, so we're using this formattedTimestamp variable and then we're binding it to this anonymous function that takes in ts, which stands for timestamp. And then we're creating a date here using our timestamp. So this is that milliseconds that we had before, and then we are going to localize that date and create a string out of it. So this will return a human readable version of the timestamp that we created in our backend.
So now we're actually creating our user object here. So we're going to return a object with our user inside of it and then the online_at, and then of course we're going to call our formattedTimestamp function here. We're going to pass in this metas.online_at. This is basically just the backend information that corresponds with online_at. If we go back to our room_channel, you'll notice that we use this online_at to build our millisecond timestamp and then we're just using it, we're passing it through here as well just to get a readable version of it.
So finally we want to get our userList element. So if you remember in our index.html, we have a unordered list here with an ID of user-list. We're assigning it to this variable userList. And then we're creating a function called render which takes in the presences, which is this list and everything. And then we are going to create some inner HTML here. So we're going to say userList.innerHTML equals the presences.list. We're passing the presences and the listBy here, and then we're creating a map, and this is mapping a list item to our presence.user and then we're just saying "online since", and this will have our timestamp inside of it. And then we're adding it to our application.
Joining the Room and Handling Events
So this is not enough yet. We need to define our room now. So we're going to create a variable called room and we're going to get the channel that we're using. And if you remember, we're using this room:lobby channel. So after telling our JavaScript about this channel here we need to handle two events. So the first event is with our presence_state. So when the server sends us the state of everybody who's online, which happens when we first connect or if we ever disconnect from our application, this will send everything to the HTML. And then the presence_diff is an event that happens when somebody joins while we're in the application, and it just needs to add that person to the list.
Demonstrating the Live User List
So if we look at our application now, as you can see here in our little box here, it says "Tensor online since" and it has the current date as well as the time. So as you can see here, we have tensor2 and tensor1, and over here it updated the list. It says Tensor first and then Tensor2, and it shows both of the timestamps as well. So this is data that will stay here.
Implementing the Message Broadcast Backend
So now that we have it so that we can have multiple users in here and it will list those users and their timestamps, we need to make it so that they can message each other. So we're going to go back into our room_channel.ex file and we're going to create a simple function here. So this function is going to listen on our socket here and it's going to be listening for messages of type "message:new". And then every time the JavaScript sends a message:new type message, it will then broadcast that message to everybody who's connected to the room, including us, the people who wrote the message. And it will broadcast it in this format. So it will give us the username and then the body, which is the message itself, and then it will have a timestamp on it as well.
Client-Side Message Handling
So now we have to go back into our app.js and we have to write the JavaScript for this part. So we're going to use a variable messageInput and we're going to assign it to our input. And if you can see up here, we have the ID of message-list here, and as you can see here, we have an ID of new-message on our input here. So we're assigning that element to this variable. And then we are adding an event listener to the variable, and it's an event listener on the key press of the Enter key. So when we hit the Enter key, and when the value inside of our input is not empty, then it will push a new message of type "new" back to our server. And then of course, it will clear the input.
Then finally we want to create a messageList variable. Now this is getting our message-list element. If you remember, it's our list here, our unordered list message-list. And then we are rendering that. So we're making a new HTML element here and as you can see, this is formatted with the username, then a timestamp, and then the message body itself. And then every single time we have a new message, we're calling this function here and then we are appending it to our list here. And finally we are actually complete.
Styling the Chat Window
So if we type in a message, so "hi", as you can see it's popping up. But let's actually make this a little nicer. So let's make a window here so that it doesn't just keep pushing down the input field as we type in messages. So if we jump into our HTML here, we can just write some style in here in line. So we want a static height. Let's say 400 pixels. We want a border, say one pixel solid black. Then we want to deal with the overflow in the Y direction because you know our chat will move up and down. So we'll just put it auto and that should be it.
So now let's take a look. So here we go. Now we have a big box. "Hello". And as you can see it's saying "Tensor: hello". Let's put a little bit of padding so we push this a little bit towards the center, and let's say 10 pixels should be enough. And let's save it. And there we go. That looks a little better.
Final Application Demonstration
All right, so I made four different users. As you can see here, we have Tensor, Abel, Tensor, Metalman, and H hey 1 2 32. And we can just type in messages and they will show up on all the screens here. So if we had actual people connected, they would be able to talk to one another. And then for example, if say Jorge leaves, you can see immediately it disappears. And say Tensor or say Metalman... Now we're on Metalman, so let's say Abel Tensor will leave. So as you can see there, it just disappears immediately. So now we only have two people connected. And if we put in say like, Tensor 2 here, it will change.
Next Steps and Conclusion
So of course our messages are not persistent, because we're not logging them into a database. So while for example Metalman can still see the messages from the other users, tensor, if I go back in as Tensor, I won't be able to see all the previous messages before I logged in. Now this is something that we will address in a future tutorial. And we will also look into creating authentication and some other things. All right guys, well I hope you enjoyed this tutorial. If you did, feel free to subscribe and like. If you have any questions or comments, feel free to leave them in the box below. And if you disliked the video, then by all means dislike it as much as you want. All right guys, well have a good day.