Angular Service Architektur & Routing

14.04.2021 Tom Trapp
Mobile angular foss javascript frontend framework handson tutorial howto

In the next part of the Angular series, we want to adapt our to-do list a little architecturally. We implement a ToDoService, add a second page and control it under certain conditions.

Service architecture

In software development, it is generally desirable to keep everything as variable and modular as possible. This also applies to Angular, so far we had the list with our to-do items in ToDosComponent, we now want to adapt and outsource this.

Real World Case:

The architecture would be similar for a real application, only the data storage would be in an external database, e.g. B. can be done via the REST interface.

First we want to create our first service in Angular via CLI:

ng g service services/to-do

What does this service have to be able to do? He should:

  • Save the list of To Do Items
  • Return the list of To Do Items
  • Add a new to do item
  • Check off a to do item
  • To delete a to do item
  • Check whether all to-do items are done

We use the LocalStorage functionality of Angular to save our To Do list, this allows us to persist the data and to be able to read it out again.

We use the annotation Injectable to register this class as a service. Learn more about the advantages of this service architecture in our Angular pro tips.

Our fully implemented ToDoService can be seen below:

//to-do.service.ts

import { Injectable } from '@angular/core';
import { ToDo } from '@models/ToDo';

@Injectable({
  providedIn: 'root'
})
export class ToDoService {
  
  toDos:ToDo[] = [];
    
  constructor() {
    let toDoString= localStorage.getItem("toDos");
    if(toDoString){
      this.toDos = JSON.parse(toDoString);
    } else {
      this.toDos = [
        {
          content: 'Get tea',
          completed: true
        },
        {
          content: 'Write blog post',
          completed: false
        },
        {
          content: 'Publish blog post',
          completed: false
        }
      ]
    }
  }

  getToDos():ToDo[]{
    return this.toDos
  }

  addToDo(toDo:ToDo):void{
    this.toDos.push(toDo);
    this.setOrUpdateLocalStorage();
  }
  
  toggleCompleted(indexToUpdate:number):void {
    this.toDos.map((item,index) => {
      if (index == indexToUpdate) {
        item.completed = !item.completed;
      }
    })
    this.setOrUpdateLocalStorage();
  }

  deleteToDo(indexToDelete:number):ToDo[] {
    this.toDos = this.toDos.filter((item,index) => index !== indexToDelete);
    this.setOrUpdateLocalStorage();
    return this.toDos;
  }

  areAllToDosCompleted():boolean {
    return this.toDos.every(x => x.completed === true);
  }

  private setOrUpdateLocalStorage(){
    localStorage.setItem("toDos",JSON.stringify(this.toDos));
  }
}

Now we have created an encapsulated logic class with all the business logic for our to-do items. We can now inject this service as a dependency via the constructor of every TypeScript class. So we have an independent, reusable class that we can address as often as we want.

In the following you can see that we still need the variable toDos in our ToDosComponent in order to be able to access it in the template. Replace the content of your ToDosComponent class with the following code:

// to-dos.component.ts
...
export class ToDosComponent implements OnInit {

    toDos:ToDo[] = [];
    inputToDoText:string = 'abc';

    constructor(private toDoService:ToDoService) { }

    ngOnInit(): void {
        this.toDos=this.toDoService.getToDos();
    }

    handleToDoEvent(toDoEvent:ToDoEvent){
        if(toDoEvent.type === ToDoEventType.COMPLETE){
            this.toDoService.toggleCompleted(toDoEvent.index);
        } else if (toDoEvent.type === ToDoEventType.DELETE){
            this.toDos = this.toDoService.deleteToDo(toDoEvent.index);
        }
    }

    addToDo(){
        this.toDoService.addToDo({
            content: this.inputToDoText,
            completed: false
        });
        this.inputToDoText = "";
    }
}

Another big advantage of this architecture is that the data source and template components are separate from each other. For example, we could implement an endpoint to get the data and would only have to adapt our ToDoService, all components remain the same.

The second side

But now we want to bring some life to our application, we are creating a second page!

The aim is to be able to show the user a page that can only be called up when all to-do items have been checked.

A page in Angular is basically nothing else than a component, as we know, we create it as follows:

ng g c components/success

The HTML in this case should be kept very simple, the user should only be shown a message:

<!-- success.component.html -->

<h2>Success! You've completed all To Do items, great!</h2>

Now we could integrate the component as well as our To Dos component via selector, but we really want to have a URL to call up the page.

routing

Angular routing allows us to do just that: you bind a component to a path which can then be called up in the browser.

We do all configuration in the app.module.ts file, there all URLs including their components are defined. In our case we say that the ToDosComponent should be rendered by default. The path "success" then leads to SuccessComponent.

//app.module.ts

import { RouterModule } from '@angular/router';
...
imports: [
...
RouterModule.forRoot([
      {path: '', component: ToDosComponent},
      {path: 'success', component: SuccessComponent},
    ])
...

Another change we have to make in order to be able to use routing at all is to adapt our app.component.html template. The selector of ToDosComponent is currently permanently entered there, we have to replace it with the selector of the Angular router module:

<!-- app.component.html -->

<div class="app">
    <header>
        <h1>b-nova To Do List</h1>
    </header>
    <router-outlet></router-outlet>
</div>

Our new component can now be successfully accessed via the /success URL.

It is important to note that the order of the routes must be correct, a so-called first match wins strategy is used. This means that a wildcard routes (typically leading to a 404 page) must be at the end. Of course, the RouterModule offers other options such as sending parameters, redirects, nested routes and many more.

Now we want to automatically redirect to the url /success if all todos are checked off, for this use the Angular class Router. At every Complete Event, we check whether all to-dos have already been completed.

//to-dos.component.ts

  handleToDoEvent(toDoEvent:ToDoEvent){
    if(toDoEvent.type === ToDoEventType.COMPLETE){
      this.toDoService.toggleCompleted(toDoEvent.index);
      if(this.toDoService.areAllToDosCompleted()){
        this.router.navigateByUrl('/success');
      }
    } else if (toDoEvent.type === ToDoEventType.DELETE){
      this.toDos = this.toDoService.deleteToDo(toDoEvent.index);
    }
  }

Guard

But how do we ensure that the SuccessComponent can only be called when all to-do items have been checked?

Of course, we were able to implement logic in the component itself and fire a redirect in the event of an error, but Angular also offers us a leaner solution here.

A so-called Guard allows us to carry out certain actions or to link access to a route to a condition.

There are basically five different types of guards:

  • CanActivate - Checks whether the route can be called
  • CanActivateChild - Checks whether a child route can be called
  • CanDeactivate - Checks whether the current route can be left
  • Resolve - Request and prepare data before a route is called
  • CanLoad - Checks whether children can be loaded, useful for lazy loading with multiple modules

To achieve our goal, all we need is CanActivate Guard, which we can create via Angular CLI as follows:

ng g guard guards/to-do-completed --implements CanActivate

Now we want to implement our guard in such a way that it redirects back to the root page in the event of an error.

Here again the great advantage of the service architecture comes to the fore, since we can inject and use our ToDoService as a dependency.

Using the class Router we can navigate programmatically to the root page.

//to-do-completed.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { ToDoService } from '../services/to-do.service';

@Injectable({
    providedIn: 'root'
})
export class ToDoCompletedGuard implements CanActivate {

    constructor(private toDoService:ToDoService, private router:Router){}

    canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

        const areAllToDosCompleted:boolean = this.toDoService.areAllToDosCompleted();
        if(!areAllToDosCompleted){
            this.router.navigateByUrl('/');
        }

        return areAllToDosCompleted;
    }

}

Now we only have to assign the guard to a route so that it is also used.

//app.module.ts

    {path: 'success', component: SuccessComponent,canActivate: [ToDoCompletedGuard]},

Another useful feature is that the guards can be specified as an array and a route can only be called if all guards return true. In addition, a guard can be used as often as required.

Now we have successfully created a second page in our service-based ToDo application. A guard allows us to control access rights for this page.

The complete 'b-nova ToDo-List' project can be found on GitHub.

Stay tuned!


This text was automatically translated with our golang markdown translator.

Tom Trapp - problem solver, innovator, athlete. Tom prefers to work on modern software all day long and attaches great importance to objectively clean, lean code.