Lesson 19

Improving User Experience

User experience improvements for the application

PRO

Lesson Outline

Improving User Experience

We are just about finished with the application now, but as I mentioned before, we want to go the extra mile and focus on the little details in the application that will do a great deal to improve user experience.

I don't want to pay too much attention to discussing user experience concepts and the rationale of why we are making certain changes, as a lot of that is covered in detail in the User Interface and User Experience module in this course. I will try to keep it brief and focus on implementing the changes (because we will be making quite a few).

Let's get into it!

Disable Buttons for Invalid Forms

The first change is going to be a subtle improvement, but it will help guide the user and it will also make things a bit easier for us. We are going to modify all of the buttons in our application that submit forms so that they are disabled until the form is valid. This will prevent the user from clicking the button if certain conditions are not met, and it will also apply visual styling to the button to indicate that the button is not active. All we need to do to achieve this is to toggle the built-in disabled attribute.

Modify the Log In button in src/app/login/login.page.html to refect the following:

      <ion-button
        [disabled]="!(username.length > 0 && password.length > 0)"
        color="light"
        fill="outline"
        class="login-button"
        (click)="login()"
      >
        <ion-icon slot="end" name="log-in"></ion-icon> Log In
      </ion-button>

This will disable the login button if the user has not entered both a username and password.

Modify the Create Account button in src/app/register/register.page.html to reflect the following:

<ion-button
  [disabled]="!registerForm.valid"
  expand="full"
  color="light"
  class="register-button"
  (click)="createAccount()"
>
  Create Account <ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>

Since we are using the FormBuilder to create our register form, all we have to do is check the valid property on the form. If the form is not valid, we disable the button.

Modify the Add Chat button in src/app/chat/chat.page.html to reflect the following:

      <ion-button
        [disabled]="message.length === 0"
        (click)="addChat()"
        class="send-chat-button"
      >
        <ion-icon slot="icon-only" name="send"></ion-icon>
      </ion-button>

For the chat button, we check if there is a message entered into the message field.

Modify the Save Notice button in src/app/add-notice/add-notice.page.html to reflect the following:

    <ion-button [disabled]="title.length === 0" class="save-notice-button" color="primary" expand="full" (click)="saveNotice()">
      Save Notice
    </ion-button>

When adding a notice the user can add a title field and a message field, but we only enforce that a title is entered, so we do the same here for disabling the button.

Improving User Feedback for Forms

We've already implemented some measures for giving feedback to the user when filling out forms, but we are going to add a couple of extra details now.

When we implemented our asynchronous username and email validators we also set up an error message that will be displayed to the user should these validations fail. However, since these are asynchronous this may take a couple of seconds to happen, and during that time the user might assume that the field was entered correctly only for it to change on them.

To combat this, we are going to add a loading indicator to our email and username fields to indicate to the user that the field is currently being checked. In order to do this, we are going to update our Error Messages component.

Modify src/components/error-messages/error-messages.component.html to reflect the following:

<div class="async-pending" *ngIf="control.pending">
  <ion-spinner></ion-spinner>
  Checking...
</div>
<div *ngIf="errorMessage !== null">
  <ul>
    <li>{{errorMessage}}</li>
  </ul>
</div>

This adds a conditional element to the template, which will only display if control.pending is true. The pending property on the control will be true if an asynchronous validator is currently executing, so the spinner will display whilst our username and email fields are checking against the server.

So that this displays properly, we are also going to add some styling to it.

Modify src/components/error-messages/error-messages.scss to reflect the following:

ion-spinner {
  height: 20px;
  width: 20px;
  margin-right: 5px;
}

.async-pending {
  display: flex;
  align-items: center;
  height: 20px;
  font-weight: normal;
}

ul {
  margin: 0;
  padding-left: 20px;
  padding-bottom: 5px;
  color: var(--ion-color-dark);
}

Now when the asynchronous validation is in progress the user should see the spinning animation.

We're going to add one more form related improvement for the login screen. Right now, we display an error message to the user for a failed login attempt. However, it just pops in instantly and it's possible that the user could miss it and not realise what is happening.

To improve this, we are going to add a shake animation to the error messages. This is a very common pattern for alert style messages, where the message dramatically and angrily shakes at you, drawing your attention to the element. We are going to create a simple CSS animation to achieve this effect.

Modify src/app/login/login.page.scss to reflect the following:

ion-content {
  --ion-background-color: var(--ion-color-primary);
}

ion-item {
  --ion-item-background: transparent;

  ion-input {
    color: #cecece;
    width: 100% !important;
    background-color: #e06179 !important;
    padding-left: 10px !important;
    margin: 10px 0 10px 10px !important;
    box-shadow: inset 1px 1px 2px 0px #c75363;
  }
}

ion-footer {
  background-color: #e06179;
}

.login-button {
  margin-top: 10px;
  font-weight: bold;
  color: #7d7c7c;
}

.error-logging-in {
  background-color: #f7f07b;
  padding: 10px;
  text-align: center;
  border: 2px solid var(--ion-color-danger);
  animation: shake 0.7s ease-out;
  animation-fill-mode: forwards;
}

@keyframes shake {
  10%,
  90% {
    transform: translate3d(-1px, 0, 0);
  }

  20%,
  80% {
    transform: translate3d(2px, 0, 0);
  }

  30%,
  50%,
  70% {
    transform: translate3d(-4px, 0, 0);
  }

  40%,
  60% {
    transform: translate3d(4px, 0, 0);
  }
}

We've created a shake keyframe animation that translates the element from left to right, and then we apply that animation to the error-logging-in class.

Handling Loading States

Since we are using a local PouchDB database that syncs to a remote CouchDB database, the data is going to appear in our application very quickly (because there is already local data available). However, we still want to make sure we are handling the state where the data is being loaded to provide a nice user experience. We will just use a simple spinner icon for when notices and chats are being loaded.

Modify src/app/chat/chat.page.html to reflect the following:

<ion-header class="ion-no-border">
  <ion-toolbar color="primary">
    <ion-title>Chat</ion-title>

    <ion-buttons slot="start">
      <ion-button (click)="logout()" class="logout-button">
        <ion-icon slot="icon-only" name="log-out"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div *ngIf="chats.length === 0" class="spinner-container">
    <ion-spinner></ion-spinner>
  </div>
  <ion-list lines="none">
    <ion-item *ngFor="let chat of chats">
      <ion-avatar
        [slot]="chat.author === this.userService.currentUser.user_id ? 'end' : 'start'"
        class="animate-in-primary"
      >
        <img
          src="https://avatars.dicebear.com/api/bottts/{{chat.author}}.svg"
        />
      </ion-avatar>
      <div class="chat-message animate-in-secondary">
        <ion-note>{{ chat.author }}</ion-note>
        <p>{{ chat.message }}</p>
      </div>
    </ion-item>
  </ion-list>
</ion-content>

<ion-footer>
  <ion-toolbar color="medium-contrast">
    <ion-textarea
      (keyup)="handleKeyup($event)"
      [(ngModel)]="message"
      class="chat-input"
      placeholder="type message..."
    >
    </ion-textarea>

    <ion-buttons slot="primary">
      <ion-button
        [disabled]="message.length === 0"
        (click)="addChat()"
        class="send-chat-button"
      >
        <ion-icon slot="icon-only" name="send"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-footer>

Improving the Chat User Interface

Our chat interface is already pretty good, but one annoying issue is that when new chats are added the list does not automatically scroll to the bottom. This means that a user will need to manually swipe the list down to see new messages, and if new messages appear the user might not even notice.

Another issue is that since we are resetting the data in our chats list whenever a new chat is received, the entire list pops back in with an animation which looks kind of silly. Let's address that first.

To update our chats list with new chats we just subscribe to the getChats() method from our chat service and when a new set of chats is received we update the local this.chats member variable with it:

      this.chatService.getChats().subscribe((chats) => {
        this.chats = chats;
        // ...snip
      });

This causes Angular to re-render the entire list of chats, when really we only need to render in the single new chat. To help Angular our here, what we can do is introduce a trackBy function. This is a function we define that takes in an index and the item we are looping over (the chat in this instance). We can then return a property that we want to track each item by, and Angular can use that unique property to determine which elements need to be re-rendered in the DOM. Since each of our chats have a unique _id value, we can use that as an identifier. If Angular sees a new _id it will know that is an element it will need to re-render.

Add the following method to the src/app/chat/chat.page.ts file:

  trackById(index: number, chat: Chat): string {
    return chat._id;
  }

Modify the *ngFor loop in src/app/chat/chat.page.html to use the new trackById function:

<ion-item *ngFor="let chat of chats; trackBy: trackById">

Not only does this make our animation look better, it's good for performance too! Let's add the code for making the chats auto scroll to the bottom now.

Modify src/app/chat/chat.page.ts to reflect the following:

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).