Resolver w Angular

Resolver to rozwiązanie Angularowe które pozwoli Tobie na pobranie danych przed wyświetleniem podstrony (route).

W artykule przedstawię:

  • jak stworzyć i użyć resolver
  • jak poradzić sobie z resolverami w relacji rodzic-dziecko

Tworzenie resolvera

Resolver to serwis Angularowy, więc istnieje potrzeba aby znajdował się on w polu providers w danym module w którym chcemy go użyć lub posiadał opcję providedIn: 'root' w dekoratorze Injectable.

Klasa resolvera powinna implementować interfejs Resolve<T> dostarczany przez Angular. Jest to generyczny interfejs gdzie T to typ, który metoda resolve zwraca.

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { User } from '../../interfaces/user.interface';
import { UsersService } from '../../services/users/users.service';

@Injectable({
  providedIn: 'root'
})
export class UsersResolver implements Resolve<User[]> {
  constructor(private usersService: UsersService) {
  }

  public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User[]> | Promise<User[]> | User[] {
    return this.usersService.fetchUsers();
  }
}

Użycie resolvera

Resolver dodajemy na poziomie definicji Routes. Każdy obiekt w tablicy routes może posiadać pole resolve. Jest to obiekt, który może przyjąć dowolnie nazwane klucze (potem te klucze wykorzystamy w komponencie UsersComponent). Klucz ten jako wartość wymaga klasy, która implementuje interfejs Resolve i jest oznaczona dekoratorem Injectable (jeśli nie wykorzystałeś pola providedIn musisz zadbać o to, aby dodać resolver do providers w module, w którym ten resolver jest wykorzystywany).

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { UsersComponent } from './components/users/users.component';
import { UsersResolver } from './resolvers/users/users.resolver';

const routes: Routes = [
  {
    path: '',
    component: DashboardComponent
  },
  {
    path: 'users',
    component: UsersComponent,
    resolve: {
      users: UsersResolver
    }
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Gdy resolver jest już dodany w definicji danego route możesz użyć danych zwracanych przez resolver w komponencie UsersComponent.

import { Component, OnInit } from '@angular/core';
import { User } from '../../interfaces/user.interface';
import { ActivatedRoute, Data } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
})
export class UsersComponent implements OnInit {
  public users$: Observable<User[]>;

  constructor(private route: ActivatedRoute) {
  }

  public ngOnInit(): void {
    this.users$ = this.route.data.pipe(map((data: Data) => data.users));
  }
}

Dostać się do danych zwracanych przez resolver możesz poprzez wykorzystanie ActivatedRoute. Pole data w ActivatedRoute to strumień typu Data który zawiera wszystkie pola zdefiniowane wcześniej w obiekcie resolve w definicji danego route.

Jeśli nie chcesz wykorzystywać w tak prostym przykładzie strumienia możesz również skorzystać z pola snapshot z ActivatedRoute. Ma to swoje ograniczenia: nie możesz wykorzystać operatorów rxjs do przetwarzania danych pochodzących z resolvera lub łączyć danych z innymi strumieniami, które potencjalnie mogą się znajdować w komponencie.

import { Component, OnInit } from '@angular/core';
import { User } from '../../interfaces/user.interface';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
})
export class UsersComponent implements OnInit {
  public users: User[];

  constructor(private route: ActivatedRoute) {
  }

  public ngOnInit(): void {
    this.users = this.route.snapshot.data.users;
  }
}

Relacja rodzic dziecko

Wyobraź sobie sytuację, w której routing jest zdefiniowany tak jak poniżej, czyli posiada w swojej strukturze relacje rodzic-dziecko.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { UsersComponent } from './components/users/users.component';
import { UsersResolver } from './resolvers/users/users.resolver';

const routes: Routes = [
  {
    path: '',
    component: DashboardComponent,
    resolve: {
      users: UsersResolver
    },
    children: [
      {
        path: 'users',
        component: UsersComponent,
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

W jaki sposób możesz wykorzystać dane w polu users w komponencie UsersComponent? Istnieją na to dwie metody.

Wyciągniecie danych z komponentu rodzica

W ActivatedRoute musisz dostać się najpierw do rodzica a następnie do danych które rodzic posiada.

Zalety

  • Celowość – jest to zamierzone pobranie danych z rodzica dzięki czemu unikniemy pomyłek, jeżeli relacji rodzic-dziecko jest więcej w strukturze routingu

Wady

  • Złożoność – przy większej ilości relacji rodzic-dziecko zapis, który pobiera informację z danego komponentu może być długi
import { Component, OnInit } from '@angular/core';
import { User } from '../../interfaces/user.interface';
import { ActivatedRoute, Data } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
})
export class UsersComponent implements OnInit {
  public users$: Observable<User[]>;

  constructor(private route: ActivatedRoute) {
  }

  public ngOnInit(): void {
    this.users$ = this.route.parent.data.pipe(map((data: Data) => data.users));
  }
}

ParamsInheritanceStrategy

Angular pozwala na rozszerzenie danych w aktualnym route danymi rodziców. Aby to wykorzystać potrzebujesz dokonać zmian w definicji routingu. Konfigurację dodaje jako drugi parametr metody RouterModule.forRoot, a w nim { paramsInheritanceStrategy: 'always' }.

Zalety

  • Prostota – zapis pozostaje prosty, nawet przy bardzo złożonych relacjach rodzic-dziecko

Wady

  • Awaryjność – takie zachowanie może prowadzić do kolizji danych w obiekcie resolve. Jeśli rodzic i dziecko w obiekcie resolve posiadają takie same pola, dane będą pochodzić z obiektu dziecka a nie rodzica
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { UsersComponent } from './components/users/users.component';
import { UsersResolver } from './resolvers/users/users.resolver';

const routes: Routes = [
  {
    path: '',
    component: DashboardComponent,
    resolve: {
      users: UsersResolver
    },
    children: [
      {
        path: 'users',
        component: UsersComponent,
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { paramsInheritanceStrategy: 'always' })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Wykorzystanie w komponencie.

import { Component, OnInit } from '@angular/core';
import { User } from '../../interfaces/user.interface';
import { ActivatedRoute, Data } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
})
export class UsersComponent implements OnInit {
  public users$: Observable<User[]>;

  constructor(private route: ActivatedRoute) {
  }

  public ngOnInit(): void {
    this.users$ = this.route.data.pipe(map((data: Data) => data.users));
  }
}

Podsumowanie

Resolver pozwala na pobranie informacji przed wejściem na dany ekran. Wykonuję się on tuż po pozytywnym rozwiązaniu wszystkich guardów w danym route.

Warto zauważyć, że rozwiązywanie danych przed wejściem na ekran nie jest korzystne z punktu widzenia User Experience. Resolvera powinieneś użyć tylko w przypadku danych niezbędnych do działania aplikacji, których stanów rozwiązywania (ładowania) nie jesteś w stanie pokazać na danym ekranie.

Udostępnij
Default image
Wojciech Szućko
Jestem Angular Developerem. Przedstawiam techniki i technologie, które wykorzystuje w codziennej pracy z web aplikacjami. Tworzyłem projekty między innymi dla Bank Pekao S.A., AlphaTauri czy Playmobil.