One way for handling retrieving and displaying data from an API is to route a user to a component, and then in that component’s ngOnInit
hook call a method in a service to get the necessary data. While getting the data, perhaps the component can show a loading indicator.
There is another way to use what is known as a route resolver
, which allows you to get data before navigating to the new route.
One API that is available for use is the Hacker News API. Hacker News is a website for sharing links and discussing them. The API can be used to retrieve the most popular posts and display information about individual posts.
In this tutorial, you will implement a route resolver that gets data from the Hacker News API before navigating to a route that displays the gathered data.
To complete this tutorial, you will need:
This tutorial was verified with Node v15.3.0, npm
v6.14.9, @angular/core
v11.0.1, @angular/common
v11.0.1, @angular/router
v11.0.1, and rxjs
v6.6.0.
For the purpose of this tutorial, you will build from a default Angular project generated with @angular/cli
.
- npx @angular/cli new angular-route-resolvers-example --style=css --routing --skip-tests
This will configure a new Angular project with styles set to “CSS” (as opposed to “Sass”, Less", or “Stylus”), routing enabled, and skipping tests.
Navigate to the newly created project directory:
- cd angular-route-resolvers-example
At this point, you have a new Angular project with @angular/router
.
Let’s start by implementing a resolver that returns a string after a delay of 2 seconds. This small proof of concept can help with exploring the fundamentals of wiring routes that can be applied to larger projects.
First, create a separate class for the resolver in a file of its own:
- ./node_modules/@angular/cli/bin/ng generate resolver news
This will use the @angular/cli
to generate a resolver named news
:
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class NewsResolver implements Resolve<Observable<string>> {
resolve(): Observable<string> {
return of('Route!').pipe(delay(2000));
}
}
Implementing the Angular router’s Resolve
interface requires the class to have a resolve
method. Whatever is returned from that method will be the resolved data.
This code will return an observable that wraps a string after a delay of 2 seconds.
In order to experience two different routes, you will need two new components. home
will be the landing page. And top
will feature the top posts from Hacker News API.
First, use @angular/cli
to generate a home
component:
- ./node_modules/@angular/cli/bin/ng generate component home
Then, use @angular/cli
to generate a top
component:
- ./node_modules/@angular/cli/bin/ng generate component top
Now you can set up the routing module to include the resolver.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NewsResolver } from './news.resolver';
import { TopComponent } from './top/top.component';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: HomeComponent
},
{
path: 'top',
component: TopComponent,
resolve: { message: NewsResolver }
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Notice how the resolver is provided just like a service and then you include the resolver with the route definition. Here the resolved data will be available under the message
key.
In the component, you can access the resolved data using the data
property of ActivatedRoute
’s snapshot
object:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ ... })
export class TopComponent implements OnInit {
data: any;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.data = this.route.snapshot.data;
}
}
Now, in the component, you can access the Route!
message like so:
<p>The message: {{ data.message }}</p>
At this point, you can compile your application:
- npm start
And visit localhost:4200/top
in a web browser.
OutputThe message: Route!
You will observe when navigating to the top
route that there is now a 2-second delay because the data is resolved first.
Let’s make things more real-life by actually getting some data from an API. Here you will create a service that gets data from the Hacker News API.
You will need HttpClient to request the endpoint.
First, add the HttpClientModule
to app.module.ts
:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Then, create a new service:
- ./node_modules/@angular/cli/bin/ng generate service news
This will use the @angular/cli
to generate a service named news
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class NewsService {
constructor(private http: HttpClient) { }
getTopPosts() {
const endpoint = 'https://hacker-news.firebaseio.com/v0/topstories.json';
return this.http.get(endpoint);
}
}
And now you can replace the string code in NewsResolver
with NewsService
:
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs';
import { NewsService } from './news.service';
export class NewsResolver implements Resolve<any> {
constructor(private newsService: NewsService) {}
resolve(): Observable<any> {
return this.newsService.getTopPosts();
}
}
At this point, if you look at the top
route in a browser, you will be presented with a list of numbers representing id
s of the top posts on Hacker News.
You can get access to the current route parameters in your resolver using the ActivatedRouteSnapshot
object.
Here’s an example where you would use a resolver to get access to the id
param of the current route.
First, use the @angular/cli
to generate a resolver named post
:
- ./node_modules/@angular/cli/bin/ng generate resolver news
Then, modify post.resolver.ts
to use ActivatedRouteSnapshot
:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { NewsService } from './news.service';
@Injectable({
providedIn: 'root'
})
export class PostResolver implements Resolve<any> {
constructor(private newsService: NewsService) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.newsService.getPost(route.paramMap.get('id'));
}
}
Next, add a getPost
method to NewsService
:
// ...
export class NewsService {
constructor(private http: HttpClient) { }
// ...
getPost(postId: string) {
const endpoint = 'https://hacker-news.firebaseio.com/v0/item';
return this.http.get(`${endpoint}/${postId}.json`);
}
}
And add PostResolver
and the post/:id
route to app-routing.module.ts
:
// ...
import { PostResolver } from './post.resolver';
// ...
const routes: Routes = [
// ...
{
path: 'post/:id',
component: PostComponent,
resolve: { newsData: PostResolver }
}
];
// ...
Next, create the new PostComponent
:
- ./node_modules/@angular/cli/bin/ng generate component post
Then, modify post.component.ts
to use snapshot data:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ ... })
export class PostComponent implements OnInit {
data: any;
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
this.data = this.route.snapshot.data;
}
}
And modify post.component.html
to display the title
:
<p>{{ data.newsData.title }}</p>
Now if a user goes to http://localhost:4200/post/15392112
, the data for the post id 15392112
will be resolved.
In case there’s an error while fetching the data, you could catch and deal with the error in the resolver using RxJS’s catch operator. Something like this for example:
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { NewsService } from './news.service';
@Injectable()
export class NewsResolver implements Resolve<any> {
constructor(private newsService: NewsService) {}
resolve(): Observable<any> {
return this.newsService.getTopPosts().pipe(catchError(() => {
return of('data not available at this time');
}));
}
}
Or you could return an EMPTY
observable and return the user to the root path:
import { Injectable } from '@angular/core';
import { Router, Resolve } from '@angular/router';
import { Observable, EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { NewsService } from './news.service';
@Injectable()
export class NewsResolver implements Resolve<any> {
constructor(private router: Router, private newsService: NewsService) {}
resolve(): Observable<any> {
return this.newsService.getTopPosts().pipe(catchError(() => {
this.router.navigate(['/']);
return EMPTY;
}));
}
}
These two approaches will lead to a better user experience if there is an error when retrieving data from the API.
In this tutorial, you implemented a route resolver that gets data from the Hacker News API before navigating to a route that displayed the gathered data. This was achieved by utilizing @angular/router
, @angular/common/http
, and rxjs
.
If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
This comment has been deleted