Separate your concerns

with router resolver

@filip.mam

@filip.mam

@filip.mam

@filip.mam

+    SPAs    +

=

@filip.mam

Agenda

1. Why resolver?

2. The app

3. (re)Solving problems

Why resolver?

@filip.mam

@filip.mam

SPAs without Angular Router

@filip.mam

SPAs with Angular Router

@filip.mam

SPAs with Angular Router + guards

@filip.mam

Guards

www.app.com

@filip.mam

Guards

/feature-a

/feature-b

/feature-c

@filip.mam

Guards

const routes = [{
    path: "feature-a",
    component: FeatureAComponent
},
{
    path: "feature-b",
    component: FeatureBComponent
},
{
    path: "feature-c",
    component: FeatureCComponent
}]

@filip.mam

Guards

/feature-a

/feature-b

/feature-c

@filip.mam

Resolver

/feature-b

U got some data m8?

Static data

Promises

Observables

The app

@filip.mam

@filip.mam

<nav-button routerLink="movies-ranking"></nav-button>
<nav-button routerLink="tv-shows-rankig"></nav-button>
<nav-button routerLink="movie-finder"></nav-button>
<nav-button routerLink="profile"></nav-button>

...

<router-outlet></router-outlet>

...

routes = [{
    path: "movies-ranking",
    component: MoviesRankingComponent
},
{
    path: "tv-shows-ranking",
    component: TvShowsRankingComponent
},
...]

MoviesRankingComponent

@filip.mam

moviesList: Movie[] = [...]
...

<div *ngFor="let movie of movieList">
 ...
</div>

MoviesRankingComponent

@filip.mam

moviesList: Movie[] = []
...

<div *ngFor="let movie of []">
 ...
</div>

MoviesRankingComponent

@filip.mam

MoviesRankingComponent

@Component({
    ...
})
class MoviesRankingComponent {
    movieList: Movie[] = [];
    
    constructor(http: Http) {
       http.get('api/movies').subscribe(
            movies => this.movieList = movies;
       );
    }
}

lifecycle with async data

@filip.mam

MoviesRankingComponent

component instantiated with empty data

get request for data created

component rendered with empty data

data comes back, component rerendered

lifecycle with async data

route recognized

@filip.mam

MoviesRankingComponent

with good internet connection

@filip.mam

MoviesRankingComponent

with bad internet connection

Resolver to the rescue

@filip.mam

@filip.mam

MoviesResolver


const routes = [{
    path: "movies-ranking",
    component: MoviesListComponent
}]

@filip.mam

MoviesResolver


const routes = [{
    path: "movies-ranking",
    component: MoviesRankingComponent,
    resolve: {
        movies: MoviesResolver
    }
}]

@filip.mam

MoviesRankingComponent

with async data

@Component({
    ...
})
class MoviesRankingComponent {
    movieList: Movie[] = [];
    
    constructor(http: Http) {
       http.get('api/movies').subscribe(
            movies => this.movieList = movies;
        );
    }
}

@filip.mam

MoviesResolver


@Injectable()
class MoviesResolver implements Resolve<Movie[]> {
    
    constructor(http: Http) {}

    public resolve(): Observable<Movie[]> {
        return this.http.get("api/movies");
    }
}

@filip.mam

MoviesRankingComponent

with data from resolver

@Component({
    ...
})
class MoviesRankingComponent {
    movieList: Movie[] = this.activatedRoute.snapshot.data.movies;
    
    constructor(private activatedRoute: ActivatedRoute) {}
}

@filip.mam

MoviesRankingComponent

with data from resolver

@Component({
    ...
})
class MoviesRankingComponent {
    movieList: Movie[] = this.activatedRoute.snapshot.data.movies;
    
    constructor(private activatedRoute: ActivatedRoute) {}
}

...

const routes = [{
    path: "movies-ranking",
    component: MoviesRankingComponent,
    resolve: {
        movies: MoviesResolver
    }
}]

@filip.mam

MoviesRankingComponent

reslover fired, get request

data comes back, resolver passes data forward

component constructed with data from resolver

component rendered

lifecycle with resolver

route recognized

Composing resolvers

@filip.mam

@filip.mam

TvShowsResolver

public resolve(): Observable<Movie[]> {
    return this.http.get("api/tv-shows");
}

...

const tvShowsRankingRoutes = [{
    path: "",
    component: moviesRankingComponent,
    resolver: {
        tvShows: TvShowsResolver
    }
}]

...

constructor(route: ActivatedRoute) {
    this.tvShows = route.snapshot.data.tvShows   
}

@filip.mam


const routes = [{
    path: "movies-ranking",
    component: MoviesRankingComponent,
    resolve: {
        movies: MoviesResolver
    }
},
{
    path: "tv-shows-ranking",
    component: TvShowsRankingComponent,
    resolve: {
        movies: TvShowsResolver
    }
}]

@filip.mam

MovieFinderResolver


const routes = [{
    path: "movies-finder",
    component: MoviesFinderComponent,
    resolve: {
        movieFinderData: MovieFinderResolver
    }
}]

@filip.mam

MovieFinderResolver


const routes = [{
    path: "movies-finder",
    component: MoviesFinderComponent,
    resolve: {
        movies: MoviesResolver,
        tvShows: TvShowsResolver
    }
}]

@filip.mam

MovieFinderResolver


const routes = [{
    path: "movies-finder",
    component: MoviesFinderComponent,
    resolve: {
        movies: MoviesResolver,
        tvShows: TvShowsResolver
    }
}]

...

constructor(route: ActivatedRoute) {
    this.movies = route.snapshot.data.movies;  
    this.tvShows = route.snapshot.data.tvShows;   
}

@filip.mam

App with resolvers

@filip.mam

App with notifications

@filip.mam

NotifitactionService

@Injectable()
export class NotificationService() {
   ...

   public showLoading(): void {   
  
      ...   

   }

   public hideLoading(): void {   

      ...   

   }

}

@filip.mam

MovieRankingComponent

@Injectable()
class MoviesResolver {
    
    constructor(http: Http, notifitactionService: NotifitactionService) {}

    public resolve(): Observable<Movie[]>
        notifitactionService.showNotification();
        http.get('api/movies').subscribe(movies => {
            notifitactionService.hideNotification();
            return movies;
        );
    }
}

Notifications with router.events

@filip.mam

@filip.mam

MainComponent

@Component({
    ...
})
class MainComponent {
    
    constructor(router: Router, notificationService: NotificationService) {

        this.router.events.subscribe(event) {

            if (event instanceof ResolveStart) {
                this.showLoading();
            }
        
            if (event (instanceof ResolveEnd) {
                this.hideLoading();
            }
    }
}

@filip.mam

App on error

@filip.mam

App on error with notification

Handling errors

@filip.mam

@filip.mam

NotifitactionService

@Injectable()
export class NotificationService() {
   ...

   public showLoading(): void {   
  
      ...   

   }

   public hideLoading(): void {   

      ...   

   }

   public showError(): void {

      ...


   }

}

@filip.mam

MoviesResolver


@Injectable()
class MoviesResolver implement Resolve<Movie[]> {
    
    constructor(http: Http) {}

    public resolve(): Observable<Movie[]> {
        return this.http.get("api/movies");
    }
}

@filip.mam

MoviesResolver


@Injectable()
class MoviesResolver implement Resolve<Movie[]> {
    
    constructor(http: Http, notificationService: NotificationService) {}

    public resolve(): Observable<Movie[]> {
        return this.http.get("api/movies")
            .catch(error => {
                this.handleError(error);
                notificationService.showErorr();
            });
    }

    private handleError(error): void {...}

}

@filip.mam

AppModule

@NgModule({
    imports: [RouterModule.forRoot(routes, {
        errorHandler: AppErrorHandler
    })]
})

@filip.mam

AppErrorHandler

class errorHandler() implements ErrorHandler {

    
    constructor(private notificationService: NotificationService) {}

    public handleError(error) {
        ...
        this.notificationService.showError();
    }     

}

@filip.mam

Improvements

Prevented loading fails

Created reusable resovlers

Improved UX with notifications

Added error handling

Separated the logic

Thanks!

@filip.mam

Separate your conerns with router resolver - ngPoland

By Filip Mamcarczyk

Separate your conerns with router resolver - ngPoland

  • 923