Revisiting Realtime Angular 2

angular2.jpg

To get a sense of the framework with the official release candidate, we’re going to build a realtime Angular 2 app that lets you search Twitter for realtime updates.

Introduction

In May, Angular 2 released rc.1 and it brought quite a number of changes, mainly package renaming and some moving around. To get a sense of the framework with the official release candidate, we’re going to build a realtime Angular 2 app that lets you search Twitter for realtime updates.

This blog post is an update to our original Angular 2 post, but up to date with Angular 2’s first release candidate. Many thanks to Nathan Walker for his hard work updating the code and this post.

Please note that Angular 2 is in release candidate stage so some code may well change over time, but should be fairly consistent with what you can expect in the final release. If you’re feeling eager and would like to skip ahead to the code, you can do so on GitHub. You can also see the app running and try it out for yourself.

Introduction

Our application will allow users to enter search terms and get realtime results back from the Twitter streaming API. To do this we’ll be using the Pusher datasource API community project. This API listens to channel existence webhooks from Pusher and searches Twitter whenever a new channel is occupied, using the name of the channel for the search term. We won’t dive into detail on the server side in this post, but check out the Pusher datasource API README for a detailed explanation.

Getting Started

Angular 2 introduces a lot of new concepts, many of which we’ll see throughout this tutorial. We’ll be following the structure laid out in the Angular 2 quickstart tutorial, so refer to that if you’d like more information on why our app is structured as it is. If you’ve not read through that tutorial first you should, it introduces libraries like TypeScript and SystemJS that are important when building Angular 2 applications.

Building our realtime Angular 2 Application

We’ll be starting with an identical structure to the AngularJS quickstart project. We have all our code contained within src/, including the TypeScript config file, our HTML and the app/ folder which will contain our TypeScript source.

1- package.json
2- node_modules
3- src/
4    - app/
5        - app.component.ts
6    - index.html
7    - tsconfig.json

Angular 2 removes the concept of controllers in favour of components. In Angular 2 everything is a component; the first component we’ll build will be the one that holds our entire application in it. We’ll define our component in src/app/app.component.ts.

1import { bootstrap } from '@angular/platform-browser-dynamic';
2import { Component } from '@angular/core';
3
4@Component({
5    selector: 'my-app',
6    template: '<h1>Hello World</h1>'
7})
8class AppComponent {
9}
10
11bootstrap(AppComponent);

Couple things of note here: If you are coming from the earlier betas, notice the package import changes. Instead of bootstrap coming straight from core, it’s coming from a specific package which signals the adoption of more separation to support a wide variety of platforms, which is a good thing for everyone. For instance, a native mobile app, see example, can now use it’s own bootstrap or a universal Angular app which may run on the server, see example.

Now we can update our index.html file to use this component. From the Angular 2 getting started guide you’ll remember that at the top of the HTML file we’re loading Angular, SystemJS and then importing the generated JavaScript file. To have your TypeScript compiled every time you save, run npm run tsc in a tab. This starts the TypeScript compiler and watches for changes, running for each TypeScript file in the app directory and creating the corresponding JavaScript file.

1<html>
2  <head>
3    <title>Angular 2 QuickStart</title>
4    <link rel="stylesheet" type="text/css" href="src/style.css" />
5
6    <!-- 1. Load libraries -->
7    <!-- Polyfill(s) for older browsers -->
8    <script src="node_modules/core-js/client/shim.min.js"></script>
9    <script src="node_modules/zone.js/dist/zone.js"></script>
10    <script src="node_modules/reflect-metadata/Reflect.js"></script>
11    <script src="node_modules/systemjs/dist/system.src.js"></script>
12
13    <script src="https://js.pusher.com/3.0/pusher.min.js"></script>
14    <script>
15      System.config({
16        defaultJSExtensions: true,
17        paths: {
18          'src/*': 'src/*',
19          '*': 'node_modules/*'
20        },
21        packages: {
22          'src/app': {
23            main: 'app.js',
24            defaultExtension: 'js'
25          },
26          '@angular/common': {
27            main: 'index.js',
28            defaultExtension: 'js'
29          },
30          '@angular/compiler': {
31            main: 'index.js',
32            defaultExtension: 'js'
33          },
34          '@angular/core': {
35            main: 'index.js',
36            defaultExtension: 'js'
37          },
38          '@angular/platform-browser': {
39            main: 'index.js',
40            defaultExtension: 'js'
41          },
42          '@angular/platform-browser-dynamic': {
43            main: 'index.js',
44            defaultExtension: 'js'
45          },
46          'rxjs': {
47            defaultExtension: 'js'
48          }
49        }
50      });
51      System.import('src/app');
52    </script>
53  </head>
54  <body>
55    <my-app></my-app>
56  </body>
57</html>

If you refresh, you’ll see ‘Hello World’ on the screen. You just wrote your first Angular 2 component!

With Angular rc.1, since there are distinct barrels, we need to configure SystemJS to know about these packages. This is not too different than configuring SystemJS for any 3rd party library. You could keep this in a separate config file (as seen on the quickstart guide) but for example purposes, I’ve included the whole config inline here.

The @Component call is a decorator – you can think of it as “decorating” the AppComponent class with additional properties. By decorating the class using the @Component decorator, we specify that the AppComponent is an Angular component.

We access the methods and properties we need from Angular 2 using the ES6 modules syntax, which you can read more about here.

Our App Component

First, let’s build the template for this component in app/app.html:

1<div id="app">
2  <div id="search-form">
3    <form (ngSubmit)="newSubscription()">
4      <input [(ngModel)]="newSearchTerm" placeholder="JavaScript" />
5      <button>Search</button>
6    </form>
7  </div>
8  <ul id="channels-list">
9    <li class="channel" *ngFor="let channel of channels">
10        <h2>Tweets for {{ channel }}</h2>
11        ...tweets will be displayed here in due course...
12    </li>
13  </ul>
14</div>

Some interesting changes from the earlier betas to note here is the switch to camelCase attribute bindings, which allows Angular2 templates to be case sensitive. Also the usage of let channel of channels for the *ngFor for variable names, instead of #channel which is no longer supported here.

Our template contains a form for adding a new search term and a list that will be populated with results for each search term.

1import {Component} from '@angular/core';
2
3@Component({
4  selector: 'my-app',
5  templateUrl: 'src/app/app.html'
6})
7class AppComponent {
8  private newSearchTerm: string;
9  private channels: any[];
10
11  constructor() {
12    this.channels = [];
13  }
14
15  public newSubscription() {
16    this.channels.push({term: this.newSearchTerm, active: true});
17    this.newSearchTerm = '';
18  }
19}

In app.component.ts we create newSubscription, which will be called when the user submits the form. We bind to the submit event of the form using the ngSubmit directive. The app component also has a channels property, which is an array of objects. This will contain all the search terms the user has searched for, whether they are active, which in turn will be used as the names of Pusher channels that will be updated with search results.

Adding realtime with Pusher

To keep things simple in this tutorial, we’ll just add the Pusher script tag into our index.html file:

1<script src="https://js.pusher.com/3.0/pusher.min.js"></script>

First we need to tell TypeScript that Pusher exists as a global variable, otherwise we’ll get errors from the compiler. Add the line below to the top of app.component.ts:

1declare var Pusher: any;

Secondly, in the constructor we’ll create a new instance of the Pusher library. You can find your app key from the Pusher dashboard.

1constructor() {
2  this.pusher = new Pusher('YOUR_APP_KEY_HERE');
3  this.channels = [];
4}

We’re now ready to create the subscription component, which will subscribe to a Pusher channel and be responsible for displaying new tweets.

Subscription Component

This component will take a search term and the Pusher client instance from app.component.ts. It will then bind to the new_tweet event on the Pusher channel for that search term, and render new tweets onto the screen as they come in from Pusher. To do this we need to update app.html to render the component:

1<div id="channels-list">
2  <div class="channel" *ngFor="let channel of channels">
3    <h3>Tweets for {{ channel.term }}</h3>
4    <subscription [search]="channel" [pusher]="pusher"></subscription>
5  </div>
6</div>

Each subscription component will be passed in the channel that it should subscribe to, and the pusher instance. The [search]="channel" syntax we see above denotes that the subscription component takes a search parameter as an expression. Here the "channel" expression is evaluated to return the current channel that we’re looping over in our *ngFor loop.

We’ll create the component in a new file, app/subscription.component.ts. Notice the use of the decorator @Input which denotes that the value is being given to a component, and the life cycle hooks OnInit, OnDestroy and AfterViewChecked. You can find these documented under “Lifecycle Hooks” in the API Preview.

Our subscription.component.ts looks like so:

1import {
2  Component,
3  Input,
4  AfterViewChecked,
5  OnInit
6} from '@angular/core';
7
8@Component({
9  moduleId: module.id,
10  selector: 'subscription',
11  templateUrl: 'subscription.component.html'
12})
13export default class SubscriptionComponent implements OnInit, AfterViewChecked {
14  @Input() search: any;
15  @Input() pusher;
16  public tweets : Object[];
17  private channel;
18  private className: String;
19
20  public ngOnInit() {
21    this.subscribeToChannel();
22    this.tweets = [];
23    this.className = this.search.term.replace(' ', '-');
24  }
25
26  private subscribeToChannel() {
27    this.channel = this.pusher.subscribe(btoa(this.search.term));
28    this.channel.bind('new_tweet', (data) => {
29      this.newTweet(data);
30    });
31    this.subscribed = true;
32  }
33
34  private newTweet(data: Object) {
35    this.tweets.push(data);
36  }
37
38  public ngAfterViewChecked() {
39    var listItem = document.querySelector(".channel-" + this.className);
40    if (listItem) {
41      listItem.scrollTop = listItem.scrollHeight;
42    }
43  }
44}

And our subscription.component.html is pretty straight forward:

1<ul class="channel-results channel-{{className}}">
2  <li *ngFor="let result of tweets">
3    <p class="white" [innerHTML]="result.tweet.text"></p>
4  </li>
5</ul>

There are some important bits to notice in the above code. Firstly, when we subscribe to the channel name using Pusher we first encode it as Base64.

1this.channel = this.pusher.subscribe(btoa(this.search.term));

The reason for this is that Pusher only allows certain characters to be used in channel names, and we might want our Twitter search query to include other characters. By encoding we avoid this problem, and then the server decodes the name before searching Twitter.

When a new message comes in and the view is updated we need to scroll the window down so that the latest tweets are always displayed and the user doesn’t have to scroll. To do this we can make use of one of the many lifecycle hooks Angular provides. ngAfterViewChecked is run after each time the view has been checked and potentially updated, so it’s the perfect time to perform some DOM manipulation as we know the view is in sync with the data.

You may be wondering why we have to use another lifecycle hook, ngOnInit, to subscribe to our channel, rather than the constructor. We do this because when the constructor is called the component hasn’t been given access to its inputs. Therefore, we use the ngOnInit hook, which Angular calls once it has finished setting up the component.

With our subscription component implemented, we have a working live Twitter search! If you’d like to check out the final code and the CSS written to make the app look like the below screenshot, you can find all the code on GitHub, or try the app for yourself.

Pusher and Angular 2 realtime demo

Conclusion

In this tutorial we built a realtime Angular 2 app that shows a realtime stream of tweets for keyword searches. It demonstrates that whilst Angular 2 may not yet be released or even in beta, that it doesn’t mean you shouldn’t start experimenting with it to see if you enjoy working with the framework. Whilst syntax and features may change, larger aspects such as the move to TypeScript and the use of decorators are here to stay and being familiar with them ahead of the release of Angular 2 is no bad thing. Angular 2 is a definite improvement on Angular 1 and makes it even easier to integrate other services such as Pusher into your application, making it a great fit for realtime. We’d love to hear what you think about Angular 2, so feel free to tweet us with your thoughts.

Huge thanks go to Nathan Walker for updating the above blog post and code repository to the latest version of Angular 2.

Further Reading