Build a photo feed using Angular
A basic understanding of TypeScript is needed to follow this tutorial.
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!”.
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.
// .angular-cli.json
...
"scripts": [
"../node_modules/pusher-js/dist/web/pusher.min.js"
],
...
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 ofg
andservice
has an alias ofs
, therefore the command above could have also been written asng 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.
// app.module.ts
import { PusherService } from './pusher.service';
...
@NgModule({
...
providers: [PusherService],
})
export 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.
// pusher.service.ts
...
export class PusherService {
pusher: any;
constructor() {
this.pusher = new Pusher();
}
}
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:
// pusher.service.ts
...
this.pusher = new Pusher('50ed18dd967b455393ed');
...
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.
// photo-feed.component.ts
...
export class PhotoFeedComponent implements OnInit {
images: Array<string> = [];
...
}
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.
// photo-feed.component.ts
...
export class PhotoFeedComponent implements OnInit {
images: Array<string> = [];
constructor(private pusherService: PusherService) {}
...
}
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.
// photo-feed.component.ts
...
export class PhotoFeedComponent implements OnInit {
...
ngOnInit() {
const channel = this.pusherService.pusher.subscribe('pics');
}
}
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.
// photo-feed.component.ts
...
export class PhotoFeedComponent implements OnInit {
...
ngOnInit() {
const channel = this.pusherService.pusher.subscribe('pics');
channel.bind('new-listing', (listing) => {});
}
}
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.
// photo-feed.component.ts
...
export class PhotoFeedComponent implements OnInit {
images: Array<string> = [];
...
ngOnInit() {
...
channel.bind('new-listing', (listing) => {
const image = listing.url;
this.images = [image, ...this.images];
});
}
}
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.
// photo-feed.component.html
<div *ngFor="let image of images">
<img [src]="image">
</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.
Congratulations on your working realtime photo feed!
24 May 2017
by Kirils Ladovs