October 6, 2018

Photo Gallery Part 4 - Angular Services & API Calls

Coding Tutorial - Using Angular Services and custom defined data types to add and view albums in our Photo Gallery app.

Photo Gallery Part 4 - Angular Services & API Calls

The next step of our Photo Gallery is to link our frontend to our backend. We do this by using Angular services to call our backend API. If you forget how our API is structured, check out part 2.

If you remember from part 3, we created a navbar service. We are going to follow those same steps to create a photos and albums service. Both of which will link to our API routes.

Create Services

Open a terminal and navigate to to the angular-src directory. We are going to execute these 2 commands to have the Angular CLI generate our services for us.

ng g s services/album
ng g s services/photo

A little trick I use every time I generate something with Angular CLI is to put '--dry-run' at the end of the command. This allows me to see the output of the command without the files actually being created. Once you have confirmed the files are being generated in the proper directories, remove the '--dry-run' and execute the command again.

Album Service

We are going to start with our album service. To make things a little simpler, we are going to allow users to create a new album, and retrieve the album. We will look at editing and deleting at a later time.

import { Observable } from 'rxjs/Observable';
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable()
export class AlbumService {

 albums: Array<any> = [];

 constructor(private http: HttpClient) { }

 getAlbum(id): Observable<any> {
   let headers = new HttpHeaders();
   headers = headers.set('Content-type', 'application/json');
   return this.http.get('/api/albums/' + id, { headers: headers });
 }

 getAlbums(): Observable<any> {
   let headers = new HttpHeaders();
   headers = headers.set('Content-type', 'application/json');
   return this.http.get('/api/albums', { headers: headers });
 }

 addAlbum(album: any): Observable<any> {
   let headers = new HttpHeaders();
   headers = headers.set('Content-type', 'application/json');
   return this.http.post('/api/albums', { headers: headers });
 }
}

Although the above code will work, we are slightly cheating here. Typescript allows us to define types for our data. We are saying that we will receive an observable and the data we receive will be of type any. We will now clean this up by creating a new data type for our photo album.

Custom Data types

Although we could use an Angular interface, we are going to define our new type by creating an album class. Similar to components and services, we can use Angular's CLI to create our class. Inside the angular-src directory, execute the following in your terminal window.

$ ng g class classes/album

We can create our class to match our Mongo model. Looking at album schema, we know our albums are going to have a name, description, and an array of pictures. We are also going to add an id field since Mongo will automatically add that to the document.

import { Picture } from './picture';

export class Album {
  constructor(
    public _id?: string,
    public name?: string,
    public description?: string,
    public pictures?: Array<Picture>
  ) {}
}

Again, I am using shorthand here. You could write this class many different ways, but what we are saying is that our class has 4 optional elements that match what we will receive from our database.

One thing to notice, is we are referencing a Picture object, which we haven't created yet. Similar to the Album class, we are creating a separate class just for the pictures.

$ ng g class classes/picture

picture.ts

export class Picture {
  constructor(
    public _id?: string,
    public data?: string,
    public contentType?: string
  ) {}
}

Album Service

Back in our album service, we are going to update it to use our newly created Album class instead of the type any.

album.service.ts

import { Observable } from 'rxjs/Observable';
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Album } from '../classes/album';

@Injectable()
export class AlbumService {

  albums: Array<Album> = [];

  constructor(private http: HttpClient) { }

  getAlbum(id): Observable<Album> {
    let headers = new HttpHeaders();
    headers = headers.set('Content-type', 'application/json');
    return this.http.get('/api/albums/' + id, { headers: headers });
  }

  getAlbums(): Observable<Album[]> {
    let headers = new HttpHeaders();
    headers = headers.set('Content-type', 'application/json');
    return this.http.get<Album[]>('/api/albums', { headers: headers });
  }

  addAlbum(album: any): Observable<Album> {
    let headers = new HttpHeaders();
    headers = headers.set('Content-type', 'application/json');
    return this.http.post('/api/albums', { headers: headers });
  }
}

Looking at our progress

If we run our code now, you will see that when we open the sidebar we have a button that says add album, instead of 'loading' like we saw before.

Clicking on the button won't do anything yet, but we work on that now.

Add Album Dialog Window

We are going to create a very basic dialog window, which allows the user to enter a name and description for a new album. The MatDialog is a angular material component which we are going to use to do this. We already added the MatDialog to our angular.material.ts file.

The MatDialog allows us to open a new component inside a dialog window. It also allows us to pass in information from the master component, as well as pass information from the dialog back into the master component. One key thing to note is that we have to declare the components we are going to use in our dialog as entry points in our app.module.ts file. For more information on MatDialog's, feel free to check out the official documentation here.

We are going to use the AddAlbumComponent, which we created during the last part of this series. Inside your app.module.ts file, be sure to add the following. I normally add it under the imports block.

entryComponents: [AddAlbumComponent],

Inside our AddAlbumComponent, we are going to create a basic form that allows users to enter the album name and description. We are going to use Angular's reactive forms for this, so the first thing to do is import it the following to our app.module.ts file

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
...
imports: [
...
    FormsModule,
    ReactiveFormsModule,
...
]
...

We will dive into reactive forms in more detail in a different post, but you can find the offical documentation here.

Our AddAlbumComponent html looks like this

<form [formGroup]="addAlbumForm" (ngSubmit)="submitAlbumClick()">
   <h1 mat-dialog-title>Add Album</h1>
    <mat-dialog-content>
        <mat-form-field>
            <input matInput formControlName="albumName" type="text" placeholder="Album Name">
        </mat-form-field>
        <br>
        <mat-form-field>
            <input matInput formControlName="albumDescription" type="text" placeholder="Description">
        </mat-form-field>
    </mat-dialog-content>
    <mat-dialog-actions>
        <button mat-button type="submit">Add</button>
        <button mat-button mat-dialog-close type="button">Cancel</button>
    </mat-dialog-actions>
</form>

and AddAlbumComponent.ts looks like

import { AlbumService } from './../../services/album.service';
import { Component, OnInit, Inject } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { Album } from '../../classes/album';

@Component({
  selector: 'app-add-album',
  templateUrl: './add-album.component.html',
  styleUrls: ['./add-album.component.css']
})
export class AddAlbumComponent implements OnInit {

  addAlbumForm: FormGroup;

  constructor(private albumService: AlbumService, private fb: FormBuilder, public dialogRef: MatDialogRef<AddAlbumComponent>,
  @Inject(MAT_DIALOG_DATA) public data: any) { }

  ngOnInit() {
    this.addAlbumForm = this.fb.group({
      'albumName': [null, null],
      'albumDescription': [null, null]
    });
  }

  onNoClick(): void {
    this.dialogRef.close();
  }

  submitAlbumClick() {
    const album = new Album(this.addAlbumForm.controls.albumName.value, this.addAlbumForm.controls.albumDescription.value);
    this.albumService.addAlbum(album)
      .subscribe((res) => {
        this.dialogRef.close(res);
      });
  }

}

A few things to highlight are

  • We create a form group called addAlbumForm, and assign a albumName and albumDescription property to it.
  • the onNoClick function says to close the dialog window when you click off the window
  • The submit button calls our albumService to create a new album on the backend and then closes the dialog window.

The last part is open the AddAlbumComponent as a dialog window when the user clicks on the 'Add Album' button on our sidenav. The on-click event for our button is called addAlbumClick() and here is the logic needed to open the dialog

  addAlbumClick() {
    const dialogRef = this.dialog.open(AddAlbumComponent, {
      data: { albumName: this.albumName }
    });
  }

We are telling Angular to open a dialog box, and use the AddAlbumComponent as the content for the dialog.

Viewing Albums

Being able to add albums to our app is great, but we need a way of viewing them as well. Back in our sidenav.component.html, take a look at line 6.

<a mat-list-item (click)="closeSideNav()" *ngFor="let album of albums" [routerLink]="['gallery', album._id]">{{ album.name }}</a>

We have already added the code to our view to loop through all of our albums and display them in our side bar. If you take a look at the sidenav.component.ts file, you will also see our 'albumsObservable' which calls our service and reaches out to our server to retrieve the data.

public albumsObservable: Observable<Array<Album>>;
this.albumsObservable = this.albumService.getAlbums().map(data => this.albums = data['albums']);

This service call might look a little different than the ones your are use to. We are returning the actual observable object instead of just the data like normal. We are doing this so that way the sidebar won't finish rendering our albums until all of them have been received from the server. Our app displays 'loading' until the data is retreived, at which point we display the albums in a list. The album data is stored inside our albums variable and we use map to assign the values after the observable object returns them from the server.

Conculsion

We have covered a lot so far in this post, so I am going to leave end it there for this week. I hope you enjoyed this section of the series, as we continue to build out our photo galery app.

Next week we will look at uploading photos to our albums and the differences between saving them on the server compared to saving them in our data base.

All code can be found on my Github.

I look forward to your comments and seeing what tweaks and changes you have made to your photo album.