Build a Dynamic Watchlist for Your Web App with Angular & GraphQL (Part 6)
Tags: angular, development, web, fullstack
Published: 2024-07-08 15:57:12

---
title: Build a Dynamic Watchlist for Your Web App with Angular & GraphQL (Part 6)
published: true
date: 2024-07-01 13:38:30 UTC
tags: angular, development, web, fullstack
canonical_url: https://www.syncfusion.com/blogs/post/build-dynamic-watchlist-angular-graphql
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q3rblnm2hn293xnf9fyc.jpeg
---
**TL;DR:** Implement a watchlist feature in your Angular application using the AddToWatchlist component. Integrate it with a Watchlist component via GraphQL. Learn step-by-step with code examples and best practices.

In the [previous article](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-5 "Blog: A Full-Stack Web App Using Angular and GraphQL: Adding Login and Authorization Functionalities (Part 5)") of this series, we learned how to add login functionality and implement role-based authorization in our application.

In this article, we will cover how to implement a watchlist feature similar to the wishlist feature of e-commerce apps. When a user visits the movie details page, we will add a feature to display a list of movies from the same genre.

## Create the IWatchlist interface

First, create a new file named **IWatchlist.cs** in the **MovieApp\Interfaces** folder with the following method.

```js
public interface IWatchlist
{
    Task ToggleWatchlistItem(int userId, int movieId);

    Task<string> GetWatchlistId(int userId);

    Task ClearWatchlist(int userId);
}
```

## Create the watchlist data access layer

Second, implement the data access layer for the watchlist functionality in a file named **WatchlistDataAccessLayer.cs** inside the **MovieApp.Server\DataAccess** folder. Add the following code inside it.

```js
public class WatchlistDataAccessLayer: IWatchlist
{
    readonly MovieDbContext _dbContext;
    public WatchlistDataAccessLayer(IDbContextFactory<MovieDbContext> dbContext)
    {
        _dbContext = dbContext.CreateDbContext();
    }

    public async Task<string> GetWatchlistId(int userId)
    {
        try
        {
            Watchlist? watchlist = await _dbContext.Watchlists.FirstOrDefaultAsync(x => x.UserId == userId);

            if (watchlist is not null)
            {
                return watchlist.WatchlistId;
            }
            else
            {
                return await CreateWatchlist(userId);
            }

        }
        catch
        {
            throw;
        }
    }

    public async Task ToggleWatchlistItem(int userId, int movieId)
    {
        string watchlistId = await GetWatchlistId(userId);

        WatchlistItem? existingWatchlistItem = await _dbContext.WatchlistItems
            .FirstOrDefaultAsync(x => x.MovieId == movieId && x.WatchlistId == watchlistId);

        if (existingWatchlistItem is not null)
        {
            _dbContext.WatchlistItems.Remove(existingWatchlistItem);
        }
        else
        {
            WatchlistItem watchlistItem = new()
            {
                WatchlistId = watchlistId,
                MovieId = movieId,
            };

            _dbContext.WatchlistItems.Add(watchlistItem);
        }
        await _dbContext.SaveChangesAsync();
    }

    public async Task ClearWatchlist(int userId)
    {
        try
        {
            string watchlistId = await GetWatchlistId(userId);
            List<WatchlistItem> watchlistItem = _dbContext.WatchlistItems.Where(x => x.WatchlistId == watchlistId).ToList();

            if (!string.IsNullOrEmpty(watchlistId))
            {
                foreach (WatchlistItem item in watchlistItem)
                {
                    _dbContext.WatchlistItems.Remove(item);
                    await _dbContext.SaveChangesAsync();
                }
            }
        }
        catch
        {
            throw;
        }
    }

    async Task<string> CreateWatchlist(int userId)
    {
        try
        {
            Watchlist watchlist = new()
            {
                WatchlistId = Guid.NewGuid().ToString(),
                UserId = userId,
                DateCreated = DateTime.Now.Date
            };

            await _dbContext.Watchlists.AddAsync(watchlist);
            await _dbContext.SaveChangesAsync();

            return watchlist.WatchlistId;
        }
        catch
        {
            throw;
        }
    }

}
```

The **WatchlistDataAccessLayer** class implements the **IWatchlist** interface and utilizes an **IDbContextFactory<MovieDbContext>** in its constructor to instantiate a **MovieDbContext**, enabling seamless interaction with the database.

The **GetWatchlistId** is an asynchronous method for retrieving the watchlist ID for a given user ID. If a watchlist for the user doesn’t exist, it creates a new one.

The **ToggleWatchlistItem** is an asynchronous method for adding or removing a movie from a user’s watchlist. If the movie is already on the watchlist, it will be removed; otherwise, it will be added.

The **ClearWatchlist** method asynchronously removes all movies from a user’s watchlist.

The **CreateWatchlist** is a private asynchronous method for creating a new watchlist for a user. It generates a new GUID for the watchlist ID, sets the user ID, and sets the creation date as the current date.

In each method, database changes are saved using the asynchronous **SaveChangesAsync** method, which records all modifications made within the database context. The try-catch blocks within the methods ensure any potential exceptions during database interactions are handled smoothly.

## Update the IMovie interface

Update the **IMovie** interface by adding the method declaration like in the following code example in the **IMovie.cs** file.

```js
public interface IMovie
{
    // other methods.
    Task<List<Movie>> GetSimilarMovies(int movieId);

    Task<List<Movie>> GetMoviesAvailableInWatchlist(string watchlistId);
}
```

## Update the MovieDataAccessLayer class

Update the **MovieDataAccessLayer** class by implementing the newly added methods of the **IMovie** interface.

```js
public async Task<List<Movie>> GetSimilarMovies(int movieId)
{
    List<Movie> lstMovie = new();
    Movie? movie = await _dbContext.Movies.FindAsync(movieId);

    if (movie is not null)
    {
        lstMovie = _dbContext.Movies.Where(m => m.Genre == movie.Genre && m.MovieId != movie.MovieId)
        .OrderBy(u => Guid.NewGuid())
        .Take(5)
        .ToList();
    }
    return lstMovie;
}

public async Task<List<Movie>> GetMoviesAvailableInWatchlist(string watchlistID)
{
    try
    {
        List<Movie> userWatchlist = new();
        List<WatchlistItem> watchlistItems =
            _dbContext.WatchlistItems.Where(x => x.WatchlistId == watchlistID).ToList();

        foreach (WatchlistItem item in watchlistItems)
        {
            Movie? movie = await GetMovieData(item.MovieId);

            if (movie is not null)
            {
                userWatchlist.Add(movie);
            }
        }
        return userWatchlist;
    }
    catch
    {
        throw;
    }
}
```

The asynchronous method **GetSimilarMovies** retrieves a list of movies with the same genre as the selected movie. Identified by the **movieId** parameter, it queries the database to find the corresponding Movie object and stores the result in the movie variable. If the movie object is not null, it performs another query to find all the movies with the same genre but different MovieId, ensuring the list excludes the selected movie itself. The results are then randomly ordered using GUIDs to shuffle the titles. Finally, it selects and returns up to five movies from this shuffled list that are similar to the selected movie.

The **GetMoviesAvailableInWatchlist** method asynchronously retrieves all movies in a specific watchlist identified by the **watchlistID** parameter. It queries the database to get all **WatchlistItem** objects where the **WatchlistId** matches the provided **watchlistID** and stores the result in the **watchlistItems** list. The method then iterates over each watchlist item in the list, calling the **GetMovieData** method with the **MovieId** of each item to get the corresponding **Movie** object. If the **Movie** object is not null, it adds the movie to the **userWatchlist**. Finally, it returns the **userWatchlist**.

## Add the GraphQL server query resolver for the watchlist

Add a class named **WatchlistQueryResolver.cs** inside the **MovieApp\GraphQL** folder. Refer to the following code example.

```js
[ExtendObjectType(typeof(MovieQueryResolver))]
public class WatchlistQueryResolver
{
    readonly IWatchlist _watchlistService;
    readonly IMovie _movieService;
    readonly IUser _userService;

    public WatchlistQueryResolver(IWatchlist watchlistService, IMovie movieService, IUser userService)
    {
        _watchlistService = watchlistService;
        _movieService = movieService;
        _userService = userService;
    }

    [Authorize]
    [GraphQLDescription("Get the user Watchlist.")]
    public async Task<List<Movie>> GetWatchlist(int userId)
    {
        bool user = await _userService.IsUserExists(userId);

        if (user)
        {
            string watchlistid = await _watchlistService.GetWatchlistId(userId);
            return await _movieService.GetMoviesAvailableInWatchlist(watchlistid);
        }
        else
        {
            return new List<Movie>();
        }
    }
}
```

The **WatchlistQueryResolver** class has three private, read-only fields: **\_watchlistService**, **\_movieService**, and **\_userService**. These fields are instances of the **IWatchlist**, **IMovie**, and **IUser** interfaces, respectively, and they are initialized via dependency injection in the constructor. This class extends the **MovieQueryResolver** type, adding additional fields to the existing GraphQL type.

The **GetWatchlist** method checks if a user with the provided **userId** exists by calling the **IsUserExists** method of **\_userService**. The result is stored in the user variable. If the user exists, it gets the ID of the user’s watchlist by calling the **GetWatchlistId** method of **\_watchlistService**. It then retrieves the movies available in the watchlist by calling the **GetMoviesAvailableInWatchlist** method of **\_movieService** with the watchlist ID. If the user does not exist, it returns an empty list of **Movie** objects. This method is decorated with the **Authorize** attribute, which requires the client to be authenticated to call this method.

## Add the GraphQL server mutation resolver for the watchlist

Add a class named **WatchlistMutationResolver.cs** inside the **MovieApp\GraphQL** folder. Refer to the following code example.

```js
[ExtendObjectType(typeof(MovieMutationResolver))]
public class WatchlistMutationResolver
{
    readonly IWatchlist _watchlistService;
    readonly IMovie _movieService;
    readonly IUser _userService;

    public WatchlistMutationResolver(IWatchlist watchlistService, IMovie movieService, IUser userService)
    {
        _watchlistService = watchlistService;
        _movieService = movieService;
        _userService = userService;
    }

    [Authorize]
    [GraphQLDescription("Toggle Watchlist item.")]
    public async Task<List<Movie>> ToggleWatchlist(int userId, int movieId)
    {
        await _watchlistService.ToggleWatchlistItem(userId, movieId);

        bool user = await _userService.IsUserExists(userId);

        if (user)
        {
            string watchlistid = await _watchlistService.GetWatchlistId(userId);
            return await _movieService.GetMoviesAvailableInWatchlist(watchlistid);
        }
        else
        {
            return new List<Movie>();
        }
    }

    [Authorize]
    [GraphQLDescription("Delete all items from Watchlist.")]
    public async Task<int> ClearWatchlist(int userId)
    {
        await _watchlistService.ClearWatchlist(userId);
        return userId;
    }
}
```

The **WatchlistMutationResolver** class has three private read-only fields: **\_watchlistService**, **\_movieService**, and **\_userService**. These fields are instances of **IWatchlist**, **IMovie**, and **IUser** interfaces, respectively, and they are initialized via dependency injection in the constructor.

The **ToggleWatchlist** method is an asynchronous method that toggles a movie in a user’s watchlist and then returns an updated list of movies in the watchlist. It’s decorated with the **Authorize** attribute, which means it requires the client to be authenticated to call this method. The **GraphQLDescription** attribute provides a description for this field in the GraphQL schema.

The **ClearWatchlist** method is another asynchronous method that deletes all items from a user’s watchlist and then returns the user’s ID. It’s also decorated with the **Authorize** and **GraphQLDescription** attributes.

## Add a GraphQL server query to get similar movies

Add the following method to the **MovieQueryResolver** class.

```js
[GraphQLDescription("Gets the list of movies which belongs to the same genre as the movie whose movieId is passed as parameter.")]
public async Task<List<Movie>> GetSimilarMovies(int movieId)
{
    return await _movieService.GetSimilarMovies(movieId);
}
```

This asynchronous method returns a list of **Movie** objects with the same genre as the given movie, identified by the **movieId** parameter.

## Configure the Program.cs file

Since we have added new GraphQL resolvers, we need to register them in our middleware.

Update the following code example in the **Program.cs** file.

```js
builder.Services.AddGraphQLServer()
       .AddAuthorization()
       .AddQueryType<MovieQueryResolver>()
       .AddTypeExtension<WatchlistQueryResolver>()
       .AddMutationType<MovieMutationResolver>()
       .AddTypeExtension<AuthMutationResolver>()
       .AddTypeExtension<WatchlistMutationResolver>()
       .AddFiltering()
       .AddErrorFilter(error =>
       {
          return error;
       });
```

We use the **AddTypeExtension** method to register new resolvers of the types **WatchlistQueryResolver** and **WatchlistMutationResolver**.

We will register the transient lifetime of the **IWatchlist** service using the following code.

```js
builder.Services.AddTransient<IWatchlist, WatchlistDataAccessLayer>();
```

With the server configuration complete, we can now move to the client side of the app.

## Add the GraphQL query

Add the following GraphQL queries to the **src\app\GraphQL\query.ts** file.

```js
export const GET_SIMILAR_MOVIE = gql`
  query FetchSimilarMovies($movieId: Int!) {
    similarMovies(movieId: $movieId) {
      movieId
      title
      posterPath
      genre
      rating
      language
      duration
      overview
    }
  }
`;

export const GET_WATCHLIST = gql`
  query FetchWatchList($userId: Int!) {
    watchlist(userId: $userId) {
      movieId
      title
      posterPath
      genre
      rating
      language
      duration
    }
  }
`;
```

The code uses the  **gql**  tag from the  **graphql-tag**  library to define two GraphQL queries as described:

- **GET\_SIMILAR\_MOVIE:** This query is named **FetchSimilarMovies** and it takes a single parameter, **$movieId**, of type **Int!.** The **‘!’** indicates that this parameter is required. The query fetches similar movies to the given movie ID, returning several fields for each similar movie, such as **movieId**, **title**, **posterPath**, **genre**, **rating**, **language**, **duration**, and **overview**.
- **GET\_WATCHLIST:** This query is named **FetchWatchList** and takes a single parameter, **$userId**, of type **Int!.** It fetches the watchlist for the given user ID and returning several fields for each movie in the watchlist, such as **movieId**, **title**, **posterPath**, **genre**, **rating**, **language**, and **duration**.

## Add the GraphQL mutation

Add the following GraphQL mutation in the **src\app\GraphQL\mutation.ts** file.

```js
export const TOGGLE_WATCHLIST = gql`
  mutation toggleUserWatchlist($userId: Int!, $movieId: Int!) {
    toggleWatchlist(userId: $userId, movieId: $movieId) {
      movieId
      title
      posterPath
      genre
      rating
      language
      duration
    }
  }
`;

export const CLEAR_WATCHLIST = gql`
  mutation clearWatchlist($userId: Int!) {
    clearWatchlist(userId: $userId)
  }
`;
```

The code uses the  **gql**  tag from the  **graphql-tag**  library to define two GraphQL mutations as described below:

- **TOGGLE\_WATCHLIST:** This mutation is named  **toggleUserWatchlist**,  and it takes two parameters,  **$userId**  and  **$movieId**, of type  **Int!**. The  **‘!’** indicates that these parameters are required. The mutation toggles a movie’s watchlist status for a user. If the movie is already on the user’s watchlist, it will be removed. If it’s not, it will be added. The mutation returns several fields for the toggled movie: **movieId**,  **title**,  **posterPath**,  **genre**,  **rating**,  **language**, and  **duration**.
- **CLEAR\_WATCHLIST:** This mutation is named  **clearWatchlist** and takes a single parameter, **$userId**, of type **Int!**. It clears the watchlist for the given user ID, removing all movies from it.

## Create the client side model

Add the following type definitions to the **movie.ts** file under the **src\app\models\movie.ts** folder.

```js
export type SimilarMovieType = {
  similarMovies: Movie[];
};

export type WatchlistType = {
  watchlist: Movie[];
};

export type ToggleWatchlistType = {
  toggleWatchlist: Movie[];
};
```

## Create the required GraphQL services

### Generate the fetch similar movies service

Run the following command in the **ClientApp** folder to generate a service file.

```
ng g s services\fetch-similar-movies
```

Add the following code to the **fetch-similar-movies.service.ts** file.

```js
import { Injectable } from '@angular/core';
import { Query } from 'apollo-angular';
import { SimilarMovieType } from '../models/movie';
import { GET_SIMILAR_MOVIE } from '../GraphQL/query';

@Injectable({
  providedIn: 'root',
})
export class FetchSimilarMoviesService extends Query<SimilarMovieType> {
  document = GET_SIMILAR_MOVIE;
}
```

This service performs a GraphQL query to fetch a list of similar movies for a given movie ID.

### Generate the fetch watchlist service

Run the following command to generate a new service file.

```
ng g s services\fetch-watchlist
```

Add the following code to the **fetch-watchlist.service.ts** file.

```js
import { Injectable } from '@angular/core';
import { Query } from 'apollo-angular';
import { GET_WATCHLIST } from '../GraphQL/query';
import { WatchlistType } from '../models/movie';

@Injectable({
  providedIn: 'root',
})
export class FetchWatchlistService extends Query<WatchlistType> {
  document = GET_WATCHLIST;
}
```

This service performs a GraphQL query to fetch the watchlist for a logged-in user.

### Generate the toggle watchlist service

Run the following command to generate a new service file.

```
ng g s services\toggle-watchlist
```

Add the following code to the **toggle-watchlist.service.ts** file.

```js
import { Injectable } from '@angular/core';
import { Mutation } from 'apollo-angular';
import { TOGGLE_WATCHLIST } from '../GraphQL/mutation';
import { ToggleWatchlistType } from '../models/movie';

@Injectable({
  providedIn: 'root',
})
export class ToggleWatchlistService extends Mutation<ToggleWatchlistType> {
  document = TOGGLE_WATCHLIST;
}
```

This service performs a GraphQL mutation to toggle the addition and removal of movies from the watchlist.

### Generate the clear watchlist service

Run the following command to generate a new service file.

```
ng g s services\clear-watchlist
```

Add the following code to the **clear-watchlist.service.ts** file.

```js
import { Injectable } from '@angular/core';
import { CLEAR_WATCHLIST } from '../GraphQL/mutation';
import { Mutation } from 'apollo-angular';

@Injectable({
  providedIn: 'root',
})
export class ClearWatchlistService extends Mutation {
  document = CLEAR_WATCHLIST;
}
```

This service performs a GraphQL mutation to clear the watchlist, i.e., remove all the items from it.

The next step is to update the **SubscriptionService** class in the **src\app\services\subscription.service.ts** file by adding the following two properties.

```js
export class SubscriptionService {
  // existing code.
  watchlistItemcount$ = new BehaviorSubject<number>(0);
  watchlistItem$ = new BehaviorSubject<Movie[]>([]);
}
```

Run the following command to generate a new service file.

```
ng g s services\watchlist
```

Add the following code to the **watchlist.service.ts** file.

```js
import { Injectable } from '@angular/core';
import { SubscriptionService } from './subscription.service';
import { FetchWatchlistService } from './fetch-watchlist.service';
import { ToggleWatchlistService } from './toggle-watchlist.service';
import { map } from 'rxjs';
import { Movie } from '../models/movie';
import { ClearWatchlistService } from './clear-watchlist.service';

@Injectable({
  providedIn: 'root',
})
export class WatchlistService {
  constructor(
    private readonly subscriptionService: SubscriptionService,
    private readonly fetchWatchlistService: FetchWatchlistService,
    private readonly toggleWatchlistService: ToggleWatchlistService,
    private readonly clearWatchlistService: ClearWatchlistService
  ) {}

  getWatchlistItems(userId: Number) {
    return this.fetchWatchlistService
      .watch(
        {
          userId: Number(userId),
        },
        {
          fetchPolicy: 'network-only',
        }
      )
      .valueChanges.pipe(
        map((result) => {
          if (result.data) {
            this.setWatchlist(result.data?.watchlist);
          }
        })
      );
  }

  toggleWatchlistItem(userId: number, movieId: number) {
    return this.toggleWatchlistService
      .mutate(
        {
          userId: Number(userId),
          movieId: Number(movieId),
        },
        {
          fetchPolicy: 'network-only',
        }
      )
      .pipe(
        map((result) => {
          if (result.data) {
            this.setWatchlist(result.data?.toggleWatchlist);
          }
        })
      );
  }

  clearWatchlist(userId: number) {
    return this.clearWatchlistService
      .mutate(
        {
          userId: Number(userId),
        },
        {
          fetchPolicy: 'network-only',
        }
      )
      .pipe(
        map((result) => {
          if (result.data) {
            this.setWatchlist([]);
          }
        })
      );
  }

  private setWatchlist(watchList: Movie[]) {
    this.subscriptionService.watchlistItemcount$.next(watchList.length);
    this.subscriptionService.watchlistItem$.next(watchlist);
  }
}
```

The constructor of the **WatchlistService** class injects four services into the component: **SubscriptionService**, **FetchWatchlistService**, **ToggleWatchlistService**, and **ClearWatchlistService**. These services manage the user’s watchlist.

The **getWatchlistItems** method fetches the watchlist items for a user by calling the **watch** method on **fetchWatchlistService** with the **userId** and a fetch policy of **network-only**. The watch method returns an **Observable** that emits the result of the GraphQL query. The **map** operator extracts the watchlist from the result and calls **setWatchlist**.

The **toggleWatchlistItem** method toggles the watchlist status of a movie for a user by calling the **mutate** method on **toggleWatchlistService** with the **userId** and **movieId**  and a fetch policy of **network-only**. The **mutate** method returns an Observable that emits the result of the GraphQL mutation. The **map** operator extracts the **toggleWatchlist** from the result and calls **setWatchlist**.

The **clearWatchlist** method clears the watchlist for a user by calling the **mutate** method on **clearWatchlistService** with the **userId** and a fetch policy of **network-only**. The mutate method returns an Observable that emits the result of the GraphQL mutation. The **map** operator is used to check if the data property of the result is **true,** and if so, call **setWatchlist** with an empty array.

The private method **setWatchlist** updates the watchlist in the **subscriptionService** by calling the **next** method on two Observables: **watchlistItemcount$** and **watchlistItem$**. The **next** method pushes the new values (the length of the watchlist and the watchlist itself, respectively) to any subscribers of these Observables.

## Create the AddToWatchlist component

Run the following command to create the **AddToWatchlist** component.

```
ng g c components\add-to-watchlist
```

Add the following code to the **src\app\components\add-to-watchlist\add-to-watchlist.component.ts** file.

```js
import { Component, Input, OnChanges } from '@angular/core';
import { EMPTY, ReplaySubject, switchMap, takeUntil } from 'rxjs';
import { Movie } from 'src/app/models/movie';
import { SubscriptionService } from 'src/app/services/subscription.service';
import { WatchlistService } from 'src/app/services/watchlist.service';
import { ToastUtility } from '@syncfusion/ej2-notifications';

@Component({
  selector: 'app-add-to-watchlist',
  templateUrl: './add-to-watchlist.component.html',
  styleUrls: ['./add-to-watchlist.component.css'],
})

export class AddToWatchlistComponent implements OnChanges {
  @Input({ required: true })
  movieId = 0;

  toggle = false;
  buttonText = '';
  iconClass = 'e-zoom-in-2';
  private destroyed$ = new ReplaySubject<void>(1);

  constructor(
    private readonly watchlistService: WatchlistService,
    private readonly subscriptionService: SubscriptionService
  ) {}

  ngOnChanges() {
    this.subscriptionService.watchlistItem$
      .pipe(takeUntil(this.destroyed$))
      .subscribe((movieData: Movie[]) => {
        this.setFavourite(movieData);
        this.setButtonText();
      });
  }

  toggleValue() {
    this.toggle = !this.toggle;
    this.setButtonText();

    this.subscriptionService.userData$
      .pipe(
        switchMap((user) => {
          const userId = user.userId;
          if (userId > 0) {
            return this.watchlistService.toggleWatchlistItem(
              userId,
              this.movieId
            );
          } else {
            return EMPTY;
          }
        }),
        takeUntil(this.destroyed$)
      )
      .subscribe({
        next: () => {
          if (this.toggle) {
            ToastUtility.show({
              content: 'Movie added to your Watchlist.',
              position: { X: 'Right', Y: 'Top' },
              cssClass: 'e-toast-success',
            });
          } else {
            ToastUtility.show({
              content: 'Movie removed from your Watchlist.',
              position: { X: 'Right', Y: 'Top' },
            });
          }
        },
        error: (error) => {
          console.error('Error occurred while setting the Watchlist : ', error);
        },
      });
  }

  private setFavourite(movieData: Movie[]) {
    const favouriteMovie = movieData.find((f) => f.movieId === this.movieId);

    if (favouriteMovie) {
      this.toggle = true;
    } else {
      this.toggle = false;
    }
  }

  private setButtonText() {
    if (this.toggle) {
      this.buttonText = 'Remove from Watchlist';
      this.iconClass = 'e-zoom-out-2';
    } else {
      this.buttonText = 'Add to Watchlist';
      this.iconClass = 'e-zoom-in-2';
    }
  }

  ngOnDestroy(): void {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}
```

The **AddToWatchlistComponent** class implements the **OnChanges** method to handle changes to input properties. The constructor injects **WatchlistService** and **SubscriptionService** into the component. These are used to manage the user’s watchlist.

The **movieId** is an input property that represents the ID of the movie to add to the watchlist. It’s required and defaults to 0.

The **ngOnChanges** lifecycle hook is called when an input property changes. It subscribes to **subscriptionService.watchlistItem$**, and when it emits, it calls **setFavourite** and **setButtonText**.

The **toggleValue** method toggles the movie’s watchlist status. It subscribes to **subscriptionService.userData$**, gets the **userId**, and calls **watchlistService.toggleWatchlistItem** with the **userId** and **movieId**. If the user ID is not greater than 0, it returns **EMPTY**. A toast notification is shown when a movie is added or removed from the watchlist, and any errors are logged to the console.

The private method **setFavourite** sets the toggle value based on whether the movie is on the watchlist. The private method **setButtonText** sets the button text and icon class based on the toggle value.

Add the following code to the **src\app\components\add-to-watchlist\add-to-watchlist.component.html** file.

```xml
<button
  ejs-button
  class="full-width"
  [ngClass]="{ 'e-warning': toggle, 'e-success': !toggle }"
  iconCss="e-icons {{ iconClass }}"
  mat-raised-button
  (click)="toggleValue()"
>
  {{ buttonText }}
</button>
```

We have added a button that toggles a movie’s presence in a watchlist. The **ejs-button** attribute indicates that the button is a Syncfusion [Button](https://www.syncfusion.com/angular-components/angular-button "Angular Button"). The method **toggleValue** is invoked at the click of the button. This **ngClass** directive is used to apply the CSS classes dynamically based on the condition. If the **toggle** is true, the **e-warning** class is applied; if the **toggle** is false, the **e-success** class is applied.

## Create the Watchlist component

Run the following command to create the Watchlist component.

```
ng g c components\watchlist
```

Add the following code to the **src\app\components\watchlist\watchlist.component.ts** file.

```js
import { Component, OnDestroy } from '@angular/core';
import { EMPTY, ReplaySubject, switchMap, takeUntil } from 'rxjs';
import { SubscriptionService } from 'src/app/services/subscription.service';
import { WatchlistService } from 'src/app/services/watchlist.service';
import { ToastUtility } from '@syncfusion/ej2-notifications';

@Component({
  selector: 'app-watchlist',
  templateUrl: './watchlist.component.html',
  styleUrls: ['./watchlist.component.css'],
})
export class WatchlistComponent implements OnDestroy {
  private destroyed$ = new ReplaySubject<void>(1);
  watchlistItems$ = this.subscriptionService.watchlistItem$;

  constructor(
    private readonly watchlistService: WatchlistService,
    private readonly subscriptionService: SubscriptionService
  ) {}

  clearWatchlist() {
    this.subscriptionService.userData$
      .pipe(
        switchMap((user) => {
          const userId = user.userId;
          if (userId > 0) {
            return this.watchlistService.clearWatchlist(userId);
          } else {
            return EMPTY;
          }
        }),
        takeUntil(this.destroyed$)
      )
      .subscribe({
        next: () => {
          ToastUtility.show({
            content: 'Watchlist cleared!!!',
            position: { X: 'Right', Y: 'Top' },
          });
        },
        error: (error) => {
          console.error(
            'Error occurred while deleting the Watchlist : ',
            error
          );
        },
      });
  }

  ngOnDestroy(): void {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}
```

The constructor of the class **WatchlistComponent** injects two services into the component: **WatchlistService** and **SubscriptionService**. These services manage the user’s watchlist.

The property **watchlistItems$** is an Observable that emits the watchlist items. It’s assigned the value of **subscriptionService.watchlistItem$**.

The **clearWatchlist** method is used to clear the watchlist for the current user by subscribing to **subscriptionService.userData$**, getting the **userId**, and calling **watchlistService.clearWatchlist** with the **userId**. If the user ID is not greater than 0, it returns **EMPTY**, an Observable that emits no items and immediately completes. The **takeUntil** operator automatically unsubscribes from the Observable when **destroyed$** emits a value. A toast notification is shown when the watchlist is successfully cleared and any errors are logged in the console.

Add the following code to the **src\app\components\watchlist\watchlist.component.html** file.

```xml
<ng-container *ngIf="watchlistItems$ | async as watchlistItems">
    <div class="my-2">
      <div
        class="title-container p-2 d-flex align-items-center justify-content-between"
      >
        <h2 class="m-0">My Watchlist</h2>
        <div>
          <button ejs-button cssClass="e-danger" (click)="clearWatchlist()">
            Clear Watchlist
          </button>
        </div>
      </div>
      <ng-container *ngIf="watchlistItems.length > 0; else emptyWatchlist">
        <div class="e-card">
          <div class="e-card-content">
            <ejs-grid #grid [dataSource]="watchlistItems">
              <e-columns>
                <e-column headerText="Poster" width="150">
                  <ng-template #template let-movieData>
                    <ejs-tooltip id="tooltip" content="{{ movieData.title }}">
                      <img
                        class="my-2"
                        src="/Poster/{{ movieData.posterPath }}"
                      />
                    </ejs-tooltip>
                  </ng-template>
                </e-column>
                <e-column headerText="Title" width="150">
                  <ng-template #template let-movieData>
                    <a [routerLink]="['/movies/details/', movieData.movieId]">
                      {{ movieData.title }}
                    </a>
                  </ng-template>
                </e-column>
                <e-column field="genre" headerText="Genre" width="100"></e-column>
                <e-column
                  field="language"
                  headerText="Language"
                  width="100"
                ></e-column>
                <e-column headerText="Operation" width="150">
                  <ng-template #template let-movieData>
                    <ejs-tooltip content="Edit movie">
                      <app-add-to-watchlist
                        [movieId]="movieData.movieId"
                      ></app-add-to-watchlist>
                    </ejs-tooltip>
                  </ng-template>
                </e-column>
              </e-columns>
            </ejs-grid>
          </div>
        </div>
      </ng-container>
    </div>
  </ng-container>
  
  <ng-template #emptyWatchlist>
    <div class="e-card">
      <div class="e-card-content">
        <h2 class="m-2">Your watchlist is empty.</h2>
        <button
          ejs-button
          cssClass="e-link"
          iconCss="e-icons e-back e-medium"
          [routerLink]="['/']"
        >
          Back to Home
        </button>
      </div>
    </div>
  </ng-template>
```

The **ngIf** directive subscribes to the **watchlistItems$** Observable and assigns the emitted value to the local variable **watchlistItems**. The content inside the **ng-container** is only rendered if **watchlistItems$** emits a truthy value.

A Syncfusion Button with the **e-danger** CSS class is added. When clicked, it calls the **clearWatchlist** method from the component.

The **dataSource** input of the Syncfusion DataGrid component is bound to **watchlistItems**, which displays the items in the watchlist. The **e-column** elements define the grid’s columns. Each column has a **headerText** attribute that sets the column header, and some have a **field** attribute that binds the column to a property of the data items. Some columns also have an **ng-template** that customizes how the data is displayed.

The **app-add-to-watchlist** component’s **movieId input**  is bound to **movieData.movieId**, so the component knows which movie to add to or remove from the watchlist.

When the watchlist is empty, the **emptyWatchlist** template is rendered. It displays a message and a button that navigates to the home page.

## Create the similar-movies component

Run the next command to create the **SimilarMovies** component.

```
ng g c components\similar-movies
```

Add the following code in the **src\app\components\similar-movies\similar-movies.component.ts** file.

```js
import { Component } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { EMPTY, map, switchMap } from 'rxjs';
import { FetchSimilarMoviesService } from 'src/app/services/fetch-similar-movies.service';

@Component({
  selector: 'app-similar-movies',
  templateUrl: './similar-movies.component.html',
  styleUrls: ['./similar-movies.component.css'],
})
export class SimilarMoviesComponent {
  readonly similarMovies$ = this.activatedRoute.paramMap.pipe(
    switchMap((params: Params) => {
      const selectedMovieId = Number(params.get('movieId'));
      if (selectedMovieId > 0) {
        return this.fetchSimilarMoviesService
          .watch(
            {
              movieId: Number(selectedMovieId),
            },
            {
              fetchPolicy: 'network-only',
            }
          )
          .valueChanges.pipe(map((result) => result?.data?.similarMovies));
      } else {
        return EMPTY;
      }
    })
  );
  constructor(
    private readonly activatedRoute: ActivatedRoute,
    private readonly fetchSimilarMoviesService: FetchSimilarMoviesService
  ) {}
}
```

The **SimilarMoviesComponent** class contains an observable **similarMovies$**, which emits similar movies for a selected movie. It uses the **paramMap** Observable from **ActivatedRoute** to get the **movieId** parameter from the route. It then uses the **switchMap** method to call **FetchSimilarMoviesService.watch** with the **movieId** and fetch the policy **network-only**. The **watch** method returns an Observable that emits the result of the GraphQL query. The **map** operator is used to extract the **similarMovies** from the result. If **movieId** is not greater than 0, it returns **EMPTY**, an Observable that emits no items and immediately completes.

Add the following code to the **src\app\components\similar-movies\similar-movies.component.html** file.

```xml
<ng-container *ngIf="similarMovies$ | async as movies">
  <div class="row my-4 g-0">
    <div class="col-12 title-container p-2">
      <h2 class="m-0">Similar Movies</h2>
    </div>
    <div class="e-card">
      <div class="e-card-content row p-3">
        <div class="d-flex justify-content-start flex-wrap">
          <div *ngFor="let movie of movies" class="p-1">
            <app-movie-card [movie]="movie"></app-movie-card>
          </div>
        </div>
      </div>
    </div>
  </div>
</ng-container>
```

The **ngIf** directive subscribes to the **similarMovies$** Observable and assigns the emitted value to a local variable **movies**. The async pipe subscribes to an Observable and automatically updates the view whenever a new value is emitted. If **similarMovies$** does not emit a value (i.e., null or undefined), the **ng-container** and its contents will not be rendered.

The **ngFor** directive loops over the **movies** array. For each movie, it creates a new **div** and assigns the movie to the local variable **movie**.

We will then display the movie cards using the **<****app-movie-card>**  component, binding the movie input property of **app-movie-card** to the **movie** variable.

## Create the PageNotFound component

Run the following command to create the **PageNotFound** component.

```
ng g c components\page-not-found
```

Add the following code to the **src\app\components\page-not-found\page-not-found.component.html** file.

```xml
<div class="e-card">
  <div class="e-card-content">
    <h2 class="m-2">The resource you are looking for is not found.</h2>
    <button
      ejs-button
      cssClass="e-link"
      iconCss="e-icons e-back e-medium"
      [routerLink]="['/']"
    >
      Back to Home
    </button>
  </div>
</div>
```

This page will be rendered when the user tries to access a route that does not exist. We display a message and a button to redirect the user to the home page.

## Update the MovieDetails component

Update the **MovieDetailsComponent** class in the **src\app\components\movie-details\movie-details.component.ts** file.

```js
import { Component } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';

export class MovieDetailsComponent {
  // Existing code.
  
  userData$ = this.subscriptionService.userData$;

  constructor(
    // Existing service injection.
    private readonly subscriptionService: SubscriptionService
  ) {}
}
```

Add the selectors for the **SimilarMoviesComponent** and **AddToWatchlistComponent** inside the template of the **MovieDetailsComponent**.

Add the selector for **AddToWatchlistComponent** just after the movie cover image’s **\<div\>** element.

```xml
<ng-container *ngIf="movieDetails$ | async as movie; else noMovieFound">

<!-- Existing code -->

<div *ngIf="userData$ | async as user" class="mt-2 image-width">
  <app-add-to-watchlist
    *ngIf="user.isLoggedIn"
    [movieId]="movie.movieId"
  ></app-add-to-watchlist>
</div>

<!-- Existing code -->

</ng-container>
```

Insert the selector for the **SimilarMoviesComponent** just before closing the **\<ng-container\>** element.

```xml
<ng-container *ngIf="movieDetails$ | async as movie; else noMovieFound">

<!-- Existing code -->

<app-similar-movies></app-similar-movies>

</ng-container>
```

## Configure app routing

Open the **src\app\app-routing.module.ts** file and add the route for the newly added components under the **appRoutes** array.

```js
const appRoutes: Routes = [
  // Existing routes.
  {
    path: 'watchlist',
    component: WatchlistComponent,
    canActivate: [authGuard],
  },
  { path: '**', component: PageNotFoundComponent },

];
```

The route configuration maps the path **/watchlist** to the **WatchlistComponent**. The **canActivate** option means that the **authGuard** needs to return **true** for the route to be activated.

At the end of the array, we have added the wildcard route that maps any path not matched by the previous routes to the **PageNotFoundComponent**.

**Note:** The order in which we define the application’s routes is very important. The router uses a first-match wins strategy when matching routes. Therefore, more specific routes should be placed above less specific routes. The wildcard route should be placed at the end because it matches every URL. It should be selected only if no other routes match first.

## Update the nav-bar component

Update the **NavBarComponent** class under the **src\app\components\nav-bar\nav-bar.component.ts** file.

```js
export class NavBarComponent implements OnInit, OnDestroy {
  // Existing code
  readonly watchlistItemcount$ = this.subscriptionService.watchlistItemcount$;

  constructor(
    // Existing service injection
    private readonly watchlistService: WatchlistService
  ) {}

  ngOnInit(): void {
    this.subscriptionService.userData$
      .pipe(
        switchMap((user: User) => {
          this.authenticatedUser = user;
          if (this.authenticatedUser.userId > 0) {
            return this.watchlistService.getWatchlistItems(
              this.authenticatedUser.userId
            );
          } else {
            return EMPTY;
          }
        }),
        takeUntil(this.destroyed$)
      )
      .subscribe({
        error: (error) => {
          console.error('Error occurred while setting the Watchlist : ', error);
        },
      });
  }
  
  // Existing methods
}
```

In this update, we subscribe to the **userData$** Observable from the s **ubscription** **service**. The **RxJS** operator **switchMap** cancels the previous Observable and switches to a new one. In this case, it takes the emitted **User** object, assigns it to **this.authenticatedUser**, and then returns a new Observable based on the **userId**. If **userId** is greater than 0, it calls the **getWatchlistItems** method from the **WatchlistService** to get the watchlist items for the user. If **userId** is not greater than 0, it returns **EMPTY**, an Observable that emits no items and immediately completes. If an error occurs, it logs the error message to the console.

The next step is to update the **src\app\components\nav-bar\nav-bar.component.html** file by adding a button to navigate to the watchlist page. Add the following HTML template just before the code for the **Login** button.

```xml
<button
    ejs-button
    [isToggle]="true"
    cssClass="e-inherit e-round e-small"
    *ngIf="authenticatedUser.isLoggedIn"
    [routerLink]="['/watchlist']"
    iconCss="e-icons e-bookmark e-medium"
    class="watchlist-btn">
 <span
      class="e-badge e-badge-warning e-badge-notification e-badge-overlap e-badge-circle">{{ watchlistItemcount$ | async }}</span>
</button>
```

This button will render only when the user is authenticated. It will also display a badge to show the number of items on the watchlist. When the button is clicked, it navigates to the **/watchlist** route.

## Set authorization header for API requests

Since we have added authorization checks on the GraphQL server, we need to send the authorization bearer token with each API request. The Bearer token is set to authenticate the client with the GraphQL server. When a client sends a request to a server, the server needs a way to verify the client’s identity. This is especially important for operations requiring certain permissions, such as fetching sensitive or modifying data.

Update the **createApollo** function in the **src\app\graphql.module.ts** file.

```js
// Existing code.
export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> {
  const auth = setContext(() => {
    const headerToken = localStorage.getItem('authToken');

    if (headerToken === null) {
      return {};
    } else {
      return {
        headers: {
          Authorization: `Bearer ${headerToken}`,
        },
      };
    }
  });

  const link = ApolloLink.from([auth, httpLink.create({ uri })]);
  const cache = new InMemoryCache();

  return {
    link,
    cache,
    connectToDevTools: true,
  };
}
// Existing code.
```

This function takes an **HttpLink** object as a parameter and returns an **ApolloClientOptions** object. **HttpLink** is used to connect to the GraphQL server. The **setContext** function creates middleware that can modify requests before they are sent. The bearer token is retrieved from local storage and added to the Authorization header of each request. An empty object is returned if the authentication token is null, meaning no headers are added to the request. If the token is not null, an object with an Authorization header is returned. An Apollo Link is created that first applies the **auth** link (which adds the Authorization header) and then the **httpLink** (which sends the request to the server).

The term bearer in the bearer token specifies the type of authentication. A bearer token means the bearer of this token is authorized, i.e., whoever presents this token has the right to access the data. This is a common pattern in web development, especially when working with APIs that require authentication.

## Execution demo

After executing the previous code examples, we will get output like in the following image.

<figure>

  <img src="https://www.syncfusion.com/blogs/wp-content/uploads/2024/07/Build-Dynamic-Watchlist.gif" alt="Build a Dynamic Watchlist for Your Web App with Angular & GraphQL" style="width:100%">

</figure>

## GitHub resource

For more details, refer to the [complete source code](https://github.com/SyncfusionExamples/full-stack-web-app-using-angular-and-graphql "Full stack web app using Angular and Graphql GitHub demo") on GitHub.

## Summary

Thank you for reading this article. In it, we learned how to implement the watchlist feature in our application, which allows the user to add or remove movies from the watchlist. We also added a feature to display similar movies when users visit the movie details page.

In the next and final article of this series, we will learn how to deploy this application to the IIS and Azure App Service.

If you are new to our platform, we extend an invitation to explore our Syncfusion [Angular components](https://www.syncfusion.com/angular-components "Angular components") with the convenience of a [free trial](https://www.syncfusion.com/downloads/angular "Get free evaluation of Essential Studio for Angular"). This trial allows you to experience the full potential of our components to enhance your app’s user interface and functionality.

Our dedicated support system is readily available if you need guidance or have any questions. Contact us through our [support forum](https://www.syncfusion.com/forums "Syncfusion Support Forum"), [support portal](https://support.syncfusion.com/ "Syncfusion Support Portal"), or [feedback portal](https://www.syncfusion.com/feedback/angular "Syncfusion Feedback Portal"). Your success is our priority, and we’re always delighted to assist you on your development journey!

## Related blogs

- [A Full-Stack Web App Using Angular and GraphQL: Part 1](https://www.syncfusion.com/blogs/post/full-stack-web-app-angular-graphql "Blog: A Full-Stack Web App Using Angular and GraphQL: Part 1")
- [A Full-Stack Web App Using Angular and GraphQL: Data Fetching and Manipulation (Part 2)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-2 "Blog: A Full-Stack Web App Using Angular and GraphQL: Data Fetching and Manipulation (Part 2)")
- [A Full-Stack Web App Using Angular and GraphQL: Perform Edit, Delete, and Advanced Filtering (Part 3)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-3 "Blog: A Full-Stack Web App Using Angular and GraphQL: Perform Edit, Delete, and Advanced Filtering (Part 3)")
- [A Full-Stack Web App Using Angular and GraphQL: Adding User Registration Functionality (Part 4)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-4 "Blog: A Full-Stack Web App Using Angular and GraphQL: Adding User Registration Functionality (Part 4)")
- [A Full-Stack Web App Using Angular and GraphQL: Adding Login and Authorization Functionalities (Part 5)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-5 "Blog: A Full-Stack Web App Using Angular and GraphQL: Adding Login and Authorization Functionalities (Part 5)")