Building a frontend

As we have seen in the basics guide, Feathers works great in the browser and comes with client services that allow to easily connect to a Feathers server.

In this chapter we will create a real-time chat frontend with signup and login using modern plain JavaScript. As with the basics guide, it will only work in the latest versions of Chrome, Firefox, Safari and Edge since we won’t be using a transpiler like Babel. The final version can be found here.

Note: We will not be using a frontend framework so we can focus on what Feathers is all about. Feathers is framework agnostic and can be used with any frontend framework like React, VueJS or Angular. For more information see the frameworks section.

Set up the index page

We are already serving the static files in the public folder and have a placeholder page in public/index.html. Let’s update it to the following:

  1. <html>
  2. <head>
  3. <meta http-equiv="content-type" content="text/html; charset=utf-8">
  4. <meta name="viewport"
  5. content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
  6. <title>Vanilla JavaScript Feathers Chat</title>
  7. <link rel="shortcut icon" href="favicon.ico">
  8. <link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/base.css">
  9. <link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/chat.css">
  10. </head>
  11. <body>
  12. <div id="app" class="flex flex-column"></div>
  13. <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment.js"></script>
  14. <script src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script>
  15. <script src="/socket.io/socket.io.js"></script>
  16. <script src="app.js"></script>
  17. </body>
  18. </html>

This will load our chat CSS style, add a container div #app and load several libraries:

  • The browser version of Feathers (since we are not using a module loader like Webpack or Browserify)
  • Socket.io provided by the chat API
  • MomentJS to format dates
  • An app.js for our code to live in

Let’s create public/app.js where all the following code will live (with each code sample added to the end of that file).

Connect to the API

We’ll start with the most important thing first, the connection to our Feathers API. We already learned how Feathers can be used on the client in the basics guide. Here, we do pretty much the same thing: Establish a Socket connection and initialize a new Feathers application. We also set up the authentication client for later:

  1. // Establish a Socket.io connection
  2. const socket = io();
  3. // Initialize our Feathers client application through Socket.io
  4. // with hooks and authentication.
  5. const client = feathers();
  6. client.configure(feathers.socketio(socket));
  7. // Use localStorage to store our login token
  8. client.configure(feathers.authentication({
  9. storage: window.localStorage
  10. }));

This allows us to talk to the chat API through websockets, for real-time updates.

Base and user/message list HTML

Next, we have to define some static and dynamic HTML that we can insert into the page when we want to show the login page (which also doubles as the signup page) and the actual chat interface:

  1. // Login screen
  2. const loginHTML = `<main class="login container">
  3. <div class="row">
  4. <div class="col-12 col-6-tablet push-3-tablet text-center heading">
  5. <h1 class="font-100">Log in or signup</h1>
  6. </div>
  7. </div>
  8. <div class="row">
  9. <div class="col-12 col-6-tablet push-3-tablet col-4-desktop push-4-desktop">
  10. <form class="form">
  11. <fieldset>
  12. <input class="block" type="email" name="email" placeholder="email">
  13. </fieldset>
  14. <fieldset>
  15. <input class="block" type="password" name="password" placeholder="password">
  16. </fieldset>
  17. <button type="button" id="login" class="button button-primary block signup">
  18. Log in
  19. </button>
  20. <button type="button" id="signup" class="button button-primary block signup">
  21. Sign up and log in
  22. </button>
  23. </form>
  24. </div>
  25. </div>
  26. </main>`;
  27. // Chat base HTML (without user list and messages)
  28. const chatHTML = `<main class="flex flex-column">
  29. <header class="title-bar flex flex-row flex-center">
  30. <div class="title-wrapper block center-element">
  31. <img class="logo" src="http://feathersjs.com/img/feathers-logo-wide.png"
  32. alt="Feathers Logo">
  33. <span class="title">Chat</span>
  34. </div>
  35. </header>
  36. <div class="flex flex-row flex-1 clear">
  37. <aside class="sidebar col col-3 flex flex-column flex-space-between">
  38. <header class="flex flex-row flex-center">
  39. <h4 class="font-300 text-center">
  40. <span class="font-600 online-count">0</span> users
  41. </h4>
  42. </header>
  43. <ul class="flex flex-column flex-1 list-unstyled user-list"></ul>
  44. <footer class="flex flex-row flex-center">
  45. <a href="#" id="logout" class="button button-primary">
  46. Sign Out
  47. </a>
  48. </footer>
  49. </aside>
  50. <div class="flex flex-column col col-9">
  51. <main class="chat flex flex-column flex-1 clear"></main>
  52. <form class="flex flex-row flex-space-between" id="send-message">
  53. <input type="text" name="text" class="flex flex-1">
  54. <button class="button-primary" type="submit">Send</button>
  55. </form>
  56. </div>
  57. </div>
  58. </main>`;
  59. // Add a new user to the list
  60. const addUser = user => {
  61. const userList = document.querySelector('.user-list');
  62. if(userList) {
  63. // Add the user to the list
  64. userList.insertAdjacentHTML('beforeend', `<li>
  65. <a class="block relative" href="#">
  66. <img src="${user.avatar}" alt="" class="avatar">
  67. <span class="absolute username">${user.email}</span>
  68. </a>
  69. </li>`);
  70. // Update the number of users
  71. const userCount = document.querySelectorAll('.user-list li').length;
  72. document.querySelector('.online-count').innerHTML = userCount;
  73. }
  74. };
  75. // Renders a new message and finds the user that belongs to the message
  76. const addMessage = message => {
  77. // Find the user belonging to this message or use the anonymous user if not found
  78. const { user = {} } = message;
  79. const chat = document.querySelector('.chat');
  80. // Escape HTML
  81. const text = message.text
  82. .replace(/&/g, '&amp;')
  83. .replace(/</g, '&lt;').replace(/>/g, '&gt;');
  84. if(chat) {
  85. chat.insertAdjacentHTML( 'beforeend', `<div class="message flex flex-row">
  86. <img src="${user.avatar}" alt="${user.email}" class="avatar">
  87. <div class="message-wrapper">
  88. <p class="message-header">
  89. <span class="username font-600">${user.email}</span>
  90. <span class="sent-date font-300">${moment(message.createdAt).format('MMM Do, hh:mm:ss')}</span>
  91. </p>
  92. <p class="message-content font-300">${text}</p>
  93. </div>
  94. </div>`);
  95. chat.scrollTop = chat.scrollHeight - chat.clientHeight;
  96. }
  97. };

This will add the following variables and functions:

  • loginHTML contains some static HTML for the login/signup page
  • chatHTML contains the main chat page content (once a user is logged in)
  • addUser(user) is a function to add a new user to the user list on the left
  • addMessage(message) is a function to add a new message to the list. It will also make sure that we always scroll to the bottom of the message list as messages get added

Displaying the login/signup or chat page

Next, we’ll add two functions to display the login and chat page, where we’ll also add a list of the 25 newest chat messages and the registered users.

  1. // Show the login page
  2. const showLogin = (error = {}) => {
  3. if(document.querySelectorAll('.login').length) {
  4. document.querySelector('.heading').insertAdjacentHTML('beforeend', `<p>There was an error: ${error.message}</p>`);
  5. } else {
  6. document.getElementById('app').innerHTML = loginHTML;
  7. }
  8. };
  9. // Shows the chat page
  10. const showChat = async () => {
  11. document.getElementById('app').innerHTML = chatHTML;
  12. // Find the latest 25 messages. They will come with the newest first
  13. // which is why we have to reverse before adding them
  14. const messages = await client.service('messages').find({
  15. query: {
  16. $sort: { createdAt: -1 },
  17. $limit: 25
  18. }
  19. });
  20. // We want to show the newest message last
  21. messages.data.reverse().forEach(addMessage);
  22. // Find all users
  23. const users = await client.service('users').find();
  24. users.data.forEach(addUser);
  25. };
  • showLogin(error) will either show the content of loginHTML or, if the login page is already showing, add an error message. This will happen when you try to log in with invalid credentials or sign up with a user that already exists.
  • showChat() does several things. First, we add the static chatHTML to the page. Then we get the latest 25 messages from the messages Feathers service (this is the same as the /messages endpoint of our chat API) using the Feathers query syntax. Since the list will come back with the newest message first, we need to reverse the data. Then we add each message by calling our addMessage function so that it looks like a chat app should — with old messages getting older as you scroll up. After that we get a list of all registered users to show them in the sidebar by calling addUser.

Login and signup

Alright. Now we can show the login page (including an error message when something goes wrong) and if we are logged in call the showChat we defined above. We’ve built out the UI, now we have to add the functionality to actually allow people to sign up, log in and also log out.

  1. // Retrieve email/password object from the login/signup page
  2. const getCredentials = () => {
  3. const user = {
  4. email: document.querySelector('[name="email"]').value,
  5. password: document.querySelector('[name="password"]').value
  6. };
  7. return user;
  8. };
  9. // Log in either using the given email/password or the token from storage
  10. const login = async credentials => {
  11. try {
  12. if(!credentials) {
  13. // Try to authenticate using the JWT from localStorage
  14. await client.authenticate();
  15. } else {
  16. // If we get login information, add the strategy we want to use for login
  17. const payload = Object.assign({ strategy: 'local' }, credentials);
  18. await client.authenticate(payload);
  19. }
  20. // If successful, show the chat page
  21. showChat();
  22. } catch(error) {
  23. // If we got an error, show the login page
  24. showLogin(error);
  25. }
  26. };
  27. document.addEventListener('click', async ev => {
  28. switch(ev.target.id) {
  29. case 'signup': {
  30. // For signup, create a new user and then log them in
  31. const credentials = getCredentials();
  32. // First create the user
  33. await client.service('users').create(credentials);
  34. // If successful log them in
  35. await login(credentials);
  36. break;
  37. }
  38. case 'login': {
  39. const user = getCredentials();
  40. await login(user);
  41. break;
  42. }
  43. case 'logout': {
  44. await client.logout();
  45. document.getElementById('app').innerHTML = loginHTML;
  46. break;
  47. }
  48. }
  49. });
  • getCredentials() gets us the values of the username (email) and password fields from the login/signup page to be used directly with Feathers authentication.
  • login(credentials) will either authenticate the credentials returned by getCredentials against our Feathers API using the local authentication strategy (e.g. username and password) or, if no credentials are given, try and use the JWT stored in localStorage. This will try and get the JWT from localStorage first where it is put automatically once you log in successfully so that we don’t have to log in every time we visit the chat. Only if that doesn’t work it will show the login page. Finally, if the login was successful it will show the chat page.
  • We also added click event listeners for three buttons. #login will get the credentials and just log in with those. Clicking #signup will signup and log in at the same time. It will first create a new user on our API and then log in with that same user information. Finally, #logout will forget the JWT and then show the login page again.

Real-time and sending messages

In the last step we will add functionality to send new message and make the user and message list update in real-time.

  1. document.addEventListener('submit', async ev => {
  2. if(ev.target.id === 'send-message') {
  3. // This is the message text input field
  4. const input = document.querySelector('[name="text"]');
  5. ev.preventDefault();
  6. // Create a new message and then clear the input field
  7. await client.service('messages').create({
  8. text: input.value
  9. });
  10. input.value = '';
  11. }
  12. });
  13. // Listen to created events and add the new message in real-time
  14. client.service('messages').on('created', addMessage);
  15. // We will also see when new users get created in real-time
  16. client.service('users').on('created', addUser);
  17. login();
  • The #submit button event listener gets the message text from the input field, creates a new message on the messages service and then clears out the field.
  • Next, we added two created event listeners. One for messages which calls the addMessage function to add the new message to the list and one for users which adds the user to the list via addUser. This is how Feathers does real-time and everything we need to do in order to get everything to update automatically.
  • To kick our application off, we call login() which as mentioned above will either show the chat application right away (if we signed in before and the token isn’t expired) or the login page.

What’s next?

That’s it. We now have a plain JavaScript real-time chat frontend with login and signup. This example demonstrates many of the core principles of how you interact with a Feathers API.

If you run into an issue, remember you can find a complete working example here.

In the final chapter, we’ll look at how to write automated tests for our API.