September 21, 2018

Photo Gallery Part 3 - UI Design

Coding Tutorial - Building our User Interface of our Photo Gallery app. Leveraging Angular and Material Design to build our app.

Photo Gallery Part 3 - UI Design

Welcome back to part 3. This time we are going to work on creating the UI for our photo gallery. If you are not caught up, make sure you check out part 1 and part 2.

House Keeping

Changing the Angular Build Directory

Angular will automatically put your finished app in a dist folder after you build it. This is normally fine, but since our Node server is going to look inside the public folder for the frontend, we need to change Angular's default build directory. We do this by editing the .angular-cli.json file. Change the value for outDir from './dist' to '../public'.

Custom npm scripts

Next we are going to create a customer npm script. Normally you would use the 'ng serve' command to start a development server for Angular. Since we want our backend and frontend to work together, we are going to use the 'ng build -w' command instead. This will build our angular frontend, and rebuild it after we make changes to it.

Inside package.json, your scripts section should not look like .

"scripts": {
     "start": "node app.js",
     "dev": "nodemon app.js",
     "ng-start": "cd angular-src && ng build -w",
     "test": "echo \"Error: no test specified\" && exit 1"
 },

Material Design Modules

Let's start our design journey with adding the needed modules we will need from the material design.

Lets open our material.module.ts file, and make the following changes to it.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
  MatTableModule,
  MatInputModule,
  MatDialogModule,
  MatToolbarModule,
  MatCardModule,
  MatIconModule,
  MatButtonModule,
  MatSidenavModule,
  MatListModule
} from '@angular/material';

@NgModule({
  imports: [
    MatTableModule,
    MatInputModule,
    MatDialogModule,
    MatToolbarModule,
    MatCardModule,
    MatIconModule,
    MatButtonModule,
    MatSidenavModule,
    MatListModule
  ],
  exports: [
    MatTableModule,
    MatInputModule,
    MatDialogModule,
    MatToolbarModule,
    MatCardModule,
    MatIconModule,
    MatButtonModule,
    MatSidenavModule,
    MatListModule
  ]
})
export class MaterialModule {}

Don't worry about all of these for now, we will highlight each component as we use them in our code.

Components

Now we are going to create all of the Angular components we are going to be coding. You could create them one by one, but instead we will create them all at the same time. I like keeping our folder structure tidy, so we are going to place all of our components inside a components folder under angular-src/src/app/. I am using the short-form of the command, the full command is ng generate component

ng g c components/home
ng g c components/navbar
ng g c components/navbar/sidenav
ng g c components/gallery
ng g c components/add-photo
ng g c components/add-album

Application Routing

Inside the app.component.html file, replace the contents with our sidenav

<app-sidenav></app-sidenav>

Next, lets create the routing for our application. For right now, it is going to be very basic.

In angular-src/src/app/app-routing.module.ts, add the following to the routes variable at the top.

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'gallery/:id', component: GalleryComponent }
];

This tells our application that our default route(/) will go to our Home component, and that we have gallery route which will go to our Gallery component, and provide the gallery's ID.

Sidenav and Navbar

You will commonly see that you add the navbar and router outlet to your app.component.html file. However, in this case we are going to add our sidebar as the root element in our app.component.html and then add all of our content inside there. Let's get started

App Component

Inside app.component.html, replace the content with

<app-sidenav></app-sidenav>

Sidenav

Next we are going to create the sidenav, which is where we will put our navbar and router outlet.

sidenav.component.html

<mat-sidenav-container>
    <mat-sidenav #sidenav mode="side" [opened]="state" [fixedInViewport]="true">
        <mat-nav-list *ngIf="albumsObservable | async; let albums; else loading">
            <button mat-raised-button color="primary" (click)="addAlbumClick()"><mat-icon>add</mat-icon> Add Album</button>
            <hr>
        </mat-nav-list>
    </mat-sidenav>
    <mat-sidenav-content style="background-color: #fff;">
        <app-navbar></app-navbar>
        <router-outlet></router-outlet>
    </mat-sidenav-content>
</mat-sidenav-container>
<ng-template #loading>Loading ...</ng-template>

Lastly, we are going to create the navbar. For now, we will keep it simple with just a Home button and a way of opening the sidenav.

navbar.component.html

<mat-toolbar color="primary">
    <mat-toolbar-row>
        <button mat-icon-button (click)="toggleSideNavClick()"><mat-icon>menu</mat-icon></button>
        <span>Photo Gallery</span>
        <button mat-button [routerLinkActive]="['active']" [routerLinkActiveOptions]="{extended: true}" [routerLink]="['']"><mat-icon>home</mat-icon> Home</button>
    </mat-toolbar-row>

We need to add some logic to our typescript, which will allow the sidenav to open and close.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.css']
})
export class NavbarComponent implements OnInit {
  albumName = '';

  constructor(private router: Router) { }

  ngOnInit() {
  }

  toggleSideNavClick() {
    this.navService.toggleSidebar();
  }
}

You can see that we have created the toggleSideNavClick function, but it is making a call to some service which we haven't created yet. Although we could have done all of this code from within our components, I thought it would be fun to create a service to do it for us.

Navbar Service

We'll start by creating the service with the Angular CLI

ng g service services/navbar

In our service, we are going to create a behaviorsubject and use it as an observable. This allows us to change the state of the sienav and have our components reflect the changes in the browser.

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class NavbarService {

  private state = false;
  private sidebarOpen = new BehaviorSubject<boolean>(false);
  sidebarState = this.sidebarOpen.asObservable();

  constructor() { }

  toggleSidebar() {
    if (this.state) {
      this.sidebarOpen.next(false);
    } else {
      this.sidebarOpen.next(true);
    }
    this.state = !this.state;
  }

  private openSidebar() {
    this.state = true;
    this.sidebarOpen.next(true);
  }

  private closeSidebar() {
    this.state = false;
    this.sidebarOpen.next(false);
  }
}

Don't forget to add our new service to the provider section app.module.ts.

Now, every time we click on the menu icon, it will change the state of our service to either true or false.

If you look at in your browser, you will see that the sidebar still doesn't open. This is because we have to tell our sidebar to look in our service to get it's state.

Open sidenav.component.ts and add the following

import { NavbarService } from './../../../services/navbar.service';
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { MatSidenav } from '@angular/material/sidenav';

@Component({
  selector: 'app-sidenav',
  templateUrl: './sidenav.component.html',
  styleUrls: ['./sidenav.component.css']
})
export class SidenavComponent implements OnInit {
  @ViewChild('sidenav') sidenav: MatSidenav;
  state: boolean;

  constructor(public router: Router, private navbarService: NavbarService) { }

  ngOnInit() {
    this.navbarService.sidebarState.subscribe(res => this.state = res);
  }
}

By adding this, we are looking up the state of the sidebar inside our service and assigning it to our state variable. If you check the sidenav HTML, you will see the [open]="state", which is how it knows to change from open to close. Since we are using an observable, the sidenav will open/close when it receives an update from our service.

Testing

To run our app, we will want to open 2 terminals. Navigate to your project's folder and execute the following commands

Start the NodeJS Server (terminal 1)

npm run dev

Start Angular (terminal 2)

npm run ng-start

Once both have started, open your web browser and go to localhost:3000 to see what our app looks like.

One thing you will notice is that when you open the sidenav, you will see 'Loading...'. This is fine for the time being. The sidenav is going to by dynamic and will show the list of photo albums we have. Next time, we will add the needed services to load this data.

photo_gallery3-app_overview

Concussion

I think that is enough for this time. We have create the base of how our app will look. Using Angular Material components, we were able to create a sidenav and navbar to our application. We also created a service and used a behaviorsubject and observables to move the state of the sidebar between our sidebar and navbar component.

Next time we will focus on creating our services and making the needed API calls to our server.

As always, all of the code can be found on GitHub

Make sure you subscribe, so you get first access to part 4 of our Photo Album.