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

Angular Components & GraphQL Subscriptions

Integrate GraphQL Subscriptions into Angular components to build a list of messages

Our subscription is ready in our GraphQL API. Lets now integrate our Angular components with the new subscription to get live message updates. We will adjust our GraphQL setup to allow for websocket communication, create a service to contain our GraphQL subscription logic and finally integrate the subscription exposed from the new service into an Angular component.

Configuring Apollo Client for use with Subscriptions

We need to set up the Apollo client to allow websocket traffic and communication. To achieve this we need to split the link we currently have to handle the interaction with our API in to two parts. One branch of code will handle HTTP requests and another will handle websocket subscriptions.

We will first need to install a few new dependencies to set up a websocket by running the following commands within from your project folder's web-app directory:

npm install --save apollo-link-ws
npm install --save apollo-link
npm install --save subscriptions-transport-ws

This will give us the WebSocketLink dependency that we'll need to define our GraphQL endpoint used for subscriptions as well as peer dependencies and utilities that will give us the ability to split the request based on the operation type.

From your project folder, open the file web-app/src/app/graphql.module.ts and add the following imports to the top of the file:

web-app/src/app/graphql.module.ts
// Existing code...
import { WebSocketLink } from 'apollo-link-ws'; // <-- Add new import
import { ApolloLink, split } from 'apollo-link'; // <-- Add new import
import { getMainDefinition } from 'apollo-utilities'; // <-- Add new import
import { ApolloClientOptions } from 'apollo-client'; // <-- Add new import

const uri = 'http://localhost:4123/graphql';

// Existing code...

Then replace the createApollo function with the following:

web-app/src/app/graphql.module.ts
// Existing code...

export function createApollo(httpLink: HttpLink) {
  const http = httpLink.create({
    uri,
    withCredentials: true
  });

  const ws = new WebSocketLink({
    uri: `ws://localhost:4123/graphql`,
    options: {
      reconnect: true
    }
  });

  /**
   * Split the Apollo link to route differently based on the operation type. 
   */
  const link: ApolloLink = split(
    // split based on operation type
    ({ query }) => {
      let definition = getMainDefinition(query);
      return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
    },
    ws,
    http,
  );

  const opts: ApolloClientOptions<any> = {
    link,
    cache: new InMemoryCache()
  };

  return opts;
}

@NgModule({
  // Existing code...
})
export class GraphQLModule {}

The new version of the createApollo function will separate queries made from the client, splitting them by operation type, passing all HTTP requests to Apollo's HttpLink and subscription requests to the WebSocketLink.

The InMemoryCache will configure Apollo to save data (with caching enabled) to the physical server memory of which the app runs. For obvious reasons, this is only suitable for development purposes and for this tutorial.

Create an Angular Service for Subscription logic

Before we can integrate our components with the subscription, we will need to create an Angular service to hold the subscription logic.

Within the terminal or command prompt, create and open the file web-app/src/app/messages/shared/message-subscription-gql.service.ts with the Angular CLI.

ng generate service messages/shared/message-subscription-gql

In the new file add the following code:

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

@Injectable({
  providedIn: 'root'
})
export class MessageSubscriptionGQLService extends Subscription {
  document = gql`
    subscription {
      messageAdded {
        ...conversationMessage
      }
    }
    ${conversationMessageFragment}
  `;
}

We import our new Subscription class from apollo-angular to extend this in our custom class. All we have to do is specify the document property on the class to use this service in our component and subscribe to the query it generates.

Subscribe to new messages added to the conversation

In our component, we will first need a way to add a new message to our message list for when data is emitted from the subscription. We will create a method addNewMessage that will be called each time a new message is added. This will be responsible for updating our view-model of messages.

Open the file web-app/src/app/messages/view-conversation/view-conversation.component.ts, add an import to the new service and define a new dependency in our constructor.

web-app/src/app/messages/view-conversation/view-conversation.component.ts
// Existing code...
import { MessageSubscriptionGQLService } from '../shared/message-subscription-gql.service'; // <-- Add new import

// Existing code...

@Component({
  // Existing code...
})
export class ViewConversationComponent implements OnInit {
  // Existing code...

  constructor(
    private retrieveMessagesService: RetrieveMessagesService,
    private messageSubscriptionGQLService: MessageSubscriptionGQLService // <-- Add new dependency
  ) {}

  // Existing code...
}

Add the following method after the updateMessages method:

web-app/src/app/messages/view-conversation/view-conversation.component.ts
// Existing code...

@Component({
  // Existing code...
})
export class ViewConversationComponent implements OnInit {
  // Existing code...

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

  addNewMessage(messageData: any) { // <-- Add new method
    this.messages.push(
      ViewConversationComponent.createMessageInstance(messageData)
    );
  }
}

The method addNewMessage changes the user interface by mapping new message data to a new MessageViewModel and adding it to the messages array along with all other messages. The Angular template will detect the changes in the component and will update the user interface automatically for us.

We have a method that is purely responsible for the way we update the user interface. We also need to define a handler to react to new messages sent from the subscription and notify addNewMessage of these new changes.

After the addNewMessage method add the new method messageReceived:

web-app/src/app/messages/view-conversation/view-conversation.component.ts
// Existing code...

@Component({
  // Existing code...
})
export class ViewConversationComponent implements OnInit {
  // Existing code...

  addNewMessage(message: any) {
    // Existing code...
  }

  messageReceived(result: ApolloQueryResult<{messageAdded: MessageModel}>) {// <-- Add new method
    if (!result || !result.data || !result.data.messageAdded) {
      return;
    }

    this.addNewMessage(result.data.messageAdded);
  }
}

This method is pretty similar to our updateMessages method that we previously wrote. The difference being that we have delegated the responsibility of updating the user interface to another function.

The last thing to do is to subscribe to our new GraphQL subscription. At the bottom of the ngOnInit function, add the following code:

web-app/src/app/messages/view-conversation/view-conversation.component.ts
// Existing code...

@Component({
  // Existing code...
})
export class ViewConversationComponent implements OnInit {
  // Existing code...

  ngOnInit() {
    // Existing code...

    // Add new subscription below
    this.messageSubscriptionGQLService
      .subscribe()
      .subscribe(this.messageReceived.bind(this));
  }
}

Note: The chained calls to what appear to be duplicate subscribe() methods is not an error. The first subscribe is needed to retrieve the observable that we need to subscribe to.

We should have everything we need to test our Angular subscription. Make sure your API is running (along with required environment variables from the setup guide and run your application with ng serve (if it's not already running).

Open your browser window and navigate to http://localhost:4200. If we type a new message and click send, or send a new message from a downloaded Slack app (in the channel conversation-1), the user interface should update instantly with your new message without the need for a page refresh.

We've done well to build functionality into our new feature. Our application could benefit a lot from focusing on a better user experience.

Coming soon

Animate new incoming messages in an Angular component