Creating a Dumb Component to Display GIFs
As usual, we are going to start by implementing a feature that moves us toward the main goal of the application as quick as possible. This is an application that displays GIFs, so we are going to start by creating our dumb/presentational component that handles displaying the GIFs.
Remember, in this application build I am going to go very light on the instructions and explanations. In some places I will be intentionally vague and leave steps out (e.g. like not reminding you to import dependencies for a particular component we built). If you forget to do this you should run into errors, and this will be good practice in reading and trying to decipher the errors. Again, I want to reiterate that this is not because I expect this to be easy for you now. I expect that this will likely cause most people to get stuck on things, and that is the point.
Whenever we are implementing something we haven’t seen before, I will still explain it thoroughly.
Create an interface for GIFs
Create the following interface at
src/app/shared/interfaces/gif.ts
:
export interface Gif {
src: string;
author: string;
name: string;
permalink: string;
title: string;
thumbnail: string;
comments: number;
}
This might seem like a little bit more than we usually do. We will not be making use of all of these fields immediately, but eventually this is all of the data that we are going to want to retrieve for a GIF from Reddit.
We are also going to be creating a lot of different interfaces for this
application, so for convenience sake, we are going to create a single index.ts
file that we can use to import any of our interfaces by exporting them all from
that file.
Create a file at
src/app/shared/interfaces/index.ts
and add the following:
export * from './gif';
Any time we add a new interface, we will export it from this file.
Create the GifListComponent
Create a file at
src/app/home/ui/gif-list.component.ts
and add the following:
import { Component, input } from '@angular/core';
import { Gif } from '../../shared/interfaces';
@Component({
selector: 'app-gif-list',
template: `
@for (gif of gifs(); track gif.permalink){
<div>
{{ gif.title }}
</div>
}
`,
})
export class GifListComponent {
gifs = input.required<Gif[]>();
}
For now, all we are doing is looping through the supplied gifs
and rendering
the title. We just want to get some data displaying in the application first,
but soon we will come back to this component and extend it to actually display
the gifs.
This will involve creating another dumb component that will fill the role of displaying the gifs/videos.
Create the Reddit Service
Before we can use our new component, we are going to need to be able to fetch the data we need to supply to it. We are going to do that now so that we can add it to our home page, but we will not be implementing the full Reddit API integration right away. For now, we will just return some dummy data.
Create a file at
src/app/shared/data-access/reddit.service.ts
and add the following:
import { Injectable, computed, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Gif } from '../interfaces';
import { of } from 'rxjs';
export interface GifsState {
gifs: Gif[];
}
@Injectable({ providedIn: 'root' })
export class RedditService {
// state
private state = signal<GifsState>({
gifs: [],
});
// selectors
gifs = computed(() => this.state().gifs);
// sources
gifsLoaded$ = of([
{
src: '',
author: '',
name: '',
permalink: '',
title: 'test gif',
thumbnail: '',
comments: 0,
},
]);
constructor() {
//reducers
this.gifsLoaded$.pipe(takeUntilDestroyed()).subscribe((gifs) =>
this.state.update((state) => ({
...state,
gifs: [...state.gifs, ...gifs],
}))
);
}
}
This is probably starting to look pretty standard by now — it’s our same basic
state set up again. This time we have a gifsLoaded$
source that will emit with
our gifs, and for now we are just creating some dummy data by creating an
observable of an array by using the of
creation operator from RxJS.
The of
operator will take whatever we supply it and create a stream that emits
that data. The purpose of this here, rather than just returning the data
directly, is to simulate the data being loaded via HttpClient
. The
HttpClient
needs to make an asynchronous request and returns the data as
a stream, so for now we are just creating a fake implementation of that where we
immediately return the data as a stream.
Whenever you run into RxJS operators you don’t understand — and this will happen a lot as it takes some time to get used to them — I would recommend looking up the operator on learnrxjs.io. There is also the official documentation at rxjs.dev but this is generally more on the technical side and likely better suited once you are more comfortable with RxJS. The Learn RxJS website has more examples that you can look at to see what is going on. Of course, ChatGPT is actually generally quite good at answering questions about RxJS operators too if you have access to that.
Later we will swap this out for a stream that actually loads the data from the API.
Adding the GifListComponent to the Home Page
I will still post the code for this one in a moment, but see if you can add the
component we just created to the template of the home component, and supply it
with the data it needs from the RedditService
.
Click here to reveal solution
Solution
Add the following to the
HomeComponent
:
import { Component, inject } from '@angular/core';
import { GifListComponent } from './ui/gif-list.component';
import { RedditService } from '../shared/data-access/reddit.service';
@Component({
selector: 'app-home',
template: `
<app-gif-list [gifs]="redditService.gifs()" class="grid-container" />
`,
imports: [GifListComponent],
})
export default class HomeComponent {
redditService = inject(RedditService);
}
Our component should now successfully be displaying on our home page with just
the test title for now. We have also added a class
to the component that we
will use for styling later.
Create the GifPlayerComponent
We’ve got the basic wiring in place now, but displaying our gifs will be a little bit more involved than just rendering some text as we are doing currently.
In fact, this is going to be the first time we will be managing complex state within a dumb component. Usually we deal with our state in services, but this component will have its state management built into the component itself.
The responsibility of this component will be to display the gifs (they are actually videos), but to do that we will need to handle:
- Rendering the video
- Loading the video when the user taps the video
- Playing the video when it has finished loading
- Displaying a loading spinner whilst the video is loading
- Pausing the video when the user taps the video (if it is playing)
We want to make sure all of this happens seamlessly — for example, if a user is spam tapping a video whilst it is loading we want to make sure we don’t get into any weird states that cause unwanted behaviour. This is why the robustness of our state management approach will work well here.
Since this component is quite complex, we are going to start with a basic outline, and then we will fill in some more details.
Create a file at
src/app/home/ui/gif-player.component.ts
and add the following:
import {
Component,
ElementRef,
input,
viewChild,
computed,
signal,
} from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Subject } from 'rxjs';
interface GifPlayerState {
playing: boolean;
status: 'initial' | 'loading' | 'loaded';
}
@Component({
selector: 'app-gif-player',
template: `
@if (status() === 'loading'){
<mat-progress-spinner mode="indeterminate" diameter="50" />
}
<div>
<video
(click)="togglePlay$.next()"
#gifPlayer
playsinline
preload="none"
[loop]="true"
[muted]="true"
[src]="src()"
></video>
</div>
`,
imports: [MatProgressSpinnerModule],
})
export class GifPlayerComponent {
src = input.required<string>();
thumbnail = input.required<string>();
videoElement = viewChild.required<ElementRef<HTMLVideoElement>>('gifPlayer');
videoElement$ = toObservable(this.videoElement);
state = signal<GifPlayerState>({
playing: false,
status: 'initial',
});
//selectors
playing = computed(() => this.state().playing);
status = computed(() => this.state().status);
// sources
togglePlay$ = new Subject<void>();
constructor() { }
}