Copied RSS Feed

Angular

A Full-Stack Web App Using Angular and GraphQL: Data Fetching and Manipulation (Part 2)

Welcome to the second part of our series on building a full-stack web application using Angular and GraphQL. In part 1, we learned how to configure a full-stack .NET app to create a GraphQL server using Hot Chocolate. We created a GraphQL query and used the Banana Cake Pop IDE to execute it.

In this installment, we will configure the Angular client using Apollo GraphQL to consume the GraphQL server endpoints. We will also create a GraphQL mutation to add new movie data. This article is designed to guide you through integrating GraphQL with Angular, demonstrating how data can be fetched and manipulated using GraphQL queries and mutations.

So, let’s continue our journey into the fascinating world of full-stack development with Angular and GraphQL!

Configure the app to handle the uploaded poster images

We’ll design the app to allow the users to upload the movies’ poster images while adding movie data. We will store the images on the server inside a folder named Poster. To do this, add the following code to the Program.cs file.

var FileProviderPath = app.Environment.WebRootPath + "/Poster";
if (!Directory.Exists(FileProviderPath))
{
    Directory.CreateDirectory(FileProviderPath);
}
app.UseFileServer(new FileServerOptions
{
    FileProvider = new PhysicalFileProvider(FileProviderPath),
    RequestPath = "/Poster",
    EnableDirectoryBrowsing = true
});

In this process, we’re setting up a file server to deliver static files from a designated directory. The variable FileProviderPath is assigned the directory path containing the static files. If the directory doesn’t exist, it will be created using the Directory.CreateDirectory method.

The app.UseFileServer method is invoked to set up the file server, with a FileServerOptions object passed as a parameter to define the server’s settings. The FileProvider property is assigned a PhysicalFileProvider object that targets the FileProviderPath directory. The RequestPath property is set to “/Poster”, denoting the URL path from which the server will deliver files. The EnableDirectoryBrowsing property is enabled, allowing users to view the directory’s contents.

A default image is displayed if a user doesn’t upload a movie poster. The name of this default image is added to the appsettings.json file, as shown.

"DefaultPoster": "DefaultPoster.jpg",

Note: The default image hasn’t been added to the app yet. We’ve only included its name in the appsettings.json file for consistency. The default poster image will be incorporated once the Poster folder is created during the first execution of the app. We’ll cover this process later in this article.

Update the IMovie interface

Update the IMovie.cs file by adding the method declaration, as shown.

public interface IMovie
{
    // other methods.
    
    Task AddMovie(Movie movie);
}

Implement the Add Movie functionality in the data access layer

Since we have added a new method in the IMovie interface, we need to update the MovieDataAccessLayer class by implementing the method. Refer to the following code example.

public async Task AddMovie(Movie movie)
{
    try
    {
        await _dbContext.Movies.AddAsync(movie);
        await _dbContext.SaveChangesAsync();
    }
    catch
    {
        throw;
    }
}

In this method, the AddAsync method is called on the _dbContext.Movies object, passing in the movie object as a parameter. This adds the movie object to the Movies table in the database. The SaveChangesAsync method is then called on the _dbContext object to save the changes to the database.

If an exception occurs during the process, the catch block catches the exception and rethrows it. This helps with error handling, ensuring that any exceptions that occur are not left unhandled, which could lead to crashes.

Add a GraphQL mutation resolver

A GraphQL mutation is used to mutate or change the data on the server.  Add a class called MovieMutationResolver.cs inside the MovieApp\GraphQL folder and then add the following code inside it.

using MovieApp.Interfaces;
using MovieApp.Models;
namespace MovieAppDemo.GraphQL
{
  public class MovieMutationResolver
  {
    public record AddMoviePayload(Movie movie);
    readonly IWebHostEnvironment _hostingEnvironment;
    readonly IMovie _movieService;
    readonly IConfiguration _config;
    readonly string posterFolderPath = string.Empty;
    public MovieMutationResolver(IConfiguration config, IMovie movieService, IWebHostEnvironment hostingEnvironment)
    {
        _config = config;
        _movieService = movieService;
        _hostingEnvironment = hostingEnvironment;
        posterFolderPath = System.IO.Path.Combine(_hostingEnvironment.WebRootPath, "Poster");
    }
    [GraphQLDescription("Add a new movie data.")]
    public AddMoviePayload AddMovie(Movie movie)
    {
        if (!string.IsNullOrEmpty(movie.PosterPath))
        {
            movie.PosterPath = WriteImageToServer(movie);
        }
        else
        {
            movie.PosterPath = _config["DefaultPoster"];
        }
        _movieService.AddMovie(movie);
        return new AddMoviePayload(movie);
    }
    string WriteImageToServer(Movie movie)
    {
        string fileName = Guid.NewGuid() + ".jpg";
        string fullPath = System.IO.Path.Combine(posterFolderPath, fileName);
        byte[] imageBytes = Convert.FromBase64String(movie.PosterPath);
        File.WriteAllBytes(fullPath, imageBytes);
        return fileName;
    }
  }
}

In the MovieMutationResolver class, we’ve initialized the _config, _movieService, and _hostingEnvironment objects in the constructor. We’ve also set up the posterFolderPath variable, which points to the Poster directory in the web root path.

Next, we have the AddMovie method. This method accepts a Movie object and returns an AddMoviePayload object. It begins by checking whether the PosterPath property of the Movie object is null or empty. If not, it uses the WriteImageToServer method to save the image on the server and updates the PosterPath property with the file name.

The AddMovie method sets the PosterPath property to the default poster path from the configuration file if empty. Then, it uses the AddMovie method of the _movieService object to add the Movie object to the database. Finally, it returns a new AddMoviePayload object containing the added Movie object.

The WriteImageToServer method accepts a Movie object and returns the file name of the saved image. It creates a unique file name using the Guid.NewGuid() method and saves the image on the server with the File.WriteAllBytes method.

Register mutation resolver

Let’s enhance the registration of the mutation resolver. Update the Program.cs file as follows.

builder.Services.AddGraphQLServer()
    .AddQueryType<MovieQueryResolver>() 
    .AddMutationType<MovieMutationResolver>();

This code adds the MovieQueryResolver and MovieMutationResolver to the GraphQL server, enabling it to handle queries and mutations for movie data.

Configure CORS

Let’s enhance the configuration of CORS (cross-origin resource sharing) in your application. Add the following code to the Program.cs file.

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyHeader()
               .AllowAnyMethod()
               .SetIsOriginAllowed((host) => true);
    });
});

CORS is a security mechanism that prevents web pages or scripts from requesting a domain other than the one that served the original web page. In this instance, the AddCors method adds a CORS policy to the app’s services. The AddDefaultPolicy method is then invoked to add a default policy to the options object.

To activate CORS, include the UseCors method like in the following code example.

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();

This code allows any header and method from any origin, providing flexibility while developing, but remember to restrict it to known origins when deploying for security reasons.

Note: The UseCors must be called after the UseRouting and UseStaticFiles methods. Refer to the Middleware order documentation to learn more about middleware order in ASP.NET Core.

Now, we’re done with the server configuration. Let’s move to the client side of the app.

Install Apollo Angular

The ClientApp folder of our project contains the code for the UI app. Open a command window/terminal in this folder and execute the following command.

ng add apollo-angular

You’ll be prompted to input the URL for the GraphQL API. Type in the base URL of your app, appending /graphql, at the end. For instance, in this case, the URL for the GraphQL API would be https://localhost:44469/graphql/.

Following this, you’ll be asked to specify the version of GraphQL. You can input a custom version or press Enter to choose the default GraphQL version that Apollo Angular supports.

Once these steps are completed, the command will generate a file named graphql.module.ts. This file will contain all the configurations specific to GraphQL.

Add the GraphQL query and mutation

Follow these steps to add the GraphQL query and mutation:

Step 1: Create a folder named GraphQL under the src\app directory.

Step 2: Create a file named query.ts within the GraphQL folder. This file will contain the GraphQL query to fetch the genre list. Then, add the following code to the query.ts file.

import { gql } from 'apollo-angular';
export const GET_GENRE = gql`
  query FetchGenreList {
    genreList {
      genreId
      genreName
    }
  }
`;

This query FetchGenreList retrieves the genreId and genreName from the genreList.

Step 3: Similarly, create another file named mutation.ts in the GraphQL folder. This file will contain the mutation that allows us to add a new movie record. Then, add the following code to the mutation.ts file.

import { gql } from 'apollo-angular';
export const ADD_MOVIE = gql`
  mutation AddEmployeeData($movieData: MovieInput!) {
    addMovie(movie: $movieData) {
      movie {
        title
      }
    }
  }
`;

This mutation, AddEmployeeData, takes a MovieInput object as a parameter and calls the addMovie mutation on the server. It then retrieves the title of the newly added movie.

Create client-side models

Create a folder called models under the src\app folder and add a file called genre.ts. Then, add the following code inside it.

export interface Genre {
  genreId: number;
  genreName: string;
}
export type GenreType = {
  genreList: Genre[];
};

In the previous code example, we added an interface for Genre. We also created a type called GenreType, which will be used as the type for the data returned from the GraphQL server.

Next, create a file called movie.ts and add the following code inside it.

export interface Movie {
  movieId: number;
  title: string;
  overview: string;
  genre: string;
  language: string;
  duration: number;
  rating: number;
  posterPath: string;
}

Create the required services

Let’s create the necessary services to handle API calls to the GraphQL server:

Step 1: Run the following command in the ClientApp folder to generate a service file.

ng g s services\fetch-genre

Then, add the following code to the fetch-genre.service.ts file.

import { Injectable } from '@angular/core';
import { Query } from 'apollo-angular';
import { GenreType } from '../models/genre';
import { GET_GENRE } from '../GraphQL/query';
@Injectable({
  providedIn: 'root',
})
export class FetchGenreService extends Query {
  document = GET_GENRE;
}

The FetchGenreService class extends the Query class and requires a document property to execute the GraphQL query. By setting the document property to the GET_GENRE query, this class can retrieve a list of movie genres from the server.

Step 2: Run the command ng g s services\movie-helper to create a new service file. Then, add the following code to the movie-helper.service.ts file.

import { Injectable } from '@angular/core';
import { FetchGenreService } from './fetch-genre.service';
import { map, shareReplay } from 'rxjs';
@Injectable({
  providedIn: 'root',
})
export class MovieHelperService {
  genreList$ = this.fetchGenreService.watch().valueChanges.pipe(
    map((result) => result?.data),
    shareReplay(1)
  );
  constructor(private readonly fetchGenreService: FetchGenreService) {}
}

The MovieHelperService retrieves a list of movie genres from the server using the FetchGenreService class. The watch() method returns an observable that emits the result of the GraphQL query. The valueChanges property extracts the data from the result. The shareReplay() operator caches the query result so that subsequent subscribers to the genreList$ observable receive the cached result instead of making a new request to the server. This is beneficial, as the list of genres doesn’t change frequently, so we cache the result on the client instead of making a server call each time.

Step 3: Create a new service called add-movie.service.ts and add the following code to it.

import { Injectable } from '@angular/core';
import { Mutation } from 'apollo-angular';
import { ADD_MOVIE } from '../GraphQL/mutation';
@Injectable({
  providedIn: 'root',
})
export class AddMovieService extends Mutation {
  document = ADD_MOVIE;
}

The AddMovieService class extends the Mutation class and requires a document property to execute the GraphQL mutation. By setting the document property to the ADD_MOVIE mutation, this class can add a new movie to the server.

Create the admin module

Our app incorporates role-based access, and only the Administrator has the permission to add, edit, and delete movie data. We’ll create a separate lazy-loaded module for admin functionalities to ensure the separation of concerns and improve the app’s startup performance.

Step 1: Run the following command to generate the Admin module.

ng g m admin

Step 2: Execute the following command to create the Admin routing module.

ng g m admin\admin-routing

Step 3: We’ll use Syncfusion Angular controls in our app, so we need a module to handle the necessary imports. Run the following command to create this module.

ng g m admin\admin-ej2-components

This module will manage the imports for the Syncfusion Angular controls used in the lazy-loaded Admin module.

Step 4: Next, run the following command to generate the movie form component.

ng g c admin\components\movie-form

We’ll add the business logic to this component later in this article.

Step 5: Update the AdminRoutingModule by referring to the following code example.

import { NgModule } from '@angular/core';
import { MovieFormComponent } from '../components/movie-form/movie-form.component';
import { RouterModule, Routes } from '@angular/router';
const adminRoutes: Routes = [
  {
    path: '',
    children: [
      { path: 'new', component: MovieFormComponent },
      { path: ':movieId', component: MovieFormComponent },
    ],
  },
];
@NgModule({
  imports: [RouterModule.forChild(adminRoutes)],
  exports: [RouterModule],
})
export class AdminRoutingModule {}

The AdminRoutingModule defines the routes for the admin section of the app. The adminRoutes constant is an array of Routes objects that define the child routes for the admin section.

Step 6: Update the AdminModule by referring to the following code example.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MovieFormComponent } from './components/movie-form/movie-form.component';
import { AdminRoutingModule } from './admin-routing/admin-routing.module';
import { ReactiveFormsModule } from '@angular/forms';
import { AdminEj2ComponentsModule } from './admin-ej2-components/admin-ej2-components.module';
@NgModule({
  declarations: [MovieFormComponent],
  imports: [
    CommonModule,
    AdminRoutingModule,
    ReactiveFormsModule,
    AdminEj2ComponentsModule,
  ],
})
export class AdminModule {}

Thus, we’ve added the imports for AdminRoutingModule, ReactiveFormsModule, and AdminEj2ComponentsModule.

Step 7: Once you have installed the packages, add the required imports in the admin-ej2-components.module.ts file, as shown in the following code.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UploaderModule } from '@syncfusion/ej2-angular-inputs';
import { ButtonModule } from '@syncfusion/ej2-angular-buttons';
import { TextBoxModule } from '@syncfusion/ej2-angular-inputs';
import { DropDownButtonModule } from '@syncfusion/ej2-angular-splitbuttons';
import { ToastModule } from '@syncfusion/ej2-angular-notifications';
import {
  GridModule,
  SortService,
  PageService,
  SearchService,
  ToolbarService,
} from '@syncfusion/ej2-angular-grids';
import { TooltipModule } from '@syncfusion/ej2-angular-popups';
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns';
const importedModules = [
  ButtonModule,
  TextBoxModule,
  UploaderModule,
  DropDownButtonModule,
  ToastModule,
  GridModule,
  TooltipModule,
  DropDownListModule,
];
@NgModule({
  imports: [CommonModule, importedModules],
  exports: importedModules,
  providers: [PageService, SortService, SearchService, ToolbarService],
})
export class AdminEj2ComponentsModule {}

Note: Ensure that the necessary CSS reference for Syncfusion Angular components is included in the src\styles.css file. For additional details on how to install and configure Syncfusion Angular controls, kindly refer to the Angular getting started documentation.

Configure lazy loading for the Admin module

Now, let’s configure lazy loading for the Admin module by following these steps:

Step 1: First, we need to create an app-routing module by running the following command.

ng g m app-routing --flat

After executing this, the –flat option will create the module file at the top level of the current project root.

Note: Refer to the Angular CLI documentation for more details.

Step 2: We will use the app-routing module to configure routes for our app. Add the following code to the app-routing.module.ts file.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
const appRoutes: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full' },
  {
    path: 'admin/movies',
    loadChildren: () =>
      import('./admin/admin.module').then((module) => module.AdminModule),
  },
];
@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {
      scrollPositionRestoration: 'top',
    }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

In the code, a constant appRoutes is defined as an array of route objects. Each object in this array represents a route in the app. The first route is the root route (‘ ‘), which maps to the HomeComponent. The pathMatch:’full’ property represents the entire URL path and needs to be matched; in this case, it means that HomeComponent will be displayed when the app’s URL is its base URL.

The second route is for admin/movies. This route uses lazy loading, which means the AdminModule (and all of its associated code) won’t be loaded until the user navigates to admin/movies. This can help improve performance by only loading code as it’s needed.

The RouterModule is imported with the forRoot method, which is supplied with the appRoutes array and a configuration object. The scrollPositionRestoration: ‘top’ option means that the app will automatically scroll to the top of the page when navigating to a new route.

Step 3: Now, import the AppRoutingModule into the AppModule. This allows the routes to be used in the app. Update the app.module.ts file as follows.

@NgModule({
  // Existing code
  imports: [
    // Existing imports
    AdminRoutingModule,
  ],
})

Step 4: Then, create a module for Syncfusion Angular controls by running the command ng g m ej2-components. This module will handle the imports for the Syncfusion Angular controls that will be used in the eager-loaded app module. Add the following code to the ej2-components.module.ts file.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonModule } from '@syncfusion/ej2-angular-buttons';
import { TextBoxModule } from '@syncfusion/ej2-angular-inputs';
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns';
import { DropDownButtonModule } from '@syncfusion/ej2-angular-splitbuttons';
import { ToastModule } from '@syncfusion/ej2-angular-notifications';
import { TooltipModule } from '@syncfusion/ej2-angular-popups';
import { AutoCompleteModule } from '@syncfusion/ej2-angular-dropdowns';
import { MessageModule } from '@syncfusion/ej2-angular-notifications';
import { GridModule } from '@syncfusion/ej2-angular-grids';
import { AppBarModule } from '@syncfusion/ej2-angular-navigations';
const importedModules = [
  AppBarModule,
  ButtonModule,
  TextBoxModule,
  DropDownListModule,
  DropDownButtonModule,
  ToastModule,
  TooltipModule,
  AutoCompleteModule,
  MessageModule,
  GridModule,
];
@NgModule({
  imports: [CommonModule, importedModules],
  exports: importedModules,
})
export class Ej2ComponentsModule {}

Step 5: Make sure to add the required CSS reference for Syncfusion Angular components in the src\styles.css file.

Configure the movie form component

In this section, we’ll configure the movie-form component, which will be used for both adding and editing movie data. We’ll utilize Angular’s reactive forms approach to create a form that allows users to add or edit movie data.

Step 1: First, create a new folder named models under the src\app\admin folder.

Step 2: In the models folder, add a new file named movie-form.ts. This file will contain the interface for the movie form. Then, add the following code to it.

import { FormControl } from '@angular/forms';
export interface MovieForm {
  movieId: FormControl<number>;
  title: FormControl<string>;
  overview: FormControl<string>;
  genre: FormControl<string>;
  language: FormControl<string>;
  duration: FormControl<number>;
  rating: FormControl<number>;
}

This code defines an interface MovieForm with various form controls like movieId, title, overview, genre, language, duration, and rating.

Step 3: Next, navigate to the src\app\admin\components\movie-form\movie-form.component.ts file and add the following necessary code to the MovieFormComponent class.

protected movieForm!: FormGroup;
protected formTitle = 'Add';
private destroyed$ = new ReplaySubject(1);
posterPreview!: ArrayBuffer | string | null;
posterFile = '';
moviePosterPath = '';
public fields: Object = { text: 'genreName', value: 'genreName' };
protected submitted = false;
genreList$ = this.movieHelperService.genreList$;
constructor(
    private readonly movieHelperService: MovieHelperService,
    private readonly formBuilder: NonNullableFormBuilder,
    private readonly addMovieService: AddMovieService,
    private readonly router: Router
  ) {
    this.initializeForm();
  }
uploadPoster(args: any) {
  const reader = new FileReader();
  reader.readAsDataURL(args.filesData[0].rawFile);
  reader.onloadend = (myevent) => {
    if (myevent.target?.result != null) {
        this.posterPreview = myevent.target.result;
        this.posterFile = (this.posterPreview as string).split(',')[1];
     }
  };
}
private initializeForm(): void {
   this.movieForm = this.formBuilder.group({
      movieId: 0,
      title: this.formBuilder.control('', Validators.required),
      genre: this.formBuilder.control('', Validators.required),
      language: this.formBuilder.control('', Validators.required),
      overview: this.formBuilder.control('', [
        Validators.required,
        Validators.maxLength(1000),
      ]),
      duration: this.formBuilder.control(0, [
        Validators.required,
        Validators.min(1),
      ]),
      rating: this.formBuilder.control(0, [
        Validators.required,
        Validators.min(0.0),
        Validators.max(10.0),
      ]),
   });
}

The uploadPoster() function is designed to manage the event triggered when a user uploads a movie poster. It constructs a new FileReader object and reads the data from the uploaded file. The onloadend event is subsequently utilized to assign the posterPreview property to the outcome of the file read operation, which is a base64-encoded string of the file data. The posterFile property is then assigned to the base64-encoded string of the file data, facilitating the transmission of the file data to the server.

The initializeForm() function is employed to set up the movie form. The movieForm property is assigned to a new FormGroup object that encompasses the form controls for the movie title, genre, language, overview, duration, and rating. The Validators class is used to establish the validation rules for each form control.

Please incorporate the following functions into the MovieFormComponent class.

protected get movieFormControl() {
    return this.movieForm.controls;
}
protected onFormSubmit(): void {
    this.submitted = true;
    if (!this.movieForm.valid) {
      return;
    }
    this.addMovie();
}
private addMovie() {
    const movieData: Movie = {
      movieId: this.movieForm.controls.movieId.value,
      title: this.movieForm.controls.title.value,
      duration: Number(this.movieForm.controls.duration.value),
      rating: Number(this.movieForm.controls.rating.value),
      genre: this.movieForm.controls.genre.value,
      language: this.movieForm.controls.language.value,
      overview: this.movieForm.controls.overview.value,
      posterPath: this.posterFile,
    };
    this.addMovieService
      .mutate({ movieData: movieData })
      .pipe(takeUntil(this.destroyed$))
      .subscribe({
        next: () => {
          ToastUtility.show({
            content: 'The movie data is added successfully.',
            position: { X: 'Right', Y: 'Top' },
            cssClass: 'e-toast-success',
          });
          this.router.navigate(['/']);
        },
        error: (error) => {
          ToastUtility.show({
            content: 'Error occurred while adding movie data.',
            position: { X: 'Right', Y: 'Top' },
            cssClass: 'e-toast-danger',
          });
          console.error('Error ocurred while adding movie data : ', error);
        },
      });
}
ngOnDestroy(): void {
    this.destroyed$.next();
    this.destroyed$.complete();
}

A movieData object is created with properties that correspond to the controls in a form. The value of each control is retrieved and assigned to the corresponding property in movieData.

Note: The duration and rating values are converted to numbers using the Number() function.

The mutate() method is used to execute a GraphQL mutation that adds the new movie to the server. The pipe(takeUntil(this.destroyed$)) part is a common pattern in RxJS. It ensures that the subscription to the Observable returned by mutate() is automatically unsubscribed from when the destroyed$ Observable emits a value. This helps prevent memory leaks.

The subscribe() method handles the response from the server. If the server responds successfully, the app navigates to the root route. If an error occurs while adding the movie data, a toast notification is displayed with an appropriate message, and the error is also logged into the console.

Open the movie-form.component.html file and add the following code to it.

<div class="row justify-content-center">
  <div
    class="title-container p-2 d-flex align-items-center justify-content-between"
  >
    <h2 class="m-0">{{ formTitle }} Movie</h2>
  </div>
  <div class="e-card">
    <div class="row justify-content-start">
      <div class="col-md-8">
        <div class="e-card-content row g-0">
          <form [formGroup]="movieForm" (ngSubmit)="onFormSubmit()">
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Title"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="title"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.title.touched || submitted)&&
                  movieFormControl.title.errors?.['required']"
              class="e-error"
            >
              Title is required.
            </div>
            <div class="e-input-section">
              <ejs-dropdownlist
                placeholder="Genre"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="genre"
                [dataSource]="(genreList$ | async)?.genreList"
                [fields]="fields"
              >
              </ejs-dropdownlist>
            </div>
            <div
              *ngIf="(movieFormControl.genre.touched || submitted) &&
                    movieFormControl.genre.errors?.['required']"
              class="e-error"
            >
              Genre is required.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Language"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="language"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.language.touched || submitted)&&
                    movieFormControl.language.errors?.['required']"
              class="e-error"
            >
              Language is required.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Duration (mins)"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="duration"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.duration.touched || submitted) &&
                      movieFormControl.duration.errors?.['required']"
              class="e-error"
            >
              Duration is required.
            </div>
            <div
              *ngIf="movieFormControl.duration.touched &&
                    movieFormControl.duration.errors?.['min']"
              class="e-error"
            >
              Duration cannot be less than 0.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Rating"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="rating"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.rating.touched || submitted) &&
                        movieFormControl.rating.errors?.['required']"
              class="e-error"
            >
              Rating is required.
            </div>
            <div
              *ngIf="
                (movieFormControl.rating.touched || submitted) &&
                (movieFormControl.rating.hasError('min') ||
                  movieFormControl.rating.hasError('max'))
              "
              class="e-error"
            >
              The value should be between 0 and 10.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Overview"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="overview"
                [multiline]="true"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.overview.touched || submitted) &&
                        movieFormControl.overview.errors?.['required']"
              class="e-error"
            >
              Overview is required.
            </div>
            <div
              *ngIf="movieFormControl.overview.touched &&
                        movieFormControl.overview.errors?.['maxlength']"
              class="e-error"
            >
              Overview cannot be more than 1000 characters long.
            </div>
            <div class="e-card-actions d-flex justify-content-end">
              <button type="submit" ejs-button cssClass="e-info">Save</button>
              <button ejs-button cssClass="e-danger" class="ms-2">
                Cancel
              </button>
            </div>
          </form>
        </div>
      </div>
      <div class="col-md-4 d-flex flex-column align-items-center">
        <div class="e-card-content row g-0 justify-content-center">
          <img
            class="preview-image p-2"
            src="{{ posterPreview }}"
            alt="Upload an image"
          />
          <div class="my-2">
            <ejs-uploader
              #defaultupload
              id="fileupload"
              (selected)="uploadPoster($event)"
            ></ejs-uploader>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

We have created a form that uses Syncfusion’s Angular UI components for the form fields and buttons. The form’s title is set based on the formTitle property of the component. This allows the form to be used for both adding and editing movies.

The movieForm is a FormGroup instance that tracks the value and validity state of the form controls. The formControlName attribute is used to link each form control with a FormControl instance in the movieForm FormGroup. Each form field has corresponding validation messages.

The ejs-uploader component is used to upload a poster image for the movie. When a file is selected, the uploadPoster() method is called. The {{ posterPreview }} expression is used to display a preview of the uploaded poster image.

The Save button submits the form, triggering the onFormSubmit() method. The event for the Cancel button will be added in the next part of this series of articles.

Note: To learn more about the Reactive form, refer to this Mastering Strictly Typed Reactive Forms in Angular blog.

Configure the navigation bar

Run the following command to create the nav-bar component.

ng g c components\nav-bar --m app

The –m flag allows us to specify the module where we want to declare the component. To learn more, refer to the Angular component documentation.

Then, open the nav-bar.component.html file and add the following code to it.

<ejs-appbar colorMode="Primary">
  <button
    ejs-button
    iconCss="e-icons e-video e-medium"
    cssClass="e-inherit"
    [routerLink]="['/']"
  >
    Movie App
  </button>
  <div class="e-appbar-spacer"></div>
  <button
    ejs-button
    cssClass="e-inherit"
    [routerLink]="['/admin/movies/new']"
    [isToggle]="true"
  >
    Add movie
  </button>
</ejs-appbar>

How do we get the data for the demo?

You can get dummy data to test the app from any source you like. However, for consistency, we will use The Movie Database as the data source. You can get all the required details for the app, such as movie title, rating, genre, and posters, from this website.

Execution demo

Finally, launch the app. You will see an Add Movie option on the nav bar. Click on it and add the required movies, as shown in the following GIF image.

After a movie has been added successfully, you can verify the data inside the Movie table in the database.

Once the app is executed for the first time, the Poster folder will be created in the \wwwroot folder of the project. Add a default image of your choice in this folder with the name DefaultPoster.jpg. This image will act as the default poster image if the user does not upload an image. Remember, we already configured the logic to use the default poster earlier in this article.

GitHub reference

Check out the complete source code of this full-stack web app with Angular and GraphQL on the GitHub repository.

Harness the power of Syncfusion’s feature-rich and powerful Angular UI components.

Summary

We appreciate your time in reading this article. We embarked on a journey to understand the creation of mutations in a GraphQL server, utilizing the Apollo GraphQL client to interact with server endpoints. This allowed us to implement a feature to add new movie data to our application.

In the next article of this series, we will enhance our app by introducing edit and delete functionalities for movie data. Additionally, we will configure the home page to showcase a list of movies, complete with sorting and filtering options, for a user-friendly experience.

If you’re not a Syncfusion user, we encourage you to explore our Angular components through our free trial.

If you require any assistance or have any questions, feel free to reach out to us via our support forumsupport portal, or feedback portal. We are always happy to help you!

Related blogs

Meet the Author

Ankit Sharma

Ankit Sharma is an author, a speaker, and a passionate programmer. He is a Google Developer Expert (GDE) for Angular and Microsoft’s Most Valuable Professional (MVP). Currently, he works in Cisco Systems as a software development engineer.