Building a Dynamic Serverless Photo Blog with Angular & Google Sheets - 1: Solving Angular Router Navigation Issues
Tags: angular, webdev, tutorial, serverless
Published: 2023-02-15 20:44:39

## TheIntro();

Hello **Devs**!

Are you looking to build a dynamic photo blog without the hassle of a traditional backend? In these series of articles, I'll share my experience building a server-less photo blog for my wife's photography portfolio using Angular and Google Sheets.

Hosting on a GitHub Page is a good option for a static site, but for a dynamic site, you'll need a backend. That's where Google Sheets comes in handy as a database, and a Google form to create new blog entries with photos.

In this series, I'll take you through the journey of building this photo blog and share some of the challenges I faced along the way, as well as how I solved them.

In this first part, I'll discuss how to solve Angular router navigation issues.

## TheProblem();

![Navbar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7eclo8k98c5mxxlfisgp.PNG)

As we continue to build the "**ESHARTISTIC**" photo blog, let's take a closer look at the navigation bar.
(Fun fact: My wife came up with the name "**ESHARTISTIC**" for her photo blog, which is a combination of her name, ESHA, and the word ARTISTIC. I think it's a pretty cool name, don't you?)
As you can see in the picture, the navigation bar consists of four categories: `Art`, `Design`, `Photography`, and `Craft`, all of which will be rendered using the same component in Angular. Additionally, the About page will be rendered in a different component.

To achieve this, we need to define the routes in the Angular router. The route for the four categories will be `list/:category`, where `:category` can be `art`, `design`, `photography`, or `craft`. This route will use the same component to render the different categories. The route for the About page will be `about`.

When a user clicks on any of the four category links in the navigation bar, the corresponding page should be rendered with a list of posts. To get the list of posts, we only need to call the `getPostList()` method once when the component starts to load.

So that's the two things I want to achieve.

## TheProbableSolution();

In Angular, `ngOnInit()` is a lifecycle hook method that is called only once when a component is initialized.

-> In my case I can call `getPostList()` inside `ngOnInit()` like this:

``` TS
import { ActivatedRoute } from '@angular/router';

export class ListComponent implements OnInit {
  category: string;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.category = this.route.snapshot.paramMap.get('category');
    this.getPostList(this.category);
  }

  private getPostList(category: string) {
    // fetch posts for the current category
  }
}

```
This code retrieves the current category parameter value from the URL and uses it to fetch the posts for that category using the `getPostList()` method only once when the component is loaded.

If I am currently on the Art page and click on any of the four links in the navigation bar, the `ngOnInit()` method will not be called again. This is because I am already on this component and `ngOnInit()` has already been called. Instead, what I need to do is detect a change in the :category parameter of the router.

-> To detect changes to the :category parameter of the router, I can use the `ngDoCheck()` lifecycle hook in Angular. This method is called every time the component is checked for changes, which makes it a good place to detect changes to the router parameter.

Here is an example implementation:

``` TS
import { Component, OnInit, DoCheck } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit, DoCheck {
  category: string;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.category = this.route.snapshot.paramMap.get('category');
    this.getPosts(this.category);
  }

  ngDoCheck() {
    const newCategory = this.route.snapshot.paramMap.get('category');
    if (newCategory !== this.category) {
      this.category = newCategory;
      this.getPostList(this.category);
    }
  }

  private getPostList(category: string) {
    // fetch posts for the current category
  }
}

```
Now if the new category is different from the current category, the category property is updated and the `getPostList()` method is called with the new category. This ensures that the component always displays the correct content based on the current category in the router.

Now let's explore another approach to achieve the same result, which is by using `ActivatedRoute`.

-> To reinitialize and the `ngOnInit()` method to be called again, I can subscribe to the `ActivatedRoute` parameter and detect changes in the route parameter. I can use the `paramMap` observable to detect changes and reinitialize the component as needed.

``` TS
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {
  category: string;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.route.paramMap.subscribe(params => {
      this.category = params.get('category');
      this.getPostList(this.category);
    });
  }

  private getPostList(category: string) {
    // fetch posts for the current category
  }
}
```
In the `ngOnInit()` method, we're subscribing to changes in the route parameter using `this.route.paramMap.subscribe()`. Whenever the category parameter changes, the subscribe method will be called and we can then update the category property and call the `getPostList()` method to fetch the posts for the new category.

In terms of detecting changes to the route parameters, using `ActivatedRoute` is generally the preferred approach in Angular because it provides a more efficient and reliable way to access the current route parameters.

After implementing the solution using `ActivatedRoute`, when navigating from the art page to design, photography, or craft pages, the `getPostList()` method will be called again. However, if the user clicks on the art link while already on the art page, the `getPostList()` method will not be called again because the parameter has not changed. Yeah! Life is not a full of roses!
Let's see how did I solve this problem.

## TheSolution();

In order to achieve our objective of reloading the list component and fetching posts whenever any of the four navbar links are clicked, we must first create a dummy route mapped to a dummy component.

``` TS
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutPageComponent } from './about-page/about-page.component';
import { DummyComponent } from './components/dummy/dummy.component';
import { ListPageComponent } from './list-page/list-page.component';
import { MainPageComponent } from './main-page/main-page.component';

const routes: Routes = [
  { path: 'post/:category', component: ListPageComponent },
  { path: 'post/:category/:id', component: MainPageComponent },
  { path: 'about', component: AboutPageComponent },
  { path: '', redirectTo: '/post/art', pathMatch: 'full' },
  { path: 'dummy', component: DummyComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```
We have added a dummy route mapped to a dummy component, the next step is to invoke an `onClick()` method when clicking on the navbar link. This method will first navigate to the dummy route and then immediately navigate to the clicked navbar route. While we could add the `onClick()` method in the navbar component .ts file, it may be required in other components as well. To address this, we have created a service with a `navigateToRoute()` method implementation, allowing us to call this method whenever and wherever needed.

``` TS
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable({
	providedIn: 'root'
})
export class RouteHandlerService {

	constructor(private router: Router) { }

	navigateToRoute(clickedRoute: string) {
		const currentRouteUrl = clickedRoute;

		this.router.navigateByUrl('/dummy', { skipLocationChange: true }).then(() => {
			this.router.navigate([currentRouteUrl]);
		});
	}
}
```
The `navigateToRoute()` method is defined in the `RouteHandlerService`. It takes a parameter called `clickedRoute`, which is the route that the user clicked on in the navbar.

Inside the `navigateToRoute()` method, the current route URL is set to the clicked route. Then, the `navigateByUrl()` method of the Router service is called with a dummy route and the option `skipLocationChange` set to `true`. This navigates to the dummy route without changing the URL in the browser's address bar.

Once the navigation to the dummy route is complete, the `router.navigate` method is called to navigate to the clicked route. This changes the URL in the browser's address bar and loads the corresponding component.

Overall, this service provides a way to navigate to a new route in an Angular application without causing the browser's address bar to display the intermediate route. This can be useful when you want to reload a component when a user clicks on a link, but you don't want to change the URL in the browser's address bar.

Then I created a `onClickMenuItem()` method in the navbar component and bind this with the navbar links.

``` TS
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RouteHandlerService } from 'src/app/services/route-handler.service';
import { AppConstants } from 'src/utils/appConstants';
import { Page } from 'src/utils/models/page';

@Component({
	selector: 'app-navbar',
	templateUrl: './navbar.component.html',
	styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent {

	pages: Page[] = AppConstants.pages;

	constructor(private router: Router, private routeHandlerService: RouteHandlerService) { }

	onClickMenuItem(clickedRoute: string) {
		this.routeHandlerService.navigateToRoute(clickedRoute);
	}
}
```

``` HTML
<a routerLink="{{page.getRouterLink()}}" (click)="onClickMenuItem(page.getRouterLink())" class="menu-text nav-link">{{page.getTitle()}}</a>
```
Whenever a navbar link is clicked, the `onClickMenuItem()` method will be invoked, which in turn calls the `navigateRoute()` method. As a result, the list component is reloaded every time we click on a navbar link.

Then in list component I just called `getPostList()` in `ngOnInit()` method.

``` TS
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { PostType } from 'src/utils/enums/postType';
import { PostService } from '../services/post.service';

@Component({
	selector: 'app-list-page',
	templateUrl: './list-page.component.html',
	styleUrls: ['./list-page.component.scss']
})
export class ListPageComponent implements OnInit {

	constructor(
		private postService: PostService,
		private router: Router
	) { }

	ngOnInit() {
		this.getPostList(this.getPostType(this.router.url));
	}

	async getPostList(postType: PostType) {
		await this.postService.getPosts(postType);
	}

	private getPostType(routeUrl: string): PostType {
		switch (routeUrl) {
			case "list/design":
				return PostType.DESIGN
			case "list/photography":
				return PostType.PHOTOGRAPHY
			case "list/craft":
				return PostType.CRAFT
			default:
				return PostType.ART
		}
	}
}
```

## Conclusion();

Certainly! In conclusion, this is the approach I took to solve the problem of reloading the list component and fetching the latest posts every time a navbar link is clicked. However, there may be other solutions or approaches that can achieve the same result or even better results. If anyone has a better solution or approach, I encourage them to share it with the community and keep improving the development practices.