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

Angular Components with GraphQL Queries

Use a GraphQL query to populate a list of messages in an Angular component

We will set up Apollo Angular so that we can talk to the Apollo GraphQL API to retrieve a message list with a GraphQL query.

Create an Angular Service to retrieve messages

Open a command-line prompt or terminal if you haven't already. We will need to create a few files to hold our Angular service and GraphQL query. Run the following commands.

  • cd <project-folder>/web-app/src/app/messages/
  • mkdir -p ./shared
  • cd ./shared
  • touch ./conversation-message-fragment.gql.ts
  • touch ./conversation-user-fragment.gql.ts
  • ng generate service retrieve-messages

The following folder and files should be added:

|--app
|  |--messages
|  |  |--shared
|  |  |  |--conversation-message-fragment.gql.ts
|  |  |  |--conversation-user-fragment.gql.ts
|  |  |  |--retrieve-messages.service.ts

We will create a service to describe a query that we'll use to call our GraphQL endpoint, but first we will focus on creating query Fragments in the shape of the GraphQL data that we require in the response.

GraphQL Fragments

Fragments are an excellent way of grouping common properties used for different user interface views of the same data models.

We will create a fragment for viewing a message and also a user within our view-conversation.component.ts component. We will start with the user, open the file conversation-user-fragment.gql.ts and add the following code:

web-app/arc/app/messages/shared/conversation-user-fragment.gql.ts
import gql from 'graphql-tag';

export default gql`fragment conversationUser on User {
  id
  name
  colour
  avatarUrl
}`;

You may have noticed that the properties resemble the User object that we created for our GraphQL API. The syntax fragment conversationUser on User defines a fragment called conversationUser that will define properties from the User model of our GraphQL API.

If you have a compatible IDE (such as JetBrains Webstorm), there may be a plugin available similar to JS GraphQL for Webstorm. The plugin gives you GraphQL Schema introspection, which allows external services to map the data models defined in a schema and can add an extra level of error-catching and debugging when describing properties and models used from the API.

Open the file conversation-message-fragment.gql.ts and add the following code:

web-app/arc/app/messages/shared/conversation-message-fragment.gql.ts
import gql from 'graphql-tag';
import conversationUserFragment from './conversation-user-fragment.gql';

export default gql`fragment conversationMessage on Message {
  id
  userId
  text
  fromYou
  timestamp
  user {
    ...conversationUser
  }
}
${conversationUserFragment}
`;

Here we've created a fragment to represent the data we need on a Message model for our view-conversation.component.ts file. We've also mixed-in the conversationUserFragment fragment into the user properties to add to our Message fragment and maintain a clean level of separation. In order to use the conversationUserFragment, we have to defined it within our gql template literal using the template literal expression ${conversationUserFragment}.

Now we have all the fragments of our GraphQL request, lets create a service and use them to build a query.

GraphQL Client Queries

Our new service will implement GraphQL's Query interface, which will let us focus on the format of the query and not worry too much about the implementation. Open the file shared/retrieve-messages.service.ts and replace the contents with the following code:

web-app/arc/app/messages/shared/retrieve-messages.service.ts
import { Injectable } from '@angular/core';
import gql from 'graphql-tag';
import { Query } from 'apollo-angular';
import conversationMessageFragment from './conversation-message-fragment.gql';

@Injectable({
  providedIn: 'root'
})
export class RetrieveMessagesService extends Query<Response> {
  document = gql`
    query Messages($max: Int) {
      messages(max: $max) {
        ...conversationMessage
      }
    }
    ${conversationMessageFragment}
  `;
}

In our RetrieveMessagesService service all we need to create a query is implement apollo-angular's Query and simply define a document property to hold our request string. Within the gql template literal, we've written our top-level query for retrieving a list of messages and again used fragments to define the messages shape. We can restrict the amount of messages returned from this query by using the $max parameter that will be later defined in our component that uses this service.

The @Injectable syntax lets Angular know that this is a service to be injected into components and other services. Injectors can be defined as application-wide singleton instances by specifying the providedIn: root property and value.

Subscribe to the Angular Service to display messages from the GraphQL endpoint

Open the file messages/view-conversation/view-conversation.component.ts. Lets remove the hard-coded data that we left in this file. Change the ViewConversation components messages property from:

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
export class ViewConversationComponent implements OnInit {
  messages: Array<MessageViewModel> = [
    // Remove the code below
    {
      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: '00a5d1'
      },
      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
    }
    // Stop here
  ];

  // Existing code...
}

To the empty array property:

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
export class ViewConversationComponent implements OnInit {
  messages: Array<MessageViewModel> = []; // <-- Should be left with this

  constructor() {}

  ngOnInit() {}
}

This should clear all messages from our message user interface. We will need to add two imports at the top of the file as well as a MessageModel interface that will tell Typescript what the shape of our returned data will look like, which we will put before our MessageViewModel interface.

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
import { Component, OnInit } from '@angular/core';
import { RetrieveMessagesService } from '../shared/retrieve-messages.service'; // <-- Add new import
import { ApolloQueryResult } from "apollo-client"; // <-- Add new import

// New MessageModel
interface MessageModel {
  id: string;
  userId: string;
  text: string;
  timestamp: string;
  fromYou: boolean;
  user: any;
}

interface MessageViewModel {
  // Existing code...
}

// Existing code...

Next we need to add the RetrieveMessagesService type to our component constructor to get an instance of our new service that will retrieve new messages:

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
// Existing code...
export class ViewConversationComponent implements OnInit {
  messages: Array<MessageViewModel> = [];

  constructor(
    private retrieveMessagesService: RetrieveMessagesService,
  ) {}

  ngOnInit() {}
}

Add a new static method that will be responsible for creating MessageViewModel instances from MessageModel data to create an object that our Angular template can bind to. Define the new static method after the constructor.

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
// Existing code...
export class ViewConversationComponent implements OnInit {
  // Existing code...

  // Static methods should be defined before instance methods
  static createMessageInstance(messageData: MessageModel): MessageViewModel {
    return <MessageViewModel>{
      ...messageData,
      sent: new Date(parseInt(messageData.timestamp) * 1000),
      user: {
        ...messageData.user,
        avatarUrl: messageData.user.avatarUrl || 'assets/avatar.png'
      }
    };
  }

  ngOnInit() {}
}

This method essentially transforms an object from a MessageModel type to a MessageViewModel type. The spread operator (...) will mixin properties from the MessageModel to add to the new one.

Message instances created by the method above use the same static image if the user doesn't have an image associated with their account and also anonymous users. Here, you can save and add the image below to your web-app/src/assets folder or add your own if you wish.

Avatar

We need another method for updating our messages after they've each been mapped from a MessageModel to a MessageViewModel.

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
// Existing code...
export class ViewConversationComponent implements OnInit {
  // Existing code...

  ngOnInit() {}

  updateMessages(result: ApolloQueryResult<{messages: Array<MessageModel>}>) {
    if (!result || !result.data || !result.data.messages) {
      return false;
    }

    this.messages = result.data.messages.map(ViewConversationComponent.createMessageInstance).reverse();
  }
}

We've used a guard (an if statement) to protect the function from attempting to assign the messages property without the required data in the result.

If the message data is valid, the component property messages is assigned with a mapped version of the result.data.messages array using our new static method (ViewConversationComponent.createMessageInstance).

Lastly we need to wire everything up in our ngOnInit Angular lifecycle method and subscribe to our RetrieveMessagesService service.

web-app/arc/app/messages/view-conversation/view-conversation.component.ts
// Existing code...
export class ViewConversationComponent implements OnInit {
  // Existing code...

  ngOnInit() {
    this.retrieveMessagesService
      .fetch({ max: 4 })
      .subscribe((result: ApolloQueryResult<any>) => this.updateMessages(result));
  }

  updateMessages(result: ApolloQueryResult<{messages: Array<MessageModel>}>) {
    // Existing code...
  }
}

Here we call fetch on our new service, with an object that represents the variable of our query { max: 4 }. this will instruct our API to limit the amount of results returned to the latest 4 results.

Testing our feature

At the moment we don't have messages in our service to test if our feature is working with the GraphQL endpoint, lets fix that.

We can't create a message from our application because we haven't built this feature just yet. But we can send messages from Slack's own applications to test our new user interface with real GraphQL data.

If you haven't already, download and install the Slack app to your computer, you will need to log in to your Slack workspace. Use your workspace URL (created when creating the authentication tokens from the setup guide, the details should be in your inbox) along with your email address and password to sign in to the Slack app.

You might recall that in our web-api we hard-coded a constant called channelName in our messages.messages.query.js resolver file. This holds the value conversation-1 and will be the Slack channel name that gets created before the first message is sent by a user.

Once logged in to your Slack workspace, you should see a heading in the left hand navigation called Channels and a '+' sign next to it. Click this to create a new channel and name it conversation-1, make sure this isn't a private channel.

After creating the new channel, type and send a new message in it. If you go back to your application and refresh the page, you should see the message you typed in our Angular application.