3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 4

Creating a Dumb Component to Display GIFs

Our first component

STANDARD

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({
  standalone: true,
  selector: 'app-gif-list',
  template: `
    @for (gif of gifs; track gif.permalink){
      <div>
        {{ gif.title }}
      </div>
    }
  `,
})
export class GifListComponent {
  @Input({ required: true }) gifs!: 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.

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({
  standalone: true,
  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, filter } from 'rxjs';

interface GifPlayerState {
  playing: boolean;
  status: 'initial' | 'loading' | 'loaded';
}

@Component({
  standalone: true,
  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 {
  @Input({ required: true }) src!: string;
  @Input({ required: true }) thumbnail!: string;

  // Fake new signals API
  videoElement = signal<HTMLVideoElement | undefined>(undefined);
  @ViewChild('gifPlayer') set video(element: ElementRef<HTMLVideoElement>) {
    this.videoElement.set(element.nativeElement);
  }

  videoElement$ = toObservable(this.videoElement).pipe(
    filter((element): element is HTMLVideoElement => !!element)
  );

  state = signal<GifPlayerState>({
    playing: false,
    status: 'initial',
  });

  //selectors
  playing = computed(() => this.state().playing);
  status = computed(() => this.state().status);

  // sources
  togglePlay$ = new Subject<void>();

  constructor() { }
}
STANDARD
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).