© Frank Zammetti 2020
F. ZammettiModern Full-Stack Developmenthttps://doi.org/10.1007/978-1-4842-5738-8_9

9. Delivering the Goods: MailBag, the Client

Frank Zammetti1 
(1)
Pottstown, PA, USA
 

With the server side of MailBag ready to go, it’s time now to move on to the client side of things! Here, we’ll get back to playing with our good friend React, along with some supporting libraries, and we’ll hook it all up to the server that was built in the previous chapter. In the end, we’ll have ourselves a fully functional webmail application that you could use for real if you wanted to!

What Are We Building?

Unlike the server part of MailBag, the client part is something we can easily take a look at because it is, by its nature, a visual thing. That makes describing what we’re going to build easy for an author like me! So let’s begin with Figure 9-1, which shows MailBag as it would appear after initial launch (well, in a general sense, what you would actually see would depend on the mail account you hook it up to, but this is linked to my account, so it gives you a general idea at least).
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig1_HTML.jpg
Figure 9-1

Our first look at MailBag’s client

I make no representation that this app can compete with the likes of Gmail or Outlook, but it gets the primary job of a webmail application done. Up top, you see that you can create a new message, and you can create a new contact. A list of existing contacts is over on the right, and on the left is the list of mailboxes that the IMAP server used returns. In the center, at the top, is a list of messages in the currently selected mailbox. For example, if I click the INBOX button on the left, I see some messages in Figure 9-2.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig2_HTML.jpg
Figure 9-2

A few messages in my inbox (I get A LOT of spam – this was a good day!)

In this screenshot, I’ve also gone ahead and clicked one of the messages, which shows its details at the bottom. As you can see, I can reply to the message, or I can delete it (and none of the fields above the buttons are editable). When you click the NEW MESSAGE button in the toolbar, you see the same basic screen except that the ID and the date are hidden, the From field becomes a To field, and there is just a single Send button (and everything is editable, naturally).

When you click a contact on the right or when you click the NEW CONTACT button, you wind up at essentially the same place, which is shown in Figure 9-3.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig3_HTML.jpg
Figure 9-3

Adding a contact

Similar to the view of a message, clicking an existing contact shows a screen that is essentially the same except that in that case, the SAVE button becomes a DELETE button (and the fields are read-only).

Note

Although this section is just meant as a walkthrough, you of course can (and maybe even should) fire up MailBag now and talk a look at it in action. Assuming you’ve already gone through Chapter 8 and downloaded the source code bundle and installed dependencies, you can fire up the MailBag server, and the client is then available at http://localhost automatically.

Basic Requirements

Now that you know what the app looks like, let’s catalog the basic functionality it must provide. As I said, we’re not challenging Google’s webmail supremacy, so the list isn’t all that extensive (and, given what you saw in the last chapter, you effectively already know what the list must contain):
  • The server must be consulted for a list of mailboxes under a single configured IMAP account once at startup.

  • The user can select a mailbox from the list, which will double as a refresh action. The messages will be displayed in a tabular form and will show the date received, subject, and sender.

  • Clicking a message shows it below the list, including subject, sender, ID on the server, date received, and, of course, the message itself, as plain text.

  • The user must be able to delete the message being viewed.

  • The user can begin a new message in reply to the message being viewed. The subject will automatically have “Re:” prepended to it, and the original message will be shown below a bit of marker text.

  • A list of contacts will be stored on the server. The user can select a contact from a list and can delete the contact or begin a message to them. They can also, of course, add a new contact.

  • Like mailboxes, the server will be consulted once at startup for the list of contacts.

In addition to the requirements, since this is an interface, we need to discuss the overall structure of it, if for no other reason than to provide a common vocabulary to use going forward. The screenshots make it fairly obvious, but the overall structure, or layout, of the interface is described in Figure 9-4.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig4_HTML.jpg
Figure 9-4

The overall layout of the client in block form

As you can see, in block form, it mimics the arrangement shown in the screenshots. However, how we achieve that layout is the critical piece of the puzzle, and that’s something we’re going to talk about a bit later on, after a necessary detour. But, before we get to that, we have to set up the project, so let’s do that now!

Setting Up the Project

Setting up the client project is very similar to setting up the client. Create a client directory under the project root directory alongside the server directory. Then, create a src directory in it.

Typically, I would detail the initialization steps to take at this point, but I think you’ve seen those steps enough between the server code and previous examples, so let’s save some time, and instead, I’ll just show you what the package.json file should be after doing a basic npm init and then installing all the necessary dependencies:
{
  "name": "mailbag",
  "version": "1.0.0",
  "description": "MailBag",
  "main": "main.tsx",
  "scripts": { "build": "webpack --mode production" },
  "author": "Frank W. Zammetti",
  "license": "ISC",
  "devDependencies": {
    "awesome-typescript-loader": "^5.2.1",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "css-loader": "^3.3.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "style-loader": "^1.0.1",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.3",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  },
  "dependencies": {
    "@material-ui/core": "^4.6.0",
    "@material-ui/icons": "^4.5.1",
    "axios": "^0.19.0",
    "normalize.css": "^8.0.1",
    "react": "^16.11.0",
    "react-dom": "^16.11.0",
    "@types/material-ui": "^0.21.7",
    "@types/react": "^16.9.16"
  }
}

If you download the source archive for this book, that’s what you’ll find, and you can just do an npm install at this point to get everything installed. Otherwise, just overwrite this file with this content (and this is true of the remaining configuration files we’re about to discuss).

As you can see, TypeScript is installed, and then too is Webpack. Also, several Webpack loaders and plugins are installed (and some of those results in some additional dependencies being automatically added by NPM).

Note

I also added a script that can be used to execute a build, so now npm run build is all we need to do to kick off the build. Also note that Axios didn’t have type bindings at the time of this writing and normalize.css isn’t a library of code, it’s a stylesheet, so typing doesn’t factor into it.

We’ll get back to Webpack in just a moment, but this effectively takes care of the development dependencies. But what about runtime dependencies? Well, I’ve also installed those as well, and they are as follows:
  • React – Of course!

  • normalize.css – A CSS reset to ensure we start with a consistent client-side environment across browsers as far as styles go

  • Axios – A library for server communications (more on this later)

  • Material-UI – A library providing UI widgets to build our UI with based on Google’s Material guidelines (more on this later too)

Now, when TypeScript was installed, we got a default tsconfig.json file, and that needs to be modified a bit also. Again, I’ll just show you what the final file should look like (remember that you will have other content commented out since the tsc -init will produce it, but this is the content that should be enabled with the appropriate values):
{
  "compilerOptions" : {
    "esModuleInterop" : true,
    "sourceMap" : true,
    "noImplicitAny" : false,
    "module" : "commonjs",
    "target" : "es6",
    "lib" : [ "es2015", "es2017", "dom" ],
    "removeComments" : true,
    "jsx" : "react",
    "allowJs" : true,
    "baseUrl" : "./",
    "paths" : { "components/*" : [ "src/components/*" ] }
  }
}
So far, these are the same basic steps that were done when building the server. But, unlike the server, there are some additional installation and configuration steps we must do because the client will be using Webpack. It will have been installed at this point, but we must initialize this project with Webpack too:
npx webpack init
That creates a default webpack.config.js file. As an aside, you also should go ahead and delete the .yo-rc.json file that’s created since it won’t be needed (you can keep the README.md file if you want though). For our purposes here, the answers you give during this step won’t matter because we’re just going to overwrite the file with the following:
const HtmlWebPackPlugin = require("html-webpack-plugin");
module.exports = {
  entry : "./src/code/main.tsx",
  resolve : { extensions : [ ".ts", ".tsx", ".js" ] },
  module : {
    rules : [
      { test : /.html$/, use : { loader : "html-loader" } },
      { test : /.css$/,
        use : [ "style-loader", "css-loader"] },
      { test : /.tsx?$/, loader: 'awesome-typescript-loader' }
    ]
  },
  plugins : [
    new HtmlWebPackPlugin({ template : "./src/index.html",
      filename : "./index.html" })
  ],
  performance : { hints : false },
  watch : true, devtool : "source-map"
};

You can see the rules defined for handling HTML, CSS, and TSX files, using the loaders installed before.

The HtmlWebPackPlugin has a particular purpose. We tell Webpack what HTML file in our source code to start with via the entry attribute, and it then modifies it as needed (including adding a proper module loader) so that our app can be launched after Webpack has transformed it. This plug is responsible for that.

The performance attribute is necessary because, by default, Webpack will produce a warning or error, depending on various factors, if the final bundle is over 250Kb. Setting performance : { hints : false } disables this behavior.

Setting watch:true serves much the same purpose as the scripts entries in the server did: Webpack will watch our source files and automatically rebuild the client if any files change. That gives us that nice, fast turnaround for changes we so liked when working with the server code, but we get it “for free” with Webpack just by adding this attribute!

Finally, devtool set to "source-map" ensures that a source map is created for the final bundle, allowing us to do some debugging when necessary.

Source File Rundown

Of course, you know we’re dealing with files in the client directory and, more precisely, in the src directory . Like the server code, the client code is laid out the same fundamental way. Critically, the final “executable” client code winds up in the dist directory (which would have been created when the Webpack initialization step was done, but you can, of course, add it manually if you skipped that step). But let’s talk about the files that make up this application in the src directory (and the subdirectories in it):
  • src/index.html – The main entry point of the application.

  • src/css/main.css – A regular old CSS file with some shared styles.

  • src/code/main.ts – The main code entry point. This is where React will begin to build our UI from.

  • src/code/Contacts.ts – Like the server, this is the file that contains a Worker class for dealing with contacts (this is what talks to the server side of MailBag for contacts).

  • src/code/IMAP.ts – Like Contacts.ts, this contains a Worker for performing all the IMAP functions, in conjunction with the server.

  • src/code/SMTP.ts – Just like IMAP.ts, but for the SMTP (send) functionality.

  • src/code/config.ts – A simple configuration file that will contain information about the server component and your email address.

  • src/code/state.ts – This is where most of the action of the app actually is, but we’ll get to that in due time!

  • src/code/components/BaseLayout.tsx – A React component that houses all others.

  • src/code/components/Toolbar.tsx – A React component that is the toolbar.

  • src/code/components/MaliboxList.tsx – A React component that is the list of mailboxes on the left.

  • src/code/components/ContactList.tsx – A React component that is the list of contacts on the right.

  • src/code/components/MessageList.tsx – A React component that is a list of messages in a selected mailbox.

  • src/code/components/WelcomeView.tsx – A React component that serves as just a simple splash screen when the app starts up or when certain operations occur.

  • src/code/components/ContactView.tsx – A React component that is the presentation of the contact view when a contact is selected or a new one is being created.

  • src/code/components/MessageView.tsx – A React component that is the presentation of a message or when a message is being composed.

As always, we’ll be examining each of these in turn, but I think that gives you a good rundown of what code we’ll be looking at. It seems like a lot of files, but really, most of them are very small.

The Starting Point: index.html

This is where it all begins: the entry point to the application:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>MailBag</title>
  </head>
  <body></body>
</html>

Well, that’s kind of anticlimactic, isn’t it? Remember earlier when I said that Webpack will transform this file as appropriate to make the app work. Because of that, there’s not much for us to do here, so it’s a nice, simple HTML document with no real content. It’s a little weird that we don’t even import any JavaScript because obviously, that must happen at some point, or else MailBag isn’t going to do much! But that’s what Webpack takes care of for us, so we don’t even have to think about that much.

The Starting Point, Redux: main.tsx

Okay, now it’s time to get to some real code! The first file to look at is main.tsx, which is the main entry point code-wise. This is where React will start to execute our application from. Given the .tsx extension, we know that this is a JSX file that is written in TypeScript. And it begins simply enough:

Note

For the remainder of this chapter, to save some trees, I’ve removed all imports except where they are something unique, as is the case here.

import "normalize.css";
import "../css/main.css";

It may seem a bit odd to see CSS files imported into a code file like this, but it’s okay because JSX and the TypeScript compiler (and Webpack) know how to deal with that on our behalf. That’s also why index.html doesn’t have them imported, as you’d normally do.

Now, as far as what those files are, normalize.css is a CSS reset, meaning that it normalizes initial style conditions on a page across browsers. It takes care of ensuring every browser has the same padding around a given HTML element, for example, or that the margins on the document itself are consistent across browsers. All of that stuff can be different from browser to browser, so using a reset helps ensure that the styles our app uses, which are in the main.css file, are applied on a stable, consistent foundation. You don’t use any styles in normalize.css yourself directly; they’re strictly something that gets applied automatically before your app’s styles do (which is why normalize.css must be imported first, by the way).

With the imports out of the way, it’s time for some code:
const baseComponent = ReactDOM.render(
  <BaseLayout />, document.body
);

That kicks off the React portion of the proceedings, rendering the BaseLayout component , which we’ll be looking at soon, into the body of the document. With that, we have an app on the screen!

Before we get there, though, there’s a fair number of other stops this train must make, starting with application state.

A Quick Detour: State’ing the Obvious

Every React application – well, any nontrivial application anyway – is going to need some sort of state, as was discussed in Chapters 3 and 4. Exactly how you maintain this state is a topic of much debate in the React community. I mentioned Redux in Chapter 3. Redux is a way to have a centralized state object that all the components in the application share. It’s a popular approach, but it’s just one of many. And you don’t need to look outside of React itself if you don’t want to because React offers the notion of state by default, and that’s what I did with MailBag. The trick, though, is that to have components sharing that state, you have to push it up the component tree as far as needed.

In other words, recall that React always constructs a component tree. Any tree has a single element at the top with children beneath it, and those children can have children, and so on. So, where you place your state object becomes a question, and you answer it by determining which components in the tree need access to it. You simply find the highest component in the tree that is a parent to all that need it, and that’s where you put your state.

In MailBag, you’re going to find that we have a single component, BaseLayout , that is a parent to all the rest. This is the most logical place to place state then.

However, what is state in MailBag, exactly? Well, state in React is nothing but a JavaScript object. You could define it directly within the BaseLayout.tsx code file, but I wanted to have it be separate, just to organize the code a little cleaner. That’s what the state.ts file is all about. It defines an object that BaseLayout will include, and it’s where all state for the application will live, along with the methods needed to mutate state.

But it’s not quite as simple as defining an object. We have to play some games in order to make this work, which is why this file’s code begins in not quite the way you might expect:
export function createState(inParentComponent) {
Rather than just exporting a literal object, we have a function. This function takes in a reference to the component that contains it, BaseLayout, in our case, for reasons that will become apparent shortly. Then, this function simply returns an object, our state object:
  return {
    pleaseWaitVisible : false,

Our state consists of a series of properties in the object, starting with pleaseWaitVisible. This is a flag that will tell our code whether a “please wait” popup, which we’ll show every time we call the server, is visible or not. More on this later!

We also need to maintain a list of contacts that the user has created, and that’s where the contacts array property comes in:
    contacts : [ ],
Similarly, a list of mailboxes is needed:
    mailboxes : [ ],
And, assuming a mailbox has been selected, we need the list of messages within it:
    messages : [ ],
When the user clicks a mailbox or clicks the NEW MESSAGE or NEW CONTACT button or clicks a contact in the list, what they see in the middle of the screen changes. This we refer to as the “view,” and what view is current must be known for React to render the correct content:
    currentView : "welcome",

It starts out with the "welcome" view and then changes to one of "welcome", "message" (when viewing a message), "compose" (when creating a message), "contact" (when viewing a contact), and "contactAdd" (when adding a contact). How this changes the view is something you’ll see later.

Earlier, I mentioned the array of messages in the currently selected mailbox, but how do we know what the current mailbox is? As it happens, we have a property for that:
    currentMailbox : null,
Then, we must think about what state is necessary when either viewing or creating a message. For that, we have a series of properties:
    messageID : null,
    messageDate : null,
    messageFrom : null,
    messageTo : null,
    messageSubject : null,
    messageBody : null,

I’d imagine those are all obvious. Note that messageID would only ever be populated when viewing an existing message, and it’s the ID of the message on the server.

Similar, when viewing or creating a contact, we’ll need some state too:
    contactID : null,
    contactName : null,
    contactEmail : null,

The state object also contains a collection of methods that the remainder of the application code calls on to mutate state in various ways. These are termed “state mutator methods,” and I’m going to introduce each of those methods as they are first encountered.

So, to wrap this up in a bow, what will happen is that this createState() function will be called at some point inside BaseLayout, and the state object will be returned. That object will then be a member of BaseLayout, and we’re good to go.

Well, mostly… but we’ll get to a small problem next!

Back to the Code!

Now that you have an idea of what state is in play, we can get back to the code in main.tsx:
baseComponent.state.showHidePleaseWait(true);
Once the UI is built, the next task that must be accomplished is to call the server and get a list of mailboxes available for the account and a list of contacts the user has created. Any time we call the server, we’re going to display a “please wait” popup, so the user knows something is happening. This will also serve the purpose of blocking the UI for a moment so that the user can’t go and do something that causes problems while the server works. The showHidePleaseWait() method of the state object, the first of the state mutator methods we’ve encountered, does this for us:
showHidePleaseWait : function(inVisible: boolean): void {
  this.setState({ pleaseWaitVisible : inVisible });
}.bind(inParentComponent)

You must remember that with React, you don’t directly tell components to do things. Instead, you mutate state in some way, using the setState() method on the component that holds the state, which causes React to repaint the pertinent parts of the UI as needed. In this case, to show the please wait popup, all we need to do is update the pleaseWaitVisible state attribute, setting it to true. React will then redraw the UI, and what will happen, something you’ll see later, is that the popup element will now be set to visible. This will make sense when we look at the MainLayout.tsx file, but just keep in mind for now that pleaseWaitVisible is set to true when we want the please wait popup to be shown and false when we want it hidden and that React will see that change in state and redraw the screen as needed. That’s the key thing right now.

The little problem I alluded to earlier is that since we are limited to calling the setState() method on the component that contains the state object, any code that tries to call it must execute within the context of that component. When we define a separate object for state as I did in order to break it out into its own source file, the this reference in the methods inside state won’t be what we need. In other words, any method inside the state object won’t have access to setState() because its execution context, at least in some instances, won’t be the BaseLayout component.

That’s where the bind() statements come in, and you’ll see these on every function in the state object. When the state object is constructed via the call to createState(), a reference to the BaseLayout instance was passed in. That’s what we bind all the mutator functions to. That way, they will always have access to setState() as we need. Note that if any of them needs to touch the state object itself, which they obviously would need to in at least some cases, they can do so by accessing this.state, since components always expose their state via that property.

With the please wait popup showing, we can now call the server:
async function getMailboxes() {
  const imapWorker: IMAP.Worker = new IMAP.Worker();
  const mailboxes: IMAP.IMailbox[] = await imapWorker.listMailboxes();
  mailboxes.forEach((inMailbox) => {
    baseComponent.state.addMailboxToList(inMailbox);
  });
}

We know that getting a list of mailboxes is an IMAP operation from our look at the server code, and since the IMAP Worker class on the client seeks to mimic that API that is exposed by the server to the IMAP Worker class there, it makes sense that we’d be calling IMAP.Worker.listMailboxes() here too. And the code looks almost identical to the endpoint handler function code on the server as a result. We’ll look at the client-side IMAP close a bit later, but I think you’ll find it rather trivial. The bottom line, though, is that we get back an array of mailboxes, and we then iterate them and call the addMailboxToList() method on the state object (which we can do because we have a reference to the BaseLayout component via the baseComponent variable). That will update the mailboxes array in state, causing React to render the screen to show the mailboxes on the left.

And addMailboxToList() method is the next state mutator we’ve hit:
addMailboxToList : function(inMailbox: IMAP.IMailbox): void {
  const cl: IMAP.IMailbox[] = this.state.mailboxes.slice(0);
  cl.push(inMailbox);
  this.setState({ mailboxes : cl });
}.bind(inParentComponent)

First, you have always to remember that when you call setState(), you should never pass references to objects in state. That may sound weird, but it’s easy to understand when dealing with arrays, as we are here. Your first inclination would be to directly push inMailbox into state.mailboxes and then try to call this.setState({this.state.mailboxes}). Everyone tries that at first because it seems reasonable! However, it won’t work because what you pass into setState() replaces what’s in state at the time, and trying to do that with what’s already there… well, let’s just say React won’t like you very much!

Instead, we make a copy of the array via slice(0), then push the new mailbox into that copy, and finally pass that copy to setState(). Now, everything works as expected. Note that you only have to do this sort of copying/updating/setting when dealing with objects and collections.

If you’re paying attention so far, you will have noticed that we haven’t actually called the server to get the list of mailboxes yet, we’ve only defined a function to do so. That’s because the function that calls imapWorker.listMailboxes() must be marked async since we’re await’ing the response. getMailboxes() is marked async, so now we need to call it:
getMailboxes().then(function() {
  async function getContacts() {
    const contactsWorker: Contacts.Worker = new Contacts.Worker();
    const contacts: Contacts.IContact[] = await contactsWorker.listContacts();
    contacts.forEach((inContact) => {
      baseComponent.state.addContactToList(inContact);
    });
  }
  getContacts().then(() =>
    baseComponent.state.showHidePleaseWait(false));
});

We don’t want to get the list of contacts until the list of mailboxes is done so that we know that all server calls are done before the please wait popup is hidden, so we use the then() syntax to chain them. Inside the then() callback, another function is defined, getContacts() this time, for the same reason: async/await usage. Once defined, we can call getContacts() and again use the then() syntax so that we can call showHidePleaseWait(), passing false this time, to cause React to hide the please wait popup.

The addContactToList() state mutator method is used in there, and it’s virtually identical to addMailboxToList():
addContactToList : function(inContact: Contacts.IContact): void {
  const cl = this.state.contacts.slice(0);
  cl.push({ _id : inContact._id,
    name : inContact.name, email : inContact.email });
  this.setState({ contacts : cl });
}.bind(inParentComponent)

In this case, I’ve constructed the object push()’ed into the contacts array explicitly, not for any particular reason other than to show that you can. If you wanted the client contact objects to have different fields than the server-supplied objects for some reason, this is how you can do that translation.

A Bit of Configuration: config.ts

For the client app to talk to the server, it, of course, has to know its address. The config.ts file meets that need:
export const config: {
  serverAddress: string, userEmail: string } =
{ serverAddress : "http://localhost", userEmail : [email protected] };

It’s quite simple: it’s just a literal object with a serverAddress property that gives the address of the MailBag server. During development, this is likely to be localhost. Also, note that it must contain the protocol prefix. This object also contains the userEmail that provides your eMail address. I’m gonna guess that’s not your email address, so go ahead and update it as appropriate! This file will be imported into the Worker classes as needed.

Hey, speaking of Worker classes, that’s exactly what’s up next!

A Worker for All Seasons

Just like the server side of MailBag, we have three “Worker” classes on the client side: Contacts.ts, IMAP.ts, and SMTP.ts. These are the interface between the client application itself and the MailBag server, and they break down functionally in the same way as their server counterparts. Before we look at the React code itself that defines out UI and makes it functional, let’s take a look at these Worker classes so that when you see them being used, you’ll know what they’re doing.

But how, exactly, does the code in these workers talk to the server? For that, we must take a quick detour!

A Quick Detour: AJAX

AJAX is a technique that came to life, so to speak, at the hands of one Jesse James Garrett in an essay he wrote in February 2005. There, he coined the term AJAX, which stands for Asynchronous JavaScript and XML. The interesting thing about AJAX, though, is that it doesn’t have to be asynchronous (but virtually always is), doesn’t have to involve JavaScript (but virtually always does), and doesn’t need to use XML at all (but probably doesn’t 99+% of the time).

AJAX is, at its core, an exceedingly simple and, by no stretch of the imagination, original concept: it is not necessary to refresh the entire contents of a web page for each user interaction, or each “event,” if you will. When the user clicks a button, it is no longer necessary to ask the server to render an entirely new page, as is the case with the “classic” Web. Instead, you can define regions on the page to be updated and have much more fine-grained control over user event handling as a result. No longer are you limited to simply submitting a form to a server for processing or navigating to an entirely new page when a link is clicked.

The interesting thing about AJAX is that it is in no way, shape, or form new, and it actually wasn’t even when Mr. Garrett coined the term. A decade ago, when AJAX was still somewhat new, I liked to say that you could always tell who has done AJAX before and who hadn’t because those who had are mad that it was a big deal and they didn’t get credit for “inventing” it themselves!

Nowadays, the term AJAX isn’t used as much as before. People tend to talk about “out-of-band requests” or simply “asynchronous requests” or, indeed, simply “server requests” because it’s pretty much the de facto way of communicating with a server on the Web when you aren’t refreshing the entire page.

At its core, AJAX works because of something invented originally by Microsoft: the XMLHttpRequest object . This is a JavaScript object that allows you to write code like this:
let req;
let which;
function retrieveURL(url) {
  if (window.XMLHttpRequest) {
    req = new XMLHttpRequest();
    req.onreadystatechange = processStateChange;
    try {
      req.open("GET", url, true);
    } catch (e) {
      alert(e);
    }
    req.send(null);
  } else if (window.ActiveXObject) {
    req = new ActiveXObject("Microsoft.XMLHTTP");
    if (req) {
      req.onreadystatechange = processStateChange;
      req.open("GET", url, true);
      req.send();
    }
  }
}
function processStateChange() {
  if (req.readyState == 4) {
    if (req.status == 200) {
      document.getElementById("urlContent").innerHTML = req.responseText;
    } else {
      alert("Problem: " + req.statusText);
    }
  }
}

Even if this is your first time seeing such code, I bet you can understand it without much trouble. In short, you create an XMLHttpRequest object (branching based on whether the object exists or not, because for a while, not all browsers exposed the object in the same way). You then hook a callback function up to it that will be called whenever the state of the object changes (e.g., when it connects to the server or when the response comes back – the object has an entire lifecycle you can hook into). You give it the URL to connect to, optionally tell it about any data you’re sending (in this case, there is none), and finally, send the request. The callback function will be called, multiple times, in fact, based on the lifecycle events provided. We only care about the readyState 4, which is what occurs when a response comes back. Then, assuming we got an HTTP 200 back, we take the responseText, which is what the server sent, and insert it into a DOM node, presumably a <div>, or do whatever else we want with it. That’s it, that’s all there is to it.

Nowadays, you wouldn’t even write that most likely, and instead, you’d use the newer Fetch API. Although not quite ubiquitous across all browsers, it’s not supported by the majority, so now you can write code like this:
const response = await fetch(url);

Yep, that’s much better, isn’t it?

However, aside from the browser having to support this API, it also must support async/await, as you can see. If you want to reach the widest audience possible, but you don’t want to write all the XMLHttpRequest code as in the preceding text, you’ll probably want to use a capable library that abstracts all of this away from you (and, most likely, provides many other benefits). For MailBag, that’s exactly what we’re going to do!

Getting Some Help: Axios

Rather than doing “naked” AJAX, we’ll instead use a popular library for it instead: Axios (https://github.com/axios/axios). In simplest terms, Axios is a Promise-based HTTP client that works in both browsers and Node. It uses XMLHttpRequest under the covers in a browser and uses the Node http library when used in a Node-based app. Being Promise-based means that you can use async/await with it (or the more “classical” Promise approach), which makes for a very nice API.

Axios offers some more advanced capabilities, including the ability to hook into the request and response cycle to make modifications broadly (think cross-cutting concerns in Aspect-Oriented Programming, or AOP, for things like logging and security). Or, it offers the ability to transform request and response data in various ways automatically and the ability to cancel requests, if necessary.

Using Axios also means security because it includes protection against client-side XSRF, or Cross-Site Request Forgery. This is a trick nefarious sorts can use to transmit requests to the server, masquerading as you, a legitimate user. That’s bad news, obviously, and Axios can keep your application safe from it without doing anything special on your part.

Axios has broad browser support and is as easy to use as
const response = await axios.get("your_server_url");
Or if you don’t want to use async/await (what’s wrong with you?!):
axois.get("your_server_url").then(function(response) {
  // Do something with the response.
});
Do you need to POST some data to the server? No problem:
axios.post("your_server_url",
  { firstName : "Burt", lastName : "Reynolds" }
);

Axios will automatically serialize that object into JSON for transmission to the server (naturally, you can pass an object reference there, it doesn’t need to be an object literal like that). It will also automatically deserialize a JSON response so that you have a nice JavaScript object to play with.

You can use any other HTTP method there too: DELETE, HEAD, OPTIONS, PUT, PATCH, whatever you need, it’s all there for you.

You can optionally pass a configuration object to any of the request methods after the URL (or even in place of the URL if the object itself contains the URL), which allows you to modify the requests in many ways. The options available are numerous, so I won’t go through them all, but a few of particular interest are the following:
  • transformRequest – You provide a function here, and this will allow you to modify the response before it’s passed to the response handler function (or returned to the variable specified in an await call). You can do the same for the request with the transformResponse property.

  • params – You can provide a list of URL parameters to append to the URL with this.

  • timeout – By default, Axios waits forever for a response (well, at least until the browser itself times out). With this option, you can specify how long to wait.

  • proxy – Does your network require you to go through a proxy? If so, you can specify that information with this property.

  • onDownloadProgress – This is a function to be called periodically while a response is downloading, allowing you the ability to show a progress bar or spinner or similar UI element (you can do this with onUploadProgress in the opposite direction too).

Axios is a very robust but extremely simple-to-use library that, for me, is the obvious choice for our server communication needs in MailBag.

Mirroring the Server Part 1: Contacts.ts

The first Worker class we’re going to talk about is in the Contacts.ts file, which, of course, means we’re dealing with the Contacts Worker class. But, even before the Worker class, we find that we have an interface present:
export interface IContact {
  _id?: number, name: string, email: string
}

If you look back at the Contacts.ts file on the server side, you’ll find this same interface. That should make sense to you: after all, we’re passing objects back and forth that need to have the same structure on both sides of the conversation!

After that, the Worker class begins:
export class Worker {

Nothing special there, and again identical to the server. Within the Worker, we find a series of methods, beginning with listContacts().

Listing Contacts

When we want a list of contacts to display on the screen, we need to ask the server for that. We know that, via the Express-based RESTful interface, we ultimately need the Worker.listContacts() method in the Contacts.ts file on the server to be executed, so we mimic that interface design with listContacts() in the client-side Worker class:
public async listContacts(): Promise<IContact[]> {
  const response: AxiosResponse =
    await axios.get(`${config.serverAddress}/contacts`);
  return response.data;
}

Here, you can see Axios used, as we discussed in the previous section. The serverAddress from the config object is used to construct the appropriate path, and this is of course a get request, so that’s the Axios method executed. Then, the response is returned, and we’re done. Very simple, right? But it serves the important purpose architecturally of abstracting away the MailBag client application from the server. Think of it this way: if you wanted to change the server to use an XML-based message exchange, and you didn’t want to use Axios, you’d only need to change the code in this class (and the server, obviously), but the rest of the MailBag client app would be none the wiser. That’s good architectural flexibility.

Note

I’m going to go through the rest of these methods, as well as those in the IMAP and SMTP Worker classes, pretty quickly, because they follow the same pattern, which is to mimic the server. No need to linger, I think! But you should still take the time to examine the code and make sure you do understand what’s happening, simple though it generally is, for each method presented.

Adding a Contact

To add a contact, the aptly named addContact() method is called, accepting an object adhering to the IContact interface:
public async addContact(inContact: IContact):
  Promise<IContact> {
  const response: AxiosResponse = await axios.post(
    `${config.serverAddress}/contacts`, inContact
  );
  return response.data;
}

Once more, it’s a simple Axios call, this time a post(), passing inContact as the second argument. Axios takes care of serializing that to JSON and sending it in the request body for us. We get back the same object but now with the _id field added, so that is returned so the caller can add it to the list of contacts for display (we’ll get into all of that later).

Deleting a Contact

Deleting a contact is the final bit of functionality this Worker must provide:
public async deleteContact(inID): Promise<void> {
  await axios.delete(
    `${config.serverAddress}/contacts/${inID}`);
}

Well, that’s pretty simple, isn’t it? Here, you can see the contact’s ID added to the URL, as per our REST interface server design.

Mirroring the Server Part 2: IMAP.ts

The second Worker to look at is in the IMAP.ts files. Just like with contacts, we begin with an interface:
export interface IMailbox { name: string, path: string }
And also, just like with contacts, this mimics the interface of the same name found in the server version of IMAP.ts. Similarly, we have the same IMessage interface as on the server too:
export interface IMessage {
  id: string, date: string, from: string,subject: string,
  body?: string
}

After that, the Worker class begins, and we have some methods to look at.

Listing Mailboxes

Listing mailboxes is just like listing contacts in terms of the call to the server, and we find a similar listMailboxes() method for it:
public async listMailboxes(): Promise<IMailbox[]> {
  const response: AxiosResponse =
    await axios.get(`${config.serverAddress}/mailboxes`);
  return response.data;
}

Yep, just a slightly different URL to call and different interfaces, but otherwise, the same as listing contacts.

Listing Messages

Listing messages in a mailbox is just a trivial thanks to the listMessages() method :
public async listMessages(inMailbox: string):
  Promise<IMessage[]> {
  const response: AxiosResponse = await axios.get(
    `${config.serverAddress}/mailboxes/${inMailbox}`
  );
  return response.data;
}

Here, we just need the path of the mailbox to get messages for, which we get from the inMailbox argument. Then, it’s just an Axios get() call again.

Getting the Body of a Message

Remember that the server, when sending a list of messages, does not send the message bodies. We only get the body of a selected message when needed, and that’s where getMessageBody() factors in:
public async getMessageBody(inID: string, inMailbox: String):
  Promise<string> {
  const response: AxiosResponse = await axios.get(
    `${config.serverAddress}/messages/${inMailbox}/${inID}`
  );
  return response.data;
}

We need the ID of the message and the path to the mailbox it’s in, but other than that, it’s not substantially different from getting a list of mailboxes.

Deleting a Message

The final method in this Worker is for deleting messages, the deleteMessage() method :
public async deleteMessage(inID: string, inMailbox: String):
  Promise<void> {
  await axios.delete(
    `${config.serverAddress}/messages/${inMailbox}/${inID}`
  );
}

By this point, I’m betting you’re pretty comfortable with these Worker methods and how they interact with the server. There’s nothing that says I had to mimic the basic layout of the server code on the client. I could have had a drastically different structure to these methods here. So long as they eventually call the server as expected, that’s all that matters. But hopefully, you’ll agree that mirroring them like this helps keep it all straight in your mind.

Mirroring the Server Part 3: SMTP.ts

The final worker, the one for the SMTP operation of sending a message, is, of course, found in the SMTP.ts file. In this file, there are no interfaces to deal with, and just a single method, so the entire file is this:
import axios from "axios";
import { config } from "./config";
export class Worker {
  public async sendMessage(
    inTo: string, inFrom: string, inSubject: string,
    inMessage: string
  ): Promise<void> {
    await axios.post(`${config.serverAddress}/messages`, {
      to : inTo, from : inFrom, subject : inSubject,
      text : inMessage
    });
  }
}

I’m going to go out on a limb here and say you probably don’t even need this explained at this point! So, with that assumption, let’s now move on to the true React code that makes use of the Workers we just looked at.

A Cavalcade of Components

To build a web app usually requires a user interface, and a user interface usually requires widgets, or components. Sometimes you use the basic ones that HTML itself provides, and it’s enough. Sometimes, you build your own using those primitives as building blocks. And, sometimes, you find a good library that suits your needs.

For MailBag, we’ll be doing the latter, and the choice I landed on to provide out components is Material-UI.

A Quick Detour: Material-UI

Around 2014, Google realized that most of the web app products were going in different directions in terms of look, feel, and function. Android, too, was a completely different beast visually, and in fact, Android is where the eventual solution began. They determined that this wasn’t a sustainable direction to go and they needed to come up with something to unify their products.

As a result, the Material design language was created (https://material.io). Although it’s not terribly important for our work here, I think a very brief description of Material design itself is in order.

Material design is a set of design principles that are informed by how people interact with real objects in the physical world. Primarily influenced by print media, Material design begins with concepts like sheets of paper and their digital equivalents. When you read a book, you turn pages, and that motion is encapsulated in Material design, just as the underlying pattern of the sheet of paper itself is.

Material design is concerned with how layers of content can slide over each other, for example, and those slides, those animations, are key elements. Everything is intended to be reactive to touch (remember that this all started with Android, at least in its initial implementation, so touch was automatically a key part of Material design).

As I said, knowing this doesn’t make a huge difference in our work here, but a little context never hurt anybody!

Now, what does matter is the Material-UI library itself (https://material-ui.com). Simply put, this is a library of React components built on top of Google’s Material design language. Being a React library means you simply add it to your project with NPM like any other library, import the appropriate parts of it, and use it in your code. For example, if you want to put a button on the screen:
import React from "react";
import Button from "@material-ui/core/Button";
const App = () => (
  <Button variant="contained" color="primary">
    Hello World
  </Button>
);

Now, I’m glossing over something important here, but we’re going to get to it soon (in the section on functional components), but focus on what matters here: React was imported naturally, and too was the Button component from Material-UI. Then, it’s simply a matter of dropping a <Button> tag into the code, and we have ourselves a Material-UI button.

The Material-UI library offers a wealth of components with which to build a user interface. It has all the usual suspects like buttons, checkboxes, radio buttons, drop-down lists, grids, lists, progress bars, alert dialogs, menus, and a lot more. The web site referenced in the preceding text does a great job of presenting it all. You’ll find a list of components with numerous simple examples for each and then links for the API of each that details the properties and options available.

It really is an easy-to-use library that also makes your apps look and function great with minimal effort on your part. You’ll only see a small portion of what it has to offer in MailBag, so I highly encourage you to spend some time on the Material-UI web site exploring all it has to offer. I think you’ll be very impressed, and it will, I bet, quickly become your favorite collection of widgets for building UIs with React.

Another Quick Detour: CSS Grid

One of the first questions you must answer when developing a web app is how you’re going to lay out your components. There are lots of methods, each with their own pluses and minuses. But, given that we’re trying to use relatively modern techniques in this book, we’re going to go with the newest darling on the street: CSS Grid.

Virtually any web page layout can be described in terms of a grid (in fact, I’m not sure there’s any that can’t be). It’s all just columns and rows in the end, whether there’s only one of each (read: just a single block of content) or whether there are nested grids inside nested grids. All of it just rows and columns.

Let’s start with some simple markup (Listing 9-1).
<html>
  <head>
    <title></title>
  </head>
  <body>
    <div class="container">
      <div style="background-color:#ff0000;">A</div>
      <div style="background-color:#00ff00;">B</div>
      <div style="background-color:#0000ff;">C</div>
      <div style="background-color:#ff00ff;">D</div>
      <div style="background-color:#ffff00;">E</div>
      <div style="background-color:#00ffff;">F</div>
    </div>
  </body>
</html>
Listing 9-1

CSS Grid example markup

When loaded in your browser, you’ll simply see six rows of content, stacked one right on top of the other, as shown in Figure 9-5.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig5_HTML.jpg
Figure 9-5

You can’t see the colors on the printed page, but trust me, they’re there!

To introduce CSS Grid to the mix, in order to create a more interesting layout, we start with a container element, which we have here. Then, on this element, you define the grid, that is, the rows and columns contained within the grid. The container element already has a class attribute, so we just need to define that style rule:
.container {
  display : grid;
  grid-template-columns : 150px 50px 100px;
  grid-template-rows : 100px 100px;
}
Now, if you reload the page, you’ll see two rows of items with three columns in each, as shown in Figure 9-6. The six <div> elements get dropped into each of the areas defined by the intersection of the rows and columns.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig6_HTML.jpg
Figure 9-6

Our beautiful, beautiful grid!

What’s even better is when you start to add some styling to one or more <div>’s to tell them where in the grid they should live. For example, let’s add some style to element E:
<div style="background-color:#ffff00;grid-column:1/4;grid-row:1/1;">E</div>
Now, reload the page, and you’ll find that the first <div> shown, E, extends across the entire grid, as seen in Figure 9-7. The meaning of the grid-column and grid-row attributes is that it tells the grid what columns and rows the element should cover, but it does so using the grid lines, not the boxes that make up the grid.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig7_HTML.jpg
Figure 9-7

A more interesting grid (well, I think so at least!)

In other words, when you have three columns, you have four grid lines: the two between the three columns, of course, and the one before the first column and after the last one. So, here we’re saying that this <div> should stretch from that first grid line to the fourth one, the last one, which results in it covering the entire row. Figure 9-8 should, expect, make this all clear.
../images/491030_1_En_9_Chapter/491030_1_En_9_Fig8_HTML.jpg
Figure 9-8

The numbers across the top are the column lines; the ones down the side are the row lines

Also, because <div> E now takes up the entire first row, <div>’s A, B, and C use the second row, which pushes D and F down. Sometimes, it takes a little tweaking to get your grid to work precisely the way you want it to, but when you do, you’ll find that laying out pages is a piece of cake with CSS Grid.

It’s also important to realize that you can nest grid. So say we want to have some grid layout within <div> B, we can absolutely do that, and you do it in exactly the same way: apply the display:grid style to a container <div> (along with the grid-template-columns and grid-template-rows as appropriate) inside <div> B and then define the grid in exactly the same way, but now that grid will be constrained to <div> B. And it’s exactly this nesting that will allow us to lay out the MailBag UI as shown earlier, as you’ll see in action shortly.

Yet Another Quick Detour: main.css

In the source code for this app, you’ll find the main.css file in the src/css directory. This is an ordinary CSS stylesheet, absolutely nothing special about it. Usually, I would go through it here, but I think it will make much more sense to look at the relatively small handful of CSS rules it contains in context as we go through the various components.

So, that’s precisely what I’m going to do! And, given that, we can move on to the code of BaseLayout.tsx.

BaseLayout.tsx

Earlier, I mentioned that a single React component will be the parent to all the others, as is always the case, and now it’s time to look at it: BaseLayout , as found in BaseLayout.tsx:
class BaseLayout extends Component {
First, we start by creating a class that extends React’s Component class. Next, we have to get the state into the component, as discussed earlier:
state = createState(this);

Hopefully, you’re having a “Eureka!” moment right about now. This is where createState() is called, and a reference to the BaseLayout instance itself is passed in so that all the state mutator methods can be bound to it.

After that, we come to the one method we know must be present: render():
render() {
  return (
    <div className="appContainer">
Remember that we’re going to be doing the layout of the UI via CSS Grid, so that means that whatever element is at the very top of the page must be our container element, and so it is: a <div> with the appContainer class applied. That class, found in main.css (as they all are), is as follows:
.appContainer {
  display : grid;
  grid-template-columns : 150px 1fr 240px;
  grid-template-rows : 50px 1fr;
  width : 100vw;
  height : 100vh;
}

That’s just like the example from earlier as far as defining the grid goes, but now you see width and height set using vw and vh units, respectively. The v in those units stands for viewport. So what we’re really saying is “make this container element 100% of the width and height of the viewport.” In other words, our grid fills the screen, which, of course, is what we want for MailBag (in contrast to the earlier CSS Grid examples where that wasn’t the case, the grid used only as much space as was specified in its style definition).

Next, we need to define the please wait popup, and for it, we’ll use a Dialog component, supplied by Material-UI:
<Dialog open={ this.state.pleaseWaitVisible }
  disableBackdropClick={ true }
  disableEscapeKeyDown={ true }
  transitionDuration={ 0 }>
  <DialogTitle style={{ textAlign:"center" }}>
    Please Wait
  </DialogTitle>
  <DialogContent><DialogContentText>
    ...Contacting server...
  </DialogContentText></DialogContent>
</Dialog>

It may seem odd that something like a popup dialog, something that is transient, actually exists at all times and is just hidden and shown, but that’s a common thing with React. Here, the open prop gets its value from the pleaseWaitVisible property in state, which you’ll recall from earlier I said to keep in mind. When the hideShowPleaseWait() state mutator method is called, that property gets changed, and React notices that. So it will re-render the page, now either hiding or showing this dialog based on the changed prop value.

We don’t want this dialog to be dismissible by the user, which it would be by default. So, the disableBackdropClick prop is set to true, which stops the user from clicking outside of the dialog to dismiss. Additionally, disableEscapeKeyDown is also set to true to prevent them from pressing the ESC key to dismiss it. I also set transitionDuration to 0 so that the animation that it normally does by default doesn’t occur, since the call to the server, in some cases, could actually be faster than the transition itself! Indeed, it’s expected that, in most cases, this dialog will appear and disappear so fast that the user will just see a flash. But, for times when the network is a bit slower, it’s nice to have it there.

The actual Dialog has as a child a DialogTitle component, and here I set style so that its text is centered (which I just thought looked better) and a DialogContent child, which itself has a DialogContentText child. This is simply the text displayed in the Dialog itself. As an aside, the Material-UI Dialog component is much more robust and can have buttons you define, or even input elements, virtually anything you can imagine can wind up in a Dialog. It doesn’t need to be simple static text as here, but since that’s all we need, that’s all it is in this case.

With the please wait popup defined, it’s finally time to start defining the UI that is always visible, starting with the toolbar:
<div className="toolbar">
  <Toolbar state={ this.state } />
</div>

Notice the pattern here, which you’ll see throughout this component: state is passed down to the Toolbar component (the definition of which we’ll see in the next section). Every component that is a child to BaseLayout, save for one (WelcomeView), needs state, and they all get it the same way. That’s the other half of what I was talking about earlier in terms of choosing the right component to contain state: just putting the state in a component higher up the component tree doesn’t do much unless you pass that state to child components as needed.

Now, as far as the toolbar style rule goes, that’s as follows:
.toolbar {
  grid-column : 1 / 4;
  grid-row : 1 / 1;
  border-bottom : 1px solid #e0e0e0;
  padding-top : 8px;
  padding-left : 4px;
}

As you can see, we want the toolbar to stretch across the three columns of the grid, just like in the earlier example. I also add some padding to keep the buttons from touching the edges of the screen on the left and top (again, it just looks better to my eyes that way). I also added a border on the bottom to give some separation from the rest of the UI contents (this, too, is a pattern repeated throughout).

Next, we need our list of mailboxes on the left:
<div className="mailboxList">
  <MailboxList state={ this.state } />
</div>
Hopefully, you’re seeing the pattern now! Ignoring for the moment what the MailboxList component is and how it’s implemented, the basic idea is the same as for the Toolbar. The mailboxList style rule BaseLayout is
.mailboxList {
  grid-column : 1 / 1;
  grid-row : 2 / 2;
  border-right : 1px solid #e0e0e0;
  padding-top : 6px;
  padding-left : 4px;
  overflow : auto;
}

That’s not much different from the toolbar style rule either, with the exception of overflow set to auto to ensure that the list can scroll if there are more mailboxes than the vertical space allows for.

Next up is the area of the screen below the toolbar and in between the mailbox list on the left and contact list on the right – the center area!
<div className="centerArea">
  <div className="messageList">
    <MessageList state={ this.state } />
  </div>
This is a situation where we’re going to be nesting CSS Grids, so while the outer <div> is the content of the second column in the second row of the top-level grid, we also need the contents within this <div> to have its own grid structure. The centerArea style rule defines that:
.centerArea {
  display : grid;
  grid-template-rows : 200px 1fr;
}
Here, we only need two rows, we don’t need any columns, so it’s a simpler definition. The first row in this sub-grid, so to speak, is the MessageList component, our list of messages in the selected mailbox in other words. So, we need to place it in this sub-grid, and the messageList style rule does that:
.messageList {
  border-bottom : 1px solid #e0e0e0;
  grid-row : 1 / 1;
  overflow : auto;
}

Pretty simple, right? Once again, we need this area to scroll, since a message list can be arbitrarily long, so overflow is again set to auto.

Next up is the second row of this sub-grid, and this is what I’ve termed the “view area.” It’s where the contact and message views will go, plus the welcome view. Which is showing depends on the state of the app, so we’re going to need some logic for React to determine which to render:
<div className="centerViews">
  { this.state.currentView === "welcome" && <WelcomeView /> }
  { (this.state.currentView === "message" ||
    this.state.currentView === "compose") &&
    <MessageView state={ this.state } />
  }
  { (this.state.currentView === "contact" ||
    this.state.currentView === "contactAdd") &&
    <ContactView state={ this.state } />
  }
</div>
Here is a bit of a trick that is very common in React code: if you want to conditionally render or not render a component, usually based on some value in state, you can use the general syntax:
{ <some_variable> === <some_value> && <some_component> }

Remember that { } denotes an expression in JSX syntax. Also remember that in JavaScript, && is a short-circuit boolean and operation. That means that whatever is on the right of it will only be evaluated if what’s on the left is true. Therefore, here, if the boolean outcome on the left is false, then the component won’t be rendered because it won’t even be evaluated.

So, for the <WelcomeView> component to render, for example, the currentView property in state must equal "welcome". If it doesn’t, then React won’t render that component. For the MessageView component to render, currentView must be "message" or "compose" (the view does double duty for both of those, as you’ll see later). Finally, for ContactView to render, currentView must be "contact" or "contactAdd". In the end, we’ll only ever get one of these components rendered as a result, so only one view will ever be shown, and changing currentView in state will allow us to flip between these views. Pretty nifty, right?

What about the centerViews style rule? That’s easy:
.centerViews {
  grid-row : 2 / 2;
  padding-top : 4px;
  padding-left : 4px;
  padding-right : 4px;
  overflow : auto;
}

That places this content in the second row of the sub-grid and provides some padding about it to avoid any of its content bunching up on the edges.

Finally, we have only one more component to place, and that’s the list of contacts on the right:
<div className="contactList">
  <ContactList state={ this.state } />
</div>
And, of course, its associated style rule:
.contactList {
  grid-column : 3 / 3;
  grid-row : 2 / 2;
  border-left : 1px solid #e0e0e0;
  padding-top : 4px;
  padding-left : 4px;
  overflow : auto;
}
Finally, of course, we need to export this component:
export default BaseLayout;

And with that, our BaseLayout component is done! Now, let’s look at the individual components that we just saw used – well, right after one last quick detour, that is!

Note

Each of the remaining components has a single export at the end, just like BaseLayout, but I’m going to skip showing you that over and over again.

A Quick Detour: Functional Components

In the previous Material-UI example, I dropped a little bit of coolness on you out of the blue, something called functional React components. In the past, and with BaseLayout, you’ve seen components defined this way:
class Welcome extends React.Component {
  render() { return <h1>Hello, { this.props.name }</h1>; }
}
Sometimes Component is written React.Component, but it’s the same thing, it’s just a question of how your imports are done. Either way, what’s important to remember, though, is that, fundamentally, a React component is just an object with a render() method at minimum. And recall that the syntax you see here is JSX. Because of that, you can also define this component in a more concise form:
const Welcome = () => ( <h1>Hello, { this.props.name }</h1> );

Using functional syntax like this works because JSX is aware of it and can process it. It’s clearly not valid JavaScript because if you tried to have HTML embedded in a function like that, it simply wouldn’t work. But JSX allows you to do just that. A valid component will be created, and the HTML you see there, the component tree it represents to be more precise, will be returned from an automatically created render() method.

Why would you choose the class-based approach over the functional approach? There are two primary reasons, beyond whether you prefer the brevity of the functional code or not: lifecycle event needs and state needs. If you need access to lifecycle events, then you can’t use the functional approach – at least, not without introducing the topics of Hooks, which is something that was added a little later in React, and something I’m not covering in this book because it’s a bit of a more advanced topic and not a necessary component of React coding. The same is true of state: if you need your component to have its own state, then you’ll need to use the class-based approach (but again, Hooks provides a solution here too).

However, note that with state, it’s possible to hoist the state up to a component higher in the component tree and then pass it into the component. So, in a sense, the state limitation is no limitation at all, even before you get into Hooks. You’ll be seeing exactly this approach in MailBag because the state object discussed earlier is used in exactly this fashion.

Toolbar.tsx

Now, back to our components, beginning with the Toolbar component , found in the Toolbar.tsx file, which is defined in a functional way just described:
const Toolbar = ({ state }) => (
  <div>
    <Button variant="contained" color="primary"
      size="small" style={{ marginRight:10 }}
      onClick={ () => state.showComposeMessage("new") } >
      <NewMessageIcon style={{ marginRight:10 }} />New Message
    </Button>
    <Button variant="contained" color="primary" size="small"
      style={{ marginRight:10 }}
      onClick={ state.showAddContact } >
      <NewContactIcon style={{ marginRight:10 }} />New Contact
    </Button>
  </div>
);

At a high level, it’s a simple component: it just has two Button components in it, courtesy of the Material-UI library. The Button component has several options to change how it looks and functions, and here I’ve used its variant=contained” to make it look like a more traditional button rather than the text-only buttons that are common on Android (it has elevation and fill in this form, to be more precise). I also set the color to primary, which, by default, will give it a blue appearance. The size=small” prop makes the buttons smaller than usual, to gain a bit more space in the UI. For the NEW MESSAGE button, I also added a margin on the right so that there would be separation between it and the NEW CONTACT button.

Within each Button is an icon, also provided by Material-UI. The library contains numerous icons, and most have their own components, as NewMessageIcon and NewContactIcon do. For each, I also again add margin on the right so that the icon has space between it and the text. And, speaking of text, that’s the second child of each Button, just literally static text.

A button without some sort of click handler code wouldn’t be of much use, so both buttons have such a handler hook up to it through the onClick prop . In the case of the NEW MESSAGE button, there is an anonymous function defined, and from it, state.showComposeMessage() is called. This is necessary because the "new" value needs to be passed to it in order to indicate that the MessageView component, which we’ll look at later, should display in the new message composition mode. When the user clicks a message in the message list, by contrast, the same view is shown, but there will be different fields and buttons, and what is passed to showComposeMessage() determines all that.

In fact, here is that method now:
showComposeMessage : function(inType: string): void {
  switch (inType) {
    case "new":
      this.setState({ currentView : "compose",
        messageTo : "", messageSubject : "", messageBody : "",
        messageFrom : config.userEmail
      });
    break;
    case "reply":
      this.setState({ currentView : "compose",
        messageTo : this.state.messageFrom,
        messageSubject : `Re: ${this.state.messageSubject}`,
        messageBody : messageFrom : config.userEmail,
          ` ---- Original Message ----
            ${this.state.messageBody}`
      });
    break;
    case "contact":
      this.setState({ currentView : "compose",
        messageTo : this.state.contactEmail,
        messageSubject : "", messageBody : "",
        messageFrom : config.userEmail
      });
    break;
  }
}.bind(inParentComponent)

As you can see, there are three possibilities. The first, for a value of "new", is when the user wants to compose a brand-new message. In all three cases, the goal is to set currentView in state so that the appropriate view is shown ("compose" in all cases) and to set up any state properties as necessary. For the "new" case, that means clearing out the messageTo, messageSubject, messageBody, and messageFrom fields. The first three will be populated by the user when they enter the values on the compose view (something you’ll see a bit later), and messageFrom is set from what’s in the config object.

Second, when the user clicks the REPLY button when viewing a message, it’s still ultimately the "compose" view we want to go to, but now we pre-fill the messageTo, messageSubject, and messageBody variables in state.

Third, the "contact" case is when the user clicks the Send Email button when viewing a contact. It’s just like the "reply" state except that the messageTo property comes from the contact itself while messageSubject and messageBody are blanked out.

Now, going back to our Toolbar, the second button, NEW CONTACT, doesn’t need to do this sort of logic. As a result, there’s no need for an anonymous function like for NEW MESSAGE. Instead, we can reference the state.showAddContact() method directly, and that method is
showAddContact : function(): void {
  this.setState({ currentView : "contactAdd",
  contactID : null, contactName : "", contactEmail : "" });
}.bind(inParentComponent)

Once again, it’s just a case of setting currentView appropriately and clearing out any state properties that are involved in user input. In this case, the contact won’t have an ID until we save it to the server, so contactID is null, while contactName and contactEmail start off as empty strings, which will be reflected initially in the text fields that the user will enter the values in.

Note

For the remainder of these components, I’m just going to show the content of the function, since that’s all that’s different in them, aside from the obvious of the component’s name and export.

MailboxList.tsx

Next, we’ll look at the MailboxList component from the MailboxList.tsx file:
<List>
  state.mailboxes.map(value => {
    return (
      <Chip label={ `${value.name}` } onClick={ () =>
        state.setCurrentMailbox(value.path)
      }
        style={{ width:128, marginBottom:10 }}
        color={ state.currentMailbox === value.path ?
          "secondary" : "primary" } />
    );
   } ) }
</List>

It all begins with a Material-UI List component. Material-UI has several components for displaying collections of data in various ways, and List is one of them, and perhaps the most used. Within it, you have one or more child components, one per item in your list.

The list of mailboxes is, of course, in the mailboxes array property of the state object, so we use the map() function on that array to process each item. For each element, a Chip component is returned, which is again a component from Material-UI. A Chip is a lot like a Button, and in fact, for how it’s used here, a Button could have been just as good. Typically, Chips are used to represent things like contacts, but there are no real rules for their use. They look a bit different than buttons, which is why I went with it (that, and just to show you a different component!) But, at the end of the day, each Chip has some label text, which is the name of the mailbox, some styling to set its size and ensure spacing between them, and color. The color prop is interesting because we want the currently selected mailbox to be a different color. The state.currentMailbox stores the path of the currently selected mailbox, so we can do some logic to set the color. If the path of the element of the array being processed is the same as currentMailbox , then the secondary color will be used, which is red. Otherwise, the primary color (blue) will be used. As long as currentMailbox changes when a mailbox Chip is clicked, then we’ll get the highlighting of the current mailbox as we want.

And speaking of clicking a Chip, the onClick handler is defined pointing to state.setCurrentMailbox() for that purpose. Again, because we need to pass the path of the clicked mailbox to that method, we need to use an anonymous function to call state.setCurrentMailbox(), the code for which is

Note

If you tried to set onClick={ state.setCurrentMailbox } to avoid the anonymous function, then what would be passed to it would be an Event object. While it might be possible to drill down into that object to get at the path of the mailbox, using the anonymous function approach decouples the code a little bit from React or even the browser event model in general and, in my mind, makes what’s happening clearer.

setCurrentMailbox : function(inPath: String): void {
  this.setState({ currentView : "welcome",
    currentMailbox : inPath });
  this.state.getMessages(inPath);
}.bind(inParentComponent)

Sure enough, we can see that currentMailbox is indeed set, which highlights it once React sees that state change and re-renders the appropriate part of the component tree. The currentView is also changed to "welcome" because until the user selects a message, there’s nothing to show in the view area, and any time that situation arises, I defaulted it to the welcome view.

The other thing that should occur at this point is that the list of messages in the mailbox, if any, should be retrieved. This is accomplished by the getMessages() method:
getMessages : async function(inPath: string): Promise<void> {
  this.state.showHidePleaseWait(true);
  const imapWorker: IMAP.Worker = new IMAP.Worker();
  const messages: IMAP.IMessage[] = await imapWorker.listMessages(inPath);
  this.state.showHidePleaseWait(false);
  this.state.clearMessages();
  messages.forEach((inMessage: IMAP.IMessage) => {
    this.state.addMessageToList(inMessage);
  });
}.bind(inParentComponent)
Note that this also means that the user can click the current mailbox Chip any time they like to refresh the list. The getMessages() method first shows the please wait dialog, then uses the IMAP.Worker class to call the server for the list of messages. Once returned, the first thing to do is to clear any current list of messages, which is where the clearMessages() method comes in:
clearMessages : function(): void {
  this.setState({ messages : [ ] });
}.bind(inParentComponent)
It’s a simple matter of setting a blank array into state for the messages property. Then, for each mailbox object returned, it is added to the list of mailboxes just cleared, via a call to addMessagestoList():
addMessageToList : function(inMessage: IMAP.IMessage): void {
  const cl = this.state.messages.slice(0);
  cl.push({ id : inMessage.id, date : inMessage.date,
    from : inMessage.from, subject : inMessage.subject });
  this.setState({ messages : cl });
}.bind(inParentComponent)

As you’ve seen before, the list of mailboxes is copied first, then a new item pushed into it, an object constructed from the object passed in. Once again, just adding inMessage to the array would be okay, but doing it this way provides more flexibility it we want to alter the object at all. Finally, the updated array is passed to setState(), and React does its thing to update the list of messages.

ContactList.tsx

While it might seem most logical at this point to jump into the message list and message view code, I want to save those for a little later just because it’s where most of the complexity (relatively speaking) is. Instead, I want to hit on the two contact-related pieces of code first, starting with the ContactList component itself, in the ContactList.tsx file , which is the list of contacts on the right-hand side of the screen:
<List>
  {state.contacts.map(value => {
    return (
      <ListItem key={ value } button onClick={ () =>
        state.showContact(value._id,
          value.name, value.email
        )
      }>
        <ListItemAvatar>
          <Avatar><Person /></Avatar>
        </ListItemAvatar>
        <ListItemText primary={ `${value.name}` } />
      </ListItem>
    );
  })}
</List>

As with the mailbox list, we have a List component. But, unlike the mailbox list, which just had a series of Chip components as children, here, we’re going to use some components that are more typically used within a List. Here, you have one or more child components of type ListItem. In this case, the items come from the array of contacts in state, so like with mailboxes, map() is used to iterate them. For each, a ListItem is created. Every ListItem must have a unique key, though what the value is isn’t something List defines – it’s up to us. So, in this case, I simply make it the next contact object from the contacts array itself. Then, for each, an onClick handler prop is attached that calls state.showContact(), passing it the unique ID of the contact along with the name and email properties.

The showContact() method is this:
showContact : function(inID: string, inName: string, inEmail: string): void {
  this.setState({ currentView : "contact", contactID : inID,
    contactName : inName, contactEmail : inEmail });
}.bind(inParentComponent)

As you can see, currentView is set to "contact" and the three values passed in are set in the corresponding state properties. That way, when the contact view is shown, which we’ll be looking at next, the contact’s name and email address are showing.

Backing up a bit, the List component and its child ListItem components don’t define what the look of an individual item in the list is. You are free to do whatever you like. So, I use another Material-UI component, ListItemAvatar, which is used to display an avatar, usually a small image of some sort, for each contact. ListItemAvatar demands an Avatar child that is the image to display. If we had real avatar images for each contact, we could insert it here (hint: that’d make a good suggested exercise!). But, here, I make each contact have the same image, using the Person icon that Material-UI supplies. Finally, I want to put the name of the contact next to the image, so for that, we add a ListItemText component. The primary prop is the text to show (there is also a secondary prop you could use to, perhaps, show the email address below the name).

ContactView.tsx

When a contact in the contact list is clicked or the NEW CONTACT button in the toolbar is clicked, the contact view is shown in the view area. This content is provided by the ContactView component in the ContactView.tsx file, and it begins thusly (“thusly”? Who talks like that?!):
<form>

Strictly speaking, the form isn’t necessary. But remember that render() must always return a single element (and that doesn’t change when using the functional component approach – the code here still winds up in a render() method ), whether it has children or not. While a <div> would work just as well here, I thought <form>, given its children, made sense.

For its children, we begin with a Material-UI TextField :
<TextField margin="dense" id="contactName" label="Name"
  value={ state.contactName } variant="outlined"
  InputProps={{ style : { color : "#000000" } }}
  disabled={ state.currentView === "contact" }
  style={{ width:260 }}
  onChange={ state.fieldChangeHandler } />

As the name implies, the TextField allows users to enter text. Here, we’re looking for the contact’s name. The margin prop set to dense reduces the space around the field, purely a visual choice in this instance. Similarly, setting variant to outlined results in the field having the border around it, which I felt looked better, rather than being just a single line, which is the default look. The label prop is, of course, the text that tells you what field this is (and which moves above the field from inside it as soon as you start typing, which is a nice Material-ish thing to do). The value comes from state, just like you’d expect, whether it’s an empty string in the case of creating a new contact or the contact’s name when selecting one from the list. The InputProps prop is used to style the underlying HTML <input> element, which is what provides the base functionality that Material-UI then builds upon. The problem here is that when a TextField is disabled, which is done by setting the disabled prop (which here is true when state.currentView is "contact" because that’s what it will be when the user clicked a contact in the list and so is viewing the contact in a non-editable mode), the text is a gray color. That makes it hard to read. So, by explicitly setting the color to black (#000000), that problem is solved, and it remains readable. I also set an explicit width for the field, one large enough to support the maximum characters allowed.

You may be wondering (a) why I skipped the id prop and (b) how the field knows what the maximum length is, because it’s not defined here. The answer is in the onChange handler that’s attached:
fieldChangeHandler : function(inEvent: any): void {
  if (inEvent.target.id === "contactName" &&
    inEvent.target.value.length > 16) { return; }
  this.setState({ [inEvent.target.id] :
    inEvent.target.value });
}.bind(inParentComponent)
<br />

This handler is used on all the editable fields in MailBag, in fact, and you can see where the id prop matters here. If you recall from earlier when discussing the mailbox list, I mentioned that if you don’t use an anonymous function in the event handler and instead reference the state mutator method directly, then you’ll wind up getting an event object passed in. For that situation, that was problematic, but here it’s perfect because the target attribute of the event object is the component triggering the event. That includes the id prop value. So, we can start by checking if this is the contactName field based on the id. If it is, and if the length of the value passed in is greater than 16, which is our maximum name length, then we return immediately. That effectively limits the maximum length of the field. If there’s still room, or this isn’t the contactName field, then the id again comes into play because we use it to set the appropriate state property. If you look back at the state object, you’ll find that the fields associated with editing a contact match the id’s used on the TextFields in this code. This is similarly true for the MessageView later. The result is that we have a generic event handler that can be used for all these TextFields, and that will mutate the correct property on the state object.

Note that a <br/> element follows the TextField; otherwise, the two TextFields here would be on the same line. And, speaking of both TextFields , here’s the next, this one for entering the eMail address:
<TextField margin="dense" id="contactEmail" label="Email"
  value={ state.contactEmail } variant="outlined"
  InputProps={{ style : { color:"#000000" } }} disabled={
  state.currentView === "contact" } style={{ width:520 }}
  onChange={ state.fieldChangeHandler } />
<br />
As you can see, aside from the id and label and the width, it’s the same as the name field (there’s no maximum length for this field so nothing to consider there – the width is simply arbitrary). So, let’s move on to the next thing in this code:
{ state.currentView === "contactAdd" &&
  <Button variant="contained" color="primary" size="small"
    style={{ marginTop:10 }} onClick={ state.saveContact }>
    Save
  </Button>
}
As in BaseLayout, the conditional render trick is used here because the SAVE button should only be visible when we’re adding a contact. Otherwise, this is a Material-UI button as you’ve seen before, so let’s go explore its click handler, saveContact(), in the state object:
saveContact : async function(): Promise<void> {
  const cl = this.state.contacts.slice(0);
  this.state.showHidePleaseWait(true);
  const contactsWorker: Contacts.Worker = new Contacts.Worker();
  const contact: Contacts.IContact = await contactsWorker.addContact({
      name : this.state.contactName,
      email : this.state.contactEmail });
  this.state.showHidePleaseWait(false);
  cl.push(contact);
  this.setState({ contacts : cl, contactID : null,
    contactName : "", contactEmail : "" });
}.bind(inParentComponent)

We begin, as you’ve seen a few times before, copying the contacts array. Next, the please wait dialog is shown, and then a call to Contacts.Worker.addContact() is made, passing it an object formed from the contactName and contactEmail fields in state, whose values will have been set when the user typed them in the two TextFields from before. When that call comes back, please wait is hidden, the returned contact (which now includes the _id field) is pushed into the contacts array, and finally setState() is called to update the array. I also clear out the properties associated with editing a contact so that the field is clear, in case the user wants to add another contact right away.

After that, we have two more buttons to deal with:
{ state.currentView === "contact" &&
  <Button variant="contained" color="primary" size="small"
    style={{ marginTop:10, marginRight:10 }}
    onClick={ state.deleteContact }>
    Delete
  </Button>
}
{ state.currentView === "contact" &&
  <Button variant="contained" color="primary" size="small"
    style={{ marginTop:10 }}
    onClick={ () => state.showComposeMessage("contact") }>
    Send Email
  </Button>
}

These two are only shown when a contact has been clicked from the list, and they provide the user the opportunity to delete the contact or send a new eMail to them. Note that for both, I added some margin on the top to separate them from the eMail field, and for the DELETE button, I also have some on the right so the buttons themselves don’t bunch up. For the DELETE button, the onClick handler can point directly at the deleteContact() method in the state object because that method will already have access to everything it needs to do the delete (namely, the contactID property in state), but for sending an eMail, we need to know the source of the action, as you saw earlier, hence why we need to use the anonymous function approach here.

As far as that deleteContact() goes, here it is:
deleteContact : async function(): Promise<void> {
  this.state.showHidePleaseWait(true);
  const contactsWorker: Contacts.Worker = new Contacts.Worker();
  await contactsWorker.deleteContact(this.state.contactID);
  this.state.showHidePleaseWait(false);
  const cl = this.state.contacts.filter(
    (inElement) => inElement._id != this.state.contactID
  );
  this.setState({ contacts : cl, contactID : null,
    contactName : "", contactEmail : "" });
}.bind(inParentComponent)

It’s much the same as what you’ve seen before: show please wait, call server via Contacts.Worker.deleteContact() method , and then remove it from the list. To remove it, we must find it first. The filter() method of the contacts array in state allows us to do that. For each element in the array, the function we provide to filter() is executed and is passed the next element. If the _id property of the element isn’t state.contactID, then true is returned, which filter() takes to mean we want the element included in the new array that it’s constructing. Simply put, all elements in state.contacts will be copied into the new array cl except for the contact being deleted. What that new array built, we can pass it to setState(), which results in React re-rendering the list, sans that contact. As with adding a contact, the contact editing-related properties in state are cleared, and we’re done.

MessageList.tsx

Next up is the message list, which means the MessageList component in the MessageList.tsx file. Here’s pretty much the whole thing:
<Table stickyHeader padding="none">
  <TableHead>
    <TableRow>
      <TableCell style={{ width:120 }}>Date</TableCell>
      <TableCell style={{ width:300 }}>From</TableCell>
      <TableCell>Subject</TableCell>
    </TableRow>
  </TableHead>
  <TableBody>
    { state.messages.map(message => (
      <TableRow key={ message.id }
        onClick={ () => state.showMessage(message) }>
        <TableCell>
         { new Date(message.date).toLocaleDateString() }
        </TableCell>
        <TableCell>{ message.from }</TableCell>
        <TableCell>{ message.subject }</TableCell>
      </TableRow>
    ) ) }
  </TableBody>
</Table>

The Table component is another Material-UI component for displaying data like List that you saw earlier, but this one essentially mimics an HTML table. That’s ideal for the message list because it should look like a typical table, or grid as it’s sometimes called. We have a header row, which we want to ensure stays “stuck” at the top even if you user scrolls to see the list, and that’s what the stickyHeader prop set to true does for us. Then we have three columns, one each for Date, From, and Subject. So, inside the Table component goes firstly a TableHead component, which demarks the header. Inside that is a TableRow, since the header row is, in fact, a row like any other despite it being the header. Finally, inside that TableRow goes three TableCell components, one for each column in the table. The first two have specific widths assigned, which will cause the third, the Subject column, so take up the remaining horizontal space.

After the TableHead comes a TableBody , again, just like a plain old HTML table. Inside the TableBody goes a TableRow for each message. We iterate the list of messages found in state.messages with map() like you’ve seen for mailboxes and contacts. For each of the three data elements to display, we drop a TableCell into the TableRow and render the appropriate properties from the message object into it. That’s about all there is to it!

You’ll also, no doubt, notice that there is an onClick prop on the TableRow housing the message. When the row is clicked, we need to display in the view area below, which is what the showMessage() state mutator method does:
showMessage : async function(inMessage: IMAP.IMessage): Promise<void> {
  this.state.showHidePleaseWait(true);
  const imapWorker: IMAP.Worker = new IMAP.Worker();
  const mb: String = await imapWorker.getMessageBody(
    inMessage.id, this.state.currentMailbox
  );
  this.state.showHidePleaseWait(false);
  this.setState({ currentView : "message",
    messageID : inMessage.id, messageDate : inMessage.date,
    messageFrom : inMessage.from,
    messageTo : "", messageSubject : inMessage.subject,
    messageBody : mb
  });
}.bind(inParentComponent)

Once the please wait popup is shown, the IMAP.Worker.getMessageBody() method is called, passing it the ID of the message and the path of the current mailbox. The server is called, the body returned, and please wait hidden. Finally, the setState() call is made to change the view and populate all the message details, including the body. The result is that the message is displayed, all pertinent details visible to the user in the view area below the message list.

MessageView.tsx

MessageView is perhaps the most complex of all the components, and even it isn’t anything that’ll hurt your brain! It all begins with a plain old HTML form:
<form>

As with ContactView, we need a single container for fields here, and a <form> again makes sense, though it doesn’t need to be a <form> element.

Speaking of the fields that follow:
{ state.currentView === "message" &&
  <InputBase defaultValue={ `ID ${state.messageID}` }
    margin="dense" disabled={ true } fullWidth={ true }
    className="messageInfoField" />
}
{ state.currentView === "message" && <br /> }
First, we need a field to display the ID of the message. Since this isn’t something that will ever be editable, a TextField isn’t needed. And, while it certainly could have just been a plain old <div>, I decided to use another Material-UI component, just to give you a little more exposure to it. The InputBase component serves as the basis for many of Material-UI’s form field components and provides a minimal set of style rest and state logic. It’s perfect for our purposes because it looks an awful lot like TextField from an API standpoint, but is a bit simpler. The defaultValue prop is the message ID, prefixed with a static “ID” text to serve as a label of sorts. The margin prop is again set to dense to reduce space around it and the field should always be disabled, so that prop is set to true. The fullWidth prop, set to true, stretched the field across the entire width of its container, which ensures we always have enough space to display the content. Finally, a little bit of styling is needed, but this time it’s done with a className prop rather than inlining the styles, and that leads us to the last style rule in our main.css file:
.messageInfoField {
  color : #000000!important;
}

That serves much the same purpose as the styling on the TextFields you saw earlier to make the text readable when the field is disabled. Note that appending !important was required here to get the color to override the default gray color, not an uncommon thing to have to do with CSS.

After that comes another InputBase component , this time for the message’s date:
{ state.currentView === "message" &&
  <InputBase defaultValue={ state.messageDate } margin="dense"
    disabled={ true } fullWidth={ true }
    className="messageInfoField" />
}
{ state.currentView === "message" && <br /> }

Of course, conditional rendering is used to only show these fields with currentView is "message", that is, when viewing an existing message, as opposed to when this view is shown for composing a message.

One last field remains after that:
{ state.currentView === "message" &&
  <TextField margin="dense" variant="outlined"
    fullWidth={ true } label="From"
    value={ state.messageFrom }
    disabled={ true }
    InputProps={{ style : { color : "#000000" } }} />
}
{ state.currentView === "message" && <br /> }

The From field is again only shown when viewing an existing message, and like the previous two InputBase fields , this one stretches across the entire container thanks to fullWidth being set to true. Of course, this field is never editable, hence disabled set to true with no logic.

Now we get into some fields that are shown only when composing a message:
{ state.currentView === "compose" &&
  <TextField margin="dense" id="messageTo" variant="outlined"
    fullWidth={ true } label="To"
    value={ state.messageTo }
    InputProps={{ style : { color : "#000000" } }}
    onChange={ state.fieldChangeHandler } />
}

The To field is obviously needed, and like From, we stretch it across the entire container to allow the user as much room to enter long addresses as possible. You can see our friendly neighborhood state.fieldChangeHandler() method again being used, which means that the id here must match the state property that should get the value of whatever the user enters here, which it does.

Then, we need a subject field:
{ state.currentView === "compose" && <br /> }
<TextField margin="dense" id="messageSubject" label="Subject"
  variant="outlined" fullWidth={ true }
  value={ state.messageSubject }
  disabled={ state.currentView === "message" }
  InputProps={{ style : { color : "#000000" } }}
  onChange={ state.fieldChangeHandler } />
<br />
There’s nothing new there frankly, so let’s move on to the field where the user will enter the actual text of the message:
<TextField margin="dense" id="messageBody" variant="outlined"
  fullWidth={ true } multiline={ true } rows={ 12 }
  value={ state.messageBody }
  disabled={ state.currentView === "message" }
  InputProps={{ style : { color : "#000000" } }}
  onChange={ state.fieldChangeHandler } />

Ah, here we have a few new props! When multiline is set to true, Material-UI will render an HTML <textarea> under the covers. That implies that we can set rows and cols value, and we can, though only rows is used here because fullWidth is set to true. So, what we wind up with is a <textarea> that fills the entire available horizontal space and allows for 12 rows of text to be input (it will, of course, scroll to accommodate more as necessary, as a <textarea> does by default). Other than that, though, this is the same as the other fields.

After the fields, we must work on the buttons that we need, beginning with the one to send the message being composed:
{ state.currentView === "compose" &&
  <Button variant="contained" color="primary" size="small"
    style={{ marginTop:10 }}
    onClick={ state.sendMessage }>
  Send
</Button>
}
That’s a pretty straightforward Button component, isn’t it? Given there’s nothing new, let’s jump right to that sendMessage() state method referenced in the onClick prop :
sendMessage : async function(): Promise<void> {
  this.state.showHidePleaseWait(true);
  const smtpWorker: SMTP.Worker = new SMTP.Worker();
  await smtpWorker.sendMessage(this.state.messageTo,
    this.state.messageFrom, this.state.messageSubject,
    this.state.messageBody
  );
  this.state.showHidePleaseWait(false);
  this.setState({ currentView : "welcome" });
}.bind(inParentComponent)

Again, this code is very much like other methods you’ve seen. We show the please wait dialog; call the appropriate Worker method, passing it the properties from state that will have been populated as the user typed values into the fields, thanks to that fieldChangeHandler() method ; hide please wait; and finally change the view to the welcome view, since it doesn’t make much sense to go somewhere else or even to stay on the compose view at that point.

Next, we have a button that only shows up when the user clicks a message from the message list, namely, the one to reply to the message:
{ state.currentView === "message" &&
  <Button variant="contained" color="primary" size="small"
    style={{ marginTop:10, marginRight:10 }}
    onClick={ () => state.showComposeMessage("reply") }>
    Reply
  </Button>
}

For all intents and purposes, it’s the same as the NEW MESSAGE button in the toolbar, but the one difference is that showComposeMessage() must be called and passed a different value, so it’s anonymous function time again! And, of course, this button is only shown when state.currentView is "message", not when it’s "compose".

Similarly, the last button to deal with, DELETE, is also only shown when viewing an existing message from the message list:
{ state.currentView === "message" &&
  <Button variant="contained" color="primary" size="small"
    style={{ marginTop:10 }}
    onClick={ state.deleteMessage }>
    Delete
  </Button>
}
By this point, you should have a very good handle on Material-UI Buttons, right? I expect so! Therefore, it’s time for the final method in state, the deleteMessage() method , hooked to this button via its onClick prop :
deleteMessage : async function(): Promise<void> {
  this.state.showHidePleaseWait(true);
  const imapWorker: IMAP.Worker = new IMAP.Worker();
  await imapWorker.deleteMessage(
    this.state.messageID, this.state.currentMailbox
  );
  this.state.showHidePleaseWait(false);
  const cl = this.state.messages.filter(
    (inElement) => inElement.id != this.state.messageID
  );
  this.setState({ messages : cl, currentView : "welcome" });
}.bind(inParentComponent)

This is very much the same as the deleteContact() method , except that it calls IMAP.Worker.deleteMessage() of course, passing it the message’s ID and the path to the current mailbox, and then filtering the state.messages array to remove it from the list.

We have just one more component left to examine, and here it comes!

WelcomeView.tsx

I saved perhaps the simplest component for last – WelcomeView :
<div style={{
  position:"relative", top:"40%", textAlign:"center",
  color:"#ff0000"
}}>
  <h1>Welcome to MailBag!</h1>
</div>

Yep, that’s literally it! The styling here serves to center the text (well, it’s actually a hair closer to the top than the bottom, which I think looks a little better) and then just a big, red <h1> element.

And that, my friend, is MailBag, all done and ready to serve your email needs!

Suggested Exercises

As with the server, I’ve left some things that you could do to enhance the MailBag client app that I think would serve as excellent learning experiences to put what you’ve learned to the test:
  • Did you notice that all emails are treated as plain text, whether they are or not? Wouldn’t it be nice to see HTML messages displayed as they’re intended to? I think so! See if you can pull that off. As a hint, the server will need to be altered to return HTML instead of plain text (should be a one- or two-line change), and then the client will need to display it (you might try just inserting the content into a <div>, or perhaps an <iframe>, but either will require you to make additional changes to MessageView.tsx).

  • Allow the user to upload pictures for their contacts. There are several approaches, but you might consider using a plain old HTML form with a <file> element, but then you’ll need to consider how to alter the server to accept it. Then, store the image in a temporary location and make the client fetch it to display it. Then figure out how to store it in NeDB when they save it (hint: Base64-encoded is probably a good answer there). Note that this most likely would not be part of the RESTful API of the server, so you’ll need to figure out how to make this coexist with the existing service endpoints.

Summary

In this chapter, we completed the MailBag app, building the client side of it. In the process, you learned about things like conditional rendering with React, the Material-UI component library, a bit about AJAX and the Axios library for doing it, and CSS Grid. The result is a complete application, MailBag, that, while pretty basic, does the job as a webmail client pretty well.

One project down, just one to go! And the next one is going to be something quite a bit different: a game!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.78.106