Part 4 from series: How to build a real-time chat app

Create Angular Components

Display a list of messages with Angular

We will focus on building the user interface to display a list of messages, we will create a new Angular Module to hold our new feature components and focus on presentation.

First we will need to do a bit of wiring up.

Setup the new feature module

Change directory to web-app/src/app from within your project folder.

cd <project-folder-path>/web-app/src/app

We will use the Angular CLI to create a new module that will contain all the artefacts of our new messages feature with routing.

ng generate module messages --routing=true

The CLI will create a new module folder and file called messages.module.ts that will be used to register all the items we will create with Angular's dependency injection system.

Next, we will create several user interface Components that will be responsible for different areas of the page display and interaction to give a good separation of concerns (SoC). This will hopefully make the application easier to understand from just looking at the file system arrangement.

Create the Components

  • CreateMessageComponent that will allow a user to type and send a message.
  • ViewConversationComponent that has the responsibility of displaying an interactive conversation of messages.
  • MessagesComponent to tie all components together.
ng generate component messages/create-message
ng generate component messages/view-conversation
ng generate component messages/messages

If you open the messages.module.ts file, you'll see our new components registered with Angular:

File path icon web-app/src/app/messages/messages.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MessagesComponent } from './messages/messages.component';
import { CreateMessageComponent } from './create-message/create-message.component';
import { ViewConversationComponent } from './view-conversation/view-conversation.component';

@NgModule({
  declarations: [
    MessagesComponent,
    CreateMessageComponent,
    ViewConversationComponent
  ],
  imports: [
    CommonModule
  ]
})
export class MessagesModule { }

The declarations array defines all the components that can be used within the scope of the module.

Routing

Our application is very simple with regards to routing. There will be just one route to define. Open the file web-app/src/app/messages/messages-routing.module.ts. Angular best practice is to define routing in a separate module file because routing code can become very verbose for many applications. To define a route, we simply need to add an item to the routes array that's registered with the NgModule imports metadata in this file.

To create a route to our messages feature, we need to specify a relative route path and a component to render when a user navigates to the URL route. At the top of the file we will import the MessagesComponent that we are going to route to.

File path icon web-app/src/app/messages/messages-routing.module.ts
import { MessagesComponent } from './messages/messages.component';

Then replace const routes: Routes = []; with the following:

File path icon web-app/src/app/messages/messages-routing.module.ts
const routes: Routes = [
  { path: '',     component: MessagesComponent}
];

Now that we have created our routes, we need to tell Angular to use compile this new module and route in the application. The AppModule is the top-level module that is bootstrapped and initialised by Angular so this is where we will import our new messages feature module. Open file web-app/src/app/app.module.ts and add an import for our new feature at the top of the file.

File path icon web-app/src/app/app.module.ts
import { MessagesModule } from "./messages/messages.module";

And add the MessagesModule module to the to the NgModule imports array.

File path icon web-app/src/app/app.module.ts
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    MessagesModule, // <- Add new module
    GraphQLModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Within the same directory, our app.component.html HTML page currently still displays the Angular CLI generated code. We need to update this to allow routed content to display within a designated area of our page. Replace the contents of this file with the following:

File path icon web-app/src/app/app.component.html
<router-outlet></router-outlet>

If we run ng serve, we will be able to see the contents from our routed messages/messages/messages.component.html file within our new messages feature module.

Building the user interface

Now that we've done all the wiring up, we can now focus on creating the user interface. Open the messages/messages/messages.component.html file and insert the following code:

File path icon web-app/src/app/messages/messages/messages.component.html
<div class="messages">
  <header class="header">
    <img class="slack-image" src="assets/logo.png" />Slack messaging
  </header>
  <div class="messages__conversation">
    <app-view-conversation></app-view-conversation>
    <app-create-message></app-create-message>
  </div>
</div>

Open the file messages/messages/messages.component.ts and take a look at the @Component metadata.

A component can bind to a HTML template by registering a template or templateUrl property within the @Component metadata. Angular components let us specify a custom HTML tag to allow components to reference eachother within their HTML template. This is defined by using a property called selector to associate it with a component.

In our example, the code structure uses a messages class to wrap our entire feature component, a header and a div with class messages__conversation that contains the component selectors app-view-conversation from the view-conversation.component.ts file and app-create-message from the create-message.component.ts file that we created earlier. Recall we previously added ViewConversationComponent and CreateMessageComponent to our module declarations array in messages.module.ts. By doing this, we are able to use the selectors to reference our components anywhere within our feature module.

If you would like to include a image such as a logo.png file at this point, you can add one in the assets folder located at: wep-app/assets.

If you're curious about the underscore convention used in our HTML classes. This is a common pattern called Block, Element, Modifier (BEM), used for specifying composition relationships within HTML documents and is used throughout this tutorial.

If we check our page in the web-browser, we can see content shown in all our components from using component selectors.

Display the list of messages

To create the user interface for a list of messages, we will need to change a triad of closely related files.

  • A Typescript file to hold all component behaviours and data.
  • An HTML file to describe our component layout with data.
  • An SCSS styles file to define the look and feel for our layout of messages.

The Typescript Component file

The component file (ending in component.ts) holds all the behaviours of the components interface. It provisions data in the template using data-binding and handles all events, whether the event is a new message sent back from the server or a users click on a 'send' button.

Open the messages/view-conversation/view-conversation.component.ts typescript file. After the import statements, we will first need to define a view-model for our HTML template. This interface will represent the shape of data for a single message that our template expects to bind to.

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.ts
import { Component, OnInit } from '@angular/core';

interface MessageViewModel {
  id: string;
  sent: Date;
  text: string;
  user: {
    name: string;
    avatarUrl: string;
    colour: string;
  };
  fromYou: boolean;
}

@Component({
  //...
})
// Existing code...

Then we need to define a messages component property that will hold the actual data of all our messages in an array. Each message object in the array will be structured with the above view-model interface, which is defined by using a typed array Array<MessageViewModel>.

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.ts
interface MessageViewModel {
  //...
}

@Component({
  selector: 'app-view-conversation',
  templateUrl: './view-conversation.component.html',
  styleUrls: ['./view-conversation.component.scss']
})
export class ViewConversationComponent implements OnInit {
  messages: Array<MessageViewModel> = []; // <- New property to hold messages

  constructor() {}

  ngOnInit() {}
}

The HTML template file

Next we need to create the markup that we will later use to format the layout of the array of messages held in the component. First we will create a static template but without binding to component data.

Open the messages/view-conversation/view-conversation.component.html file and replace the contents with the following:

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.html
<div class="view-conversation">
  <div class="view-conversation__list">
    <div class="view-conversation__item"
      [ngClass]="{'view-conversation__item--you': false}"> <!-- <- Directive binding -->

      <span class="view-conversation__time">9:43 AM</span>

      <div class="view-conversation__message-animation"
        [ngStyle]="{'border-color': '#c16fe0'}"><!-- <- Directive binding -->

        <div class="view-conversation__message">
          <img class="view-conversation__avatar"
            [src]="'https://lh3.googleusercontent.com/a-/AAuE7mAuiepkKGsj-1L9lhHSHtnQsXlVqHEQfJqzOoGz=s96-cc-rg'" /><!-- <- Directive binding -->
          <strong class="view-conversation__name">
            Shane Edwards
          </strong><br />
          Hi, how's it going?
        </div>
      </div>
    </div>
  </div>
</div>

Again we've used BEM to structure our HTML. We have an element with class view-conversation, which is a block to surround everything in our component and then various elements within this block denoted with class names prefixed with view-conversation__.

You may have noticed that not all of this template is plain old HTML. We have Angular built-in directive bindings such as [ngClass], [ngStyle] and [src] within the template that enables the values of each attribute to change when the corresponding properties that they are bound to change from within the Typescript component.

If we open the browser window again at http://localhost:4200, we can see the changes to our UI that show our hard-coded message, avatar and time sent (currently not so exciting, but we have to start somewhere). Lets add some styles to our user interface.

The SCSS styles file

The styles for our component will be written with SCSS (aka SASS), a preprocessor scripting language to compile CSS that helps with maintaining complex stylesheets. The style rules are again formatted with BEM. Class names when compiled will correspond to HTML class names.

Styles are associated with a component by specifying a styleUrls property within a @Component's metadata. This is automatically set up when creating a new component with the Angular CLI.

Replace the contents of the file messages/view-conversation/view-conversation.component.scss with the following:

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.scss
.view-conversation {
  display : flex;
  flex-direction : column;
  margin : 0;
  padding : .5rem;
  padding-bottom : 0;
  /*max-height : 400px;*/
  overflow-y : auto;
  list-style-type : none;
  background-color : #f5f5f5;

  * {
    line-height : 1.4rem;
    box-sizing: border-box;
    font-family: 'Helvetica Neue', Arial;
  }

  h1 {
    font-weight : normal;
  }

  &__heading {
    font-size : 1.8rem;
  }

  &__list {
    position : relative;
  }

  &__item {
    align-self : flex-start;
    margin-left : 5%;
    width : 95%;

    + .view-conversation__item {
      margin-top : 1rem;
    }

    &--you {
      margin-left : 0;
    }
  }

  &__time {
    display : block;
    margin-bottom : .3rem;
    font-size : .8rem;
    color : #666;
  }

  &__message {
    padding : 1rem;
    border-radius : 3px;
  }

  &__message-animation {
    position : relative;
    overflow : hidden;
    border-radius : 3px;
    background-color : #fff;
    border-left : 5px solid #dca629;
  }

  &__message-cover {
    position : absolute;
    top : 0;
    right : 0;
    bottom : 0;
    left : 0;
    opacity: 0;
  }

  &__avatar {
    float : right;
    margin-left : 1rem;
    margin-top : -.4rem;
    margin-right : -.4rem;
    max-width : 3.5rem;
  }

  &__name {
    display : inline-block;
    margin-bottom : .2rem;
  }
}

In our new SCSS file we have a top-level Block class selector called .view-conversation, which wraps all other styles within the scope of our component. The Element classes that we wrote in the HTML file are created by prepending the Block name (using &) followed by two underscores and the Element name. Modifiers can be used by prepending the Block name followed by two dashes and the modifier name.

BEM examples with SCSS

The following is an example SCSS block with an element and modifiers.

/* Block */
.block-name {
  font-size : 1.2rem;

  /* Element */
  &__element-name {
    display : inline-block;
    margin-bottom : .2rem;

    /* Optional element modifier */
    &--element-modifier-name {
      color : red;
    }
  }

  /* Optional block modifier */
  &--block-modifier-name {
    border : 1px solid red;
  }
}

When compiled, would create the following CSS rules:

/* Block compiled */
.block-name {
  font-size : 1.2rem;
}

/* Element compiled */
.block-name__element-name {
  display : inline-block;
  margin-bottom : .2rem;
}

/* Element modifier compiled */
.block-name__element-name--element-modifier-name {
  color : red;
}

/* Block modifier compiled */
.block-name--modifier-name {
  border : 1px solid red;
}

Then when we want to use the Block and Element structure in our HTML layout it could look similar to this.

<div class="block-name">
  <div class="block-name__element-name">
    Element of block-name
  </div>
</div>

And when used with optional modifiers.

<div class="block-name block-name--modifier-name">
  <div class="block-name__element-name block-name__element-name--element-modifier-name">
    Element of block-name
  </div>
</div>

Modifiers are useful when you want to create variants of block or element styles such as visually indicating different states of system feedback to a user for things like error or success state.

Go back to your browser window, you should see the messages now with our new styles applied.

Binding the template to component state

In order to make our HTML template display messages from our component (and not just hard-coded within our template file), we need to bind the template to the corresponding state held in the component. Recall that we created a new property that holds an array of messages on our component in the file view-conversation.component.ts. We're now going to add some fake message data within this array to help us bind the template to the component.

Open file messages/view-conversation/view-conversation.component.ts and change the following code from:

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.ts
export class ViewConversationComponent implements OnInit {
  messages: Array<MessageViewModel> = []; // <-- Change this

  // Existing code - don't change...
}

to add the following code (no code needs to be deleted):

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.ts
export class ViewConversationComponent implements OnInit {
  messages: Array<MessageViewModel> = [
    // Include this
    {
      id: '1',
      sent: new Date(),
      text: 'This message is bound to component data',
      user: {
        name: 'Shane Edwards',
        avatarUrl: 'https://lh3.googleusercontent.com/a-/AAuE7mAuiepkKGsj-1L9lhHSHtnQsXlVqHEQfJqzOoGz=s96-cc-rg',
        colour: '0080a2'
      },
      fromYou: false
    },
    {
      id: '2',
      sent: new Date(),
      text: 'Cool',
      user: {
        name: 'You',
        avatarUrl: 'https://angular.io/assets/images/logos/angular/shield-large.svg',
        colour: 'c16fe0'
      },
      fromYou: true
    }
  ];

  // Existing code - don't change...
}

We now have data that we can bind to within our template. Open file messages/view-conversation/view-conversation.component.html, we are going to add the bindings by adding the code:

Note: Please take some time to read the annotations in <!-- comments -->.

File path icon web-app/src/app/messages/view-conversation/view-conversation.component.html
<div class="view-conversation">
  <div class="view-conversation__list">

    <!-- *ngFor reapeting directive for messages property -->
    <div *ngFor="let message of messages"
      class="view-conversation__item"
      [ngClass]="{'view-conversation__item--you': message.fromYou}"><!-- Directive binding to message.fromYou -->

      <span class="view-conversation__time">
        <!-- Data binding to message.sent with date pipe -->
        {{message.sent | date: 'shortTime'}}
      </span>

      <div class="view-conversation__message-animation"
        [ngStyle]="{'border-color': '#' + message.user.colour}"><!-- Directive binding to message.user.colour -->

        <div class="view-conversation__message">
          <img class="view-conversation__avatar"
            [src]="message.user.avatarUrl" /><!-- Directive binding to message.user.avatarUrl -->
          <strong class="view-conversation__name">
            <!-- Data binding to message.user.name -->
            {{message.user.name}}
          </strong><br />

          <!-- Data binding to message.text -->
          {{message.text}}
        </div>
      </div>
    </div>
  </div>
</div>

There are three ways we bind data from a message within this template.

ngFor directive

The *ngFor directive allows us to iterate over an array and copy the contents of the template it's attached to as many times as there are items in the array.

Inside the *ngFor="" directive value, we have let message of messages. What we're doing here is binding to our messages property in our Typescript component (by using of messages) and specifying its value to iterate over. This allows each item in the array to be accessed with the message template variable (by using let message).

If you recall, we added two items to our messages array. So our template will be copied two times.

Attribute directive

We briefly touched on attribute directives earlier. These directives require a map of keys (that represent things like HTML classes or CSS style rules) and boolean values that determine when the keys are used in the HTML.

The values in our case are taken from properties on the object held within the message variable, which we can access thanks to the *ngFor directive.

For example, the code [ngClass]="{'new-class-name': message.fromYou}"> will add the class new-class-name to the element it's attached to when message.fromYou is a true value.

Interpolation

Interpolation allows us to simply embed expressions into HTML to calculate values. This can be used to write or bind data anywhere within our template between markup and can also be used for attribute values.

An example of interpolation between markup:

<p>{{message.interpolatedValue1}}<br /> {{message.interpolatedValue2}}</p>

We also use a date pipe in our template to format a date object into a nice readable text string. Pipes are great for transforming data reactively--given an input they return a predictable output.

If you now go back into your browser, you should see our template bound to the messages component data with two messages displayed.