Build a photo feed using Angular

Introduction

In this tutorial, we will build a simple web app that will display any images it receives using Pusher. More specifically, to build this realtime photo feed we will use the Pusher Realtime Reddit API to subscribe to new post in a specific subreddit and in realtime display each new post's image. Let's start by scaffolding our Angular web app.

Angular application scaffolding

We will be using Angular CLI, a command line tool maintained by Angular, to initialize our application.

First, you need to install Angular CLI globally on your machine. You can follow the official installation instructions on how to do that. Once you have installed Angular CLI, it is time to scaffold your application.

Navigate to a local directory where you want your application code to live, and within that directory, let's run a CLI command to generate a new Angular application.

$ ng new photo-feed --skip-tests

In the command above, photo-feed is the name of the app we are creating (feel free to use whatever name you like). Most CLI commands come with multiple flags and options, like --skip-tests above, which tells CLI not to create any test files for our application. You can type $ ng help to see what other options are there.

Once CLI is done generating files and installing dependencies, we can cd into the newly created directory that holds our application code. From within the root directory of our application, we can now run our application by typing $ ng serve. This command will boot up a development server and serve our application on localhost:4200 by default. Leave the server running, as it will be reloaded automatically once we make any changes to our application code. You can now navigate to localhost:4200 in your browser and you should see a message "app works!".

photo-feed-angular-app-works

Application structure

Before we jump into writing code, let's consider the structure of our application. Even though our application is small, we still want to use best practice.

Be default, our Angular application already has one main root component: AppComponent. On top of that, our application will only have one default route, where images will be displayed. Also, as we previously discussed, we will use Pusher to receive realtime data. Considering the features, a potential structure would be to create a new PhotoFeedComponent that will hold an array of images and display them, as well as a PusherService, whose responsibility would be to instantiate and hold a reference to an instance of Pusher.

Pusher

Because we're about to start using Pusher within our application, we need to install the pusher-js front end package as a dependency.

First sign up for a Pusher account here

$ npm i --save pusher-js

On top of installing and saving pusher-js as a dependency, we need to make sure it's loaded into the browser at runtime. We can easily do that by modifying the .angular-cli.json file and telling Angular CLI to take the pusher.min.js file from the node_modules and bundle it up with the rest of the code.

1// .angular-cli.json
2...
3  "scripts": [
4    "../node_modules/pusher-js/dist/web/pusher.min.js"
5  ],
6...

Angular CLI development server only listens for changes in the application code files, so you might need to restart the server for these changes to take effect. To restart the server simply stop the current process with CTRL + C and start it again with ng serve.

Pusher service

Angular CLI isn't just good for scaffolding the application boilerplate; it can also generate new components, services and other Angular entities whenever you need. Let's create a new PusherService with CLI.

$ ng generate service pusher

generate has an alias of g and service has an alias of s, therefore the command above could have also been written as ng g s pusher

Now that the PusherService is created, as with any other service we need to add it to our application providers. If we don't do this, we can't inject the service as a dependency anywhere in our application. Angular services need to be provided at a module level, so considering that our application only has one module at the moment - the AppModule, we need to add PusherService to the AppModule's providers.

1// app.module.ts
2
3import { PusherService } from './pusher.service';
4...
5@NgModule({
6  ...
7  providers: [PusherService],
8})
9export class AppModule {}

The PusherService's responsibility will be to create a new instance of Pusher and make it available for anyone to get hold of.

On our PusherService class we will have a public pusher property, which will hold the Pusher instance. Pusher instantiation will happen in the class's constructor.

1// pusher.service.ts
2...
3export class PusherService {
4  pusher: any;
5
6  constructor() {
7    this.pusher = new Pusher();
8  }
9}

At this point, the Typescript compiler will probably scream at you in a way of throwing an error, because it doesn't know what Pusher in new Pusher() is, as we haven't imported or declared it. The pusher-js library doesn't have typings, therefore we can't import it into our code. Instead, what we can do is declare a constant named Pusher at the top of the file, so that Typescript thinks we're referring to that. In reality, however, because pusher-js library is loaded into the browser, it attaches Pusher to the window and that's how we can use it in our code. So at the top of the pusher.service.ts file, just add the following.

declare const Pusher;

When instantiating Pusher, we need to pass in the Pusher application key of a specific Pusher application that we want to connect to. In our case, since we are connecting to a public Reddit API, their application key is known and is: 50ed18dd967b455393ed. So our Pusher instantiation logic evolves into:

1// pusher.service.ts
2...
3this.pusher = new Pusher('50ed18dd967b455393ed');
4...

From now on, any other component and service that needs to get access to the Pusher instance can use the PusherService.pusher property to do so.

Photo feed component

As per our application structure, we have decided that the PhotoFeedComponent will have an array of image URLs. In the view, we will loop over that array and display the individual images.

Let us start by generating the new component with CLI.

ng generate component photo-feed

First of all, we should create a property where we will be storing the images. Since all we will be storing are image URLs, we can use an array of strings as the data type for our property.

1// photo-feed.component.ts
2...
3export class PhotoFeedComponent implements OnInit {
4  images: Array<string> = [];
5
6  ...
7}

The images array will be populated over time whenever a new image is received, so we start with a blank array.

Now that we have a place to store the images, let's write out how we are going to be receiving these images. We need to get ahold of the Pusher instance that we have in the PusherService. We do that by injecting the PusherService as a dependency inside our PhotoFeedComponent class. As with any other dependency, we do that in the constructor.

1// photo-feed.component.ts
2...
3export class PhotoFeedComponent implements OnInit {
4  images: Array<string> = [];
5
6  constructor(private pusherService: PusherService) {}
7
8  ...
9}

Finally, we can subscribe to a Pusher channel and start receiving images. All we need to subscribe, is the name of the channel. In the Pusher Realtime Reddit API, each subreddit is a separate channel. Considering that we are after some good looking images, we should subscribe to a suitable subreddit, where most posts come with images, like "r/pics", for example.

Because we want to subscribe to a subreddit on startup, or whenever our component is created, we will use one of Angular component's lifecycle hooks, called ngOnInit. As you might have guessed from its name, ngOnInit is run on component initialization, which is exactly when we want to subscribe.

1// photo-feed.component.ts
2...
3export class PhotoFeedComponent implements OnInit {
4  ...
5
6  ngOnInit() {
7    const channel = this.pusherService.pusher.subscribe('pics');
8  }
9}

Pusher's subscribe() method returns a channel that we store in a variable.

Now that we have subscribed to "r/pics", we can start listening for specific events on that channel. In our case, since we want a constant feed of new images, we can use the new-listing event. new-listing signifies a new post in a subreddit.

Using the channel variable, let's start listening to the new-listing event.

1// photo-feed.component.ts
2...
3export class PhotoFeedComponent implements OnInit {
4  ...
5  ngOnInit() {
6    const channel = this.pusherService.pusher.subscribe('pics');
7    channel.bind('new-listing', (listing) => {});
8  }
9}

The second parameter to the bind method is a callback function, that will be called whenever the new-listing event is received with the payload of the received event.

Now that we receive every new listing in a subreddit, we can extract images out of each post and add them to our images array. And since we want to do that on every new listing, we want to do it in the callback function.

1// photo-feed.component.ts
2...
3export class PhotoFeedComponent implements OnInit {
4  images: Array<string> = [];
5  ...
6  ngOnInit() {
7    ...
8    channel.bind('new-listing', (listing) => {
9      const image = listing.url;
10      this.images = [image, ...this.images];
11    });
12  }
13}

Note that sometimes there aren't that many new posts, so it might take a minute or two between the new-listing events.

The final step is to loop over and display the images in the HTML. We can easily do that with Angular's ngFor directive.

1// photo-feed.component.html
2<div *ngFor="let image of images">
3  <img [src]="image">
4</div>

Using the ngFor syntax we are telling Angular, that we want to loop over the images array, assigning each individual item in that array to an image variable. We then use the image variable to feed into the src of the HTML image tag.

Our code is complete! So if your development server is still running, you can navigate to localhost:4200 and see the final result.

Reminder: you can start the development server by running $ ng serve in your terminal.

photo-feed-angular-demo

Congratulations on your working realtime photo feed!