DEV Community

Raj Vijay
Raj Vijay

Posted on

An intro to GraphQL with Apollo Client and Angular

Recently I watched an egghead.io tutorial from Eve Porcello, demonstrating how to communicate with a GraphQL API using the GraphQL query language. The instructor uses a tool called GraphQL Playground(https://pet-library.moonhighway.com), which lets the user, send queries to GraphQL endpoints and allows the user to perform various tasks on a pet library like authentication, return a list of pets, check-in and check-out pets from the pet library e.t.c.

In this blog post,we will build a simple clone of pet library playground from scratch using Angular and Apollo, with features including authentication, returning a list of all pets and check-in/check-out pets.

You can view the full source code here.

Let's start by adding Apollo Angular by using this command.

ng add apollo-angular

The above command will add an NgModule(graphql.module.ts) with an initial setup to the project. To this we need to add the URL of our GraphQL server, which in our case is http://pet-library.moonhighway.com

Let's start with returning a list of all pets from the library. List screen should have a filter to see All, Available and Checked Out status. Our final UI should look like this.

Pets UI

Let's add a new file pet.ts under models folder. You should be able to view the data types under the Schema tab in http://pet-library.moonhighway.com.

enum PetCategory {
    CAT,
    DOG,
    RABBIT,
    STINGRAY
}

export enum PetStatus {
    AVAILABLE,
    CHECKEDOUT
}

export interface Pet {
    id: string;
    name: string;
    weight: number;
    category: PetCategory;
    status: PetStatus;
    dueDate?: Date;
}

Query

We will create a file called pets-filter-query.ts under the queries folder and wrap the query with a gql function, like shown below. I am using a fragment called petFields in case if i need to reuse the fields in some other query. You can learn more about fragments here.

import gql from 'graphql-tag';
const petFieldsQuery = gql`
  fragment petFields on Pet {
    id
    name
    category
    status
    inCareOf{
      name
    }
  }`;

export const filterPetsQuery = gql`
query petsQuery($status : PetStatus)
{
  allPets(status : $status)  {
      ...petFields
  }
} ${petFieldsQuery}`;

UI Layer

Let's then add a list component and update the html with the following.

<div class="row pt-4">
    <div class="col-sm-8">
        <span style="color: #007bff;font-weight :bold;">Pet Status: </span>
        <div ngbDropdown class="d-inline-block">
            <button class="btn btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>{{getStatusName()}}</button>
            <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
                <button ngbDropdownItem *ngFor="let item of petStatus" (click)="changePetStatus(item)">
                    {{item.name}}</button>
            </div>
        </div>
        <div>
            <button class="btn btn-link float-sm-right" (click)=logOut()>Log Out</button>
        </div>
    </div>
</div>
<div class="row">
    <div class="col-sm-8">
        <table class="table table-striped">
            <thead>
                <tr>
                    <td class="w-25">
                        <p> Pet </p>
                    </td>
                    <td class="w-30">
                        <p> Category</p>
                    </td>
                    <td class="w-50">
                        <p> Customer</p>
                    </td>
                    <td class="w-50">
                        <p> Action</p>
                    </td>
                </tr>
            </thead>
            <tr *ngFor="let pet of pets$ | async |  select: 'allPets' ">
                <td>
                    {{pet.name}}
                </td>
                <td>
                    {{pet.category}}
                </td>
                <td>
                    {{pet.inCareOf?.name}}
                </td>
                <td>
                    <button (click)="checkInOut(pet)" *ngIf="loggedIn"
                        class="btn btn-link">{{pet.status == 'AVAILABLE' ? 'Check Out' : 'Check In' }}</button>
                </td>
            </tr>
        </table>
    </div>
</div>

Service Layer

Next, add a pet.service.ts under services folder and update the code as shown below.

  getPetsByStatus(petStatus: string) {
    return this.apollo
      .watchQuery({
        query: filterPetsQuery,
        fetchPolicy: 'network-only',
        variables: {
          status: petStatus,
        },
      })
      .valueChanges;
  }

I am adding 'network-only'as fetch-policy since for this call, I don't want the results from the cache and will always request data from the server. Apollo watchQuery returns an Observable to which we will subscribe in list.component.ts.

Login

To perform Check In/Check Out we need to login first, as we have to send in a user token with every request. So let's work on our login component. In the command prompt enter

ng g c login

Then edit login.component.html to add this following markup.

<h2>Login</h2>
<form #loginForm="ngForm" (ngSubmit)="loginForm.form.valid && onSubmit()">
    <p style="color: red;font-weight: 600;">{{errorMessage}}</p>
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" name="username" [(ngModel)]="model.username" #username="ngModel" class="form-control"
            [ngClass]="{ 'is-invalid': loginForm.submitted && username.invalid }" required />
        <div *ngIf="loginForm.submitted && username.invalid" class="invalid-feedback">
            <div *ngIf="username.errors.required">User Name is required</div>
        </div>
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password" [(ngModel)]="model.password" #password="ngModel" class="form-control"
            [ngClass]="{ 'is-invalid': loginForm.submitted && password.invalid }" required />
        <div *ngIf="loginForm.submitted && password.invalid" class="invalid-feedback">
            <div *ngIf="password.errors.required">Password is required</div>
        </div>
    </div>
    <div class="form-group">
        <button [disabled]="loading" class="btn btn-primary">Login</button>
        <a routerLink="/register" class="btn btn-link">Register</a>
    </div>
</form>

Add a login mutation under queries folder as shown below. We will create a class called LoginQL which extends from a Mutation class from apollo-angular.

import { Injectable } from '@angular/core';
import { Mutation } from 'apollo-angular';
import gql from 'graphql-tag';

@Injectable({
    providedIn: 'root'
})
export class LoginQL extends Mutation {
    document = gql`
    mutation($username:ID!,$password:String!) {
        logIn(username:$username, password:$password){
          customer{
            name
          }
          token
        }
      }
`;
}

Let's add the login method to account service and store the token for future requests.

  login(uname: string, pass: string) {
    return this.loginQL.mutate({
      username: uname,
      password: pass
    }
    ).pipe(tap(success =>
      this.token = (success as any).data.logIn.token
    ));
  }

Now we should be able to call this method from the login component and on successful login, navigate the user to the list screen.

  onSubmit() {
    this.errorMessage = null;
    this.accountService.login(this.model.username, this.model.password).subscribe(c => {
      this.router.navigateByUrl('/list');
    }, e => this.errorMessage = e.message);
  }

For this demo purpose I have added a user name "jsmith" and password "pass" using the playground. You should be able to use this credentials to login.

CheckIn/CheckOut

Since now we have the user token, we should be able to Check In/Check Out pets from the library. Let's add a check in mutation under queries folder.

import { Injectable } from '@angular/core';
import { Mutation } from 'apollo-angular';
import gql from 'graphql-tag';

@Injectable({
    providedIn: 'root'
})
export class CheckInQL extends Mutation {
    document = gql`
    mutation CheckIn($petId:ID!) {
        checkIn(id:$petId){
            pet {
                id
                name
                status
            }
            checkOutDate
            checkInDate
            late
        }
      }
`;
}

and also update our pet service method.

  checkIn(pId: string) {
    this.checkInQL.mutate({
      petId: pId
    },
      { refetchQueries: [`petsQuery`] }
    ).subscribe();

  }

We can call this method from our List when the user clicks on the Check In link.

  checkInOut(pet: any) {
    if (pet.status === 'AVAILABLE') {
      this.petService.checkOut(pet.id);
    } else {
      this.petService.checkIn(pet.id);
    }
  }

Since we need to modify every Check In request with the user token, we will add a middleware like shown below. We also need to pass AccountService to the middleware, since the user token is stored there.

const middleware = (acctService: AccountService) => new ApolloLink((operation, forward) => {
  if (acctService && acctService.token) {
    operation.setContext({
      headers: new HttpHeaders().set('Authorization', `Bearer ${acctService.token}`)
    });
  };
  return forward(operation);
});

export function createApollo(httpLink: HttpLink, acctService: AccountService) {
  return {
    link: middleware(acctService).concat(httpLink.create({ uri })),
    cache: new InMemoryCache(),
  };
}

Don't forget to add account service to dependencies in module declaration.

@NgModule({
  exports: [ApolloModule, HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, AccountService],
    },
  ],
})

You can view the full source code here.

Top comments (1)

Collapse
 
mcwebdev profile image
Matt Charlton

Awesome write up Raj ~@@~ Very helpful!! If anyone from the frontend crowd lands on this excellent blog post, you may want to check out youtube.com/watch?v=PeAOEAmR0D0