In this chapter, we're going to build our messaging interface as a single page application (a.k.a SPA). This means that there will be no page reloads here for the user – all the UI is instead updated dynamically using JavaScript.
In order to achieve this, we're going to use a very different approach compared to the previous lessons. Our Phoenix app will deliver all the initial static content on the first page load, and then act only as a thin API client to transmit JSON data.
As you probably know if you're a web developer, there are many JavaScript frameworks to choose from when building a single-page application, and they all have their strengths and weaknesses. For this Messengyr project, we're going to use React, since it's easy to drop into our existing app structure.
Quick note: I apologise in advance for all the JavaScript code that we have in this lesson. I know that this is an Elixir course, but if you want to build modern web applications, it's virtually impossible to ignore JavaScript. In my opinion, it's better to learn how to do some things the right way, even if it includes utilising some other languages in the process, rather than always sticking to the course language just for the sake of it. Anyway, let's move on!
Moving our JS file
You might have noticed that there's already an existing JavaScript-file in our application, at assets/js/app.js
.
This file gets imported as soon as we fire up any Phoenix page, since it's imported in in our layout template (lib/messengyr/web/templates/layout/app.html.eex
). However, we actually don't need any JavaScript in our app until the user hits the /messages
path. Therefore, let's delete the line that imports it in our layout template, and instead add it to our newly created Chat template instead:
Setting up Babel and React
Next, we're going to install two NPM dependencies – Babel and React. React is what we'll use to build our dynamic messaging interface, and Babel lets us use the newest JavaScript syntax to make it more pleasant to write.
We'll start by installing both the core React library and React DOM (so that we can use React in the browser). Go to the assets
-folder and run this command:
In order to be able to use these libraries in our client JavaScript, we need to whitelist them in our brunch-config.js
file. Scroll all the way down until you see the npm
key:
Next up: Babel! We're going to install the core Babel library, as well as babel-preset-react (so that it understands React syntax) and babel-preset-es2015 (so that we can use the new ES2015 syntax).
These dependencies are not actually going to be used in the browser, so we don't need to whitelist them. We do however want to use them when we compile our JavaScript, so go back to brunch-config.js
and look for the plugins
key, and make sure that you add "es2015"
and "react"
in the list of presets:
That's it! Now let's try out these new fancy libraries!
"Hello world" in React
To make sure that React and Babel work, we'll render a simple "Hello world" text on our /messages
page. Start by opening up assets/js/app.js
and delete everything that's in it (including the import "phoenix_html"
statement), and replace it with this code instead:
Let's quickly go through what we're doing here, in case you aren't familiar with React:
We import our two newly installed dependencies – React and ReactDOM, so that we can use them in the file
We create a new
App
class, which is a React component. In it, we simply specify that it should render adiv
-tag containing the string "Hello world!".Finally, we use ReactDOM to render this component. We specify the name of the component (
App
), and the element on the page that it should be rendered inside of (an element with the id"app"
).
This element with the ID "app" doesn't exist on our messages page yet, so we need to create it:
After doing this, you should see "Hello world!" when you reload /messages
(if not, you might have to restart your Phoenix server).
Composing our UI
Now, you might be thinking "Gee, that was a lot of work just to render a shitty 'Hello world!' text"! And yes, you'd be right about that. But remember what the interface that we actually want to build looks like:
No matter how good Elixir's own EEx templating language is, it's still being rendered on the server instead of the client. If we want to see our messages come in in real-time, without manually refreshing the page (which would be a terrible user experience), we need to use JavaScript. ¯\_(ツ)_/¯
With that out of the way, let's start building our messaging interface! Whenever you use a component-based UI library such as React, it's good to try to estimate in advance the number of components that we're going to need.
Based on our Sketch file, it seems logical to split our app into four components:
MenuContainer
MenuMessage
ChatContainer
ChatMessage
We're going to create a new folder in our js
folder called components
where we'll put these.
We'll start by creating our two main components – MenuContainer
and ChatContainer
. Since this isn't really a React course, we won't go through every little part in detail, so you can just copy and paste the following code. You'll notice that our React components are basically just outputting HTML anyway, the only caveat with JSX is that if you want to add a class
to an element, you have to use the className
attribute instead.
As you can see, we're importing the MenuMessage
component in our menu container, and the ChatMessage
component in our chat container and rendering them inside a list. So we need to create these components as well!
Finally, now that all our components have been defined, we simply need to import ChatContainer
and MenuContainer
into our initial app.js
file, and render them there instead of "Hello world!":
Now restart your Phoenix server and reload the page. If everything went well, you should see all the HTML from your components rendered onto the page!
To make it look good, we can create a new messages.scss
file in our css
folder, and add these styles (sorry, it's long, but it's the last CSS we'll need!)
Adding fake data
Right now, our React templates contain static data such as "John Doe" and "Here's a message!". We want to remove this static data from our templates and instead use whatever data is given to the components via props!
There are primarily three types of data in our UI that we need to account for – rooms, messages, and users. The "rooms" list is the collection of conversations in our MenuContainer
. Each room then contains a list of "messages", which are rendered in theChatContainer
.
For now, we'll just define some fake data in a separate file (don't worry, in the next chapter, we'll learn how to fetch it from the database instead). Let's create a file called fake-data.js
in our js
folder and add a big JS object inside of it that contains all the data we're interested in:
Let's take a moment to analyse this data. We have a single room with an ID of . Our counterpart (the other user) in that room is "alice", who has the user ID . Alice and our user have a list of messages. The first one ("Hi!") is outgoing
, whereas the second one ("Hello there") was sent from her to us.
Alright, let's render this data! We'll start by importing the fake data into app.js
, and there we'll also create the constants ROOMS
and MESSAGES
. The first constant is passed as a prop to the MenuContainer
and the second one is passed as a prop to the ChatContainer
:
Next, we go to the MenuContainer
. The important thing here is that we want to render each room as a MenuMessage
inside our ul
tag. In other words, each object in our ROOMS
-data needs to be transformed into a MenuMessage
component. To do this, we can use JavaScript's map
function. Notice that we pass down every room
as a prop to the MenuMessage
component too.
Then, we simply render our newly created rooms
variable inside the ul
tag:
Next, we go to MenuMessage
. Now that we receive the room
prop from MenuContainer
, we can correctly render that data in the component.
There are three things that we want to render here (if we exclude the avatar, since we don't have any fake data for that yet) – the username of the counterpart, the last message that was sent (by either of us), and the time when it was sent. All this can easy be extracted from the room
prop that we receive:
After doing this, you should be able to see our fake data in the Messages menu!
Next, we move on to the ChatContainer
. Here, we apply the same principle as with the MenuContainer
– we want to render each item in the messages
list as a ChatMessage
component. Again, we use the .map
function, and we replace the content inside the ul
-tag with our new messages
variable instead:
Finally, we go to ChatMessage
. Here, we first need to know if the message is outgoing or not. If it is, we apply the class "user"
on our li
element, otherwise we use "counterpart"
instead. This will render the chat bubbles as either blue (outgoing) or gray (incoming). Then we simply render the text.
And now, we're finally done!
In review
Phew, all this JavaScript gives me fatigue! You should now know roughly how to set up Babel and React (or a similar frontend library) with Phoenix, as well as how to render some dynamic UI as components by passing down data as properties.
In the next chapter, we're going to go back to Elixir (yay!) to learn how to fetch this data from our database and render it as JSON so that we can use it in our client.
Comments
You need a Ludu account in order to ask the instructor a question or post a comment.
"Then you don't deserve me at my ..."
"If you don't love me at my ..."
I have a problem here: Couldn't find preset babel-preset-react
@diemesleno-souza-carvalho: Did you run
npm install --save-dev babel-preset-react
?Wise words indeed
I approve of the word "shitty" here.
;)
web/templates/chat/index.html.eex
@jabreu: 👍