RESOLVER – dostarczamy dane przed aktywacją routa
W tym artykule przyjrzymy się bliżej funkcji Resolver, która może nam zwrócić Promise<any>, Observable<any> lub po prostu jakieś dane. Zapewne sporo z Was dokładnie wie czemu służy Resolve, kojarząc go z Ui-Routera w Angularze 1.x. Dla niewtajemniczonych, Route resolvers służy dostarczeniu danych do Routa. Przed aktywacją Routa, a jeszcze prościej mówiąc, zmuszamy naszą aplikację, aby pokazał widok dopiero jak dane zostaną dostarczone.
Dla łatwiejszej analizy dalszej treści, w linku live example przykładu, który rozpatrzymy:
W aplikacji powyżej, mamy listę userów, klikając na username danego usera, przechodzimy do komponentu z jego szczegółami, a kontroler ów komponentu woła asynchronicznie o dane usera.
Z racji, że call do backendu jest asynchroniczny, to podczas inicjalizacji komponentu, przekazany obiekt “user” do widoku przyjmuje wartość undefined ponieważ dane usera nie zostały jeszcze pobrane, co prowadzi do tego, że widok nie zostanie poprawnie sparsowany.
Zapewne wielu z Was może to dziwić, gdyż Angular 1.x nie miał problemu z parsowaniem wartości null & undefined w interpolacjach, natomiast Angular 2 domyślnie już na to nie pozwala. W naszym przypadku, próba użycia na widoku (user-detail.component.html):
1 |
{{user.name}} |
konsola zwróci nam błąd:
1 |
TypeError: Cannot read property 'name' of undefined |
Oczywiście możemy się przed tym zabezpieczyć używająć elvisów na widoku:
1 |
{{user?.name}} |
Na dłuższą metę, używanie wszędzie “elvisów” może być męczące, zwłaszcza, że nie wszędzie możemy ich użyć (np w ngModel lub w dyrektywach routerLink).
IMPLEMENTACJA RESOLVERA
Aby skorzystać z funkcjonalności Resolve, napiszemy serwis, który implementuje interfejs Resolve, udostępniający nam metodę resolve(). Metoda resolve() może przyjąć dwa parametry, route: ActivatedRouteSnapshot oraz state: RouterStateSnapshot.
Dzięki naszemu serwisowi, będziemy mogli przygotować dane usera przed aktywacją Routa danego komponentu. Powstałą klasę UserResolve, zawołamy w konfiguracji naszego routingu oraz dodamy do providers głównego modułu aplikacji.
1 2 3 4 5 6 7 8 |
@Injectable() export class UserResolve implements Resolve<User> { constructor(private usersService: UsersService) {} resolve(route: ActivatedRouteSnapshot) { return this.usersService.getUser(route.params['id']); } } |
Czym jest route.params[‘id’] przekazane jako parametr do metody getUser? Jeśli zajrzymy do dokumentacji ActivatedRouteSnapshot, widzimy, że posiada pole “params”. Params odczytuje wartość id, która pojawia się w URLu, w naszym przypadku jest to ID danego usera. Oczywiście params[‘id’] jest dlatego, że nasz path w konfiguracji routingu jest określony jako:
1 |
path: 'users/:id' |
Jeśli nazwiesz swojego patha do danego komponentu przykładowo ‘products/:name’, otrzymasz do niego dostęp poprzez params[‘name’].
Dodajemy nasz serwis do providers w głównym module (module.app.ts):
1 2 3 4 5 |
@NgModule({ ... providers: [ UsersService, UserResolve ], ... }) |
Przekazujemy resolver w routingu (app-routing.module.ts):
1 2 3 4 5 6 7 8 9 |
const routes: Routes = [ ... { path: 'users/:id', component: UserDetailComponent, resolve: { user : UserResolve } }, ... ]; |
Oraz przypisujemy wartość do usera w user-detail.component:
1 2 3 4 5 6 7 |
.... user: User; constructor(private usersService: UsersService, private route: ActivatedRoute) {} ngOnInit(): void { this.user = this.route.snapshot.data['user']; } |
Interfejs ActivatedRoute daje nam dostęp do snapshota o typie ActivatedRouteSnapshot (reprezentacja aktywnego stanu routa w danym momencie), z kolei ActivatedRouteSnapshot posiada dostęp do obiektu data, który reprezentuje statyczne dane lub otrzymane poprzez Resolve. Z racji, że w konfiguracji routingu, nasz Resolver został przypisany do property ‘user’, otrzymujemy dostęp do naszego obiektu poprzez
1 |
this.route.snapshot.data.user; |
Voila! Link do live example po dodaniu resolvera:
Zamiast użycia snapshota, możesz również podłączyć się pod strumień z danymi, poprzez subscribe, tzn.:
1 |
this.route.data.subscribe((data) => this.user = data.user) |
PODSUMOWANIE
Resolver nie musi być klasą, można również do Resolve w konfiguracji routingu przypisać funkcję pobraną z useValue z providers. W przypadku zainteresowania takim przypadkiem, polecam sprawdzić drugi przykład z API Angulara:
https://angular.io/docs/ts/latest/api/router/index/Resolve-interface.html
Pingback: ANGULAR – ROUTER EVENTS I SPINNER
W konfiguracji routingu jest :”path: ‘users/:id’,”, nie powinno być tak user zamiast users?
hej Tomku, users jest poprawne ponieważ pod /users kryje się tablica z userami (liczba mnoga)
Pingback: Rozmowa o pracę na Angular Developera - jakich pytań możesz się spodziewać? - No Fluff Jobs - blog
Tomek, dzięki za wyjaśnienie. Ja tylko dodam, że w przypadku, kiedy nasz Resolve zwraca Observable musimy zasubskrybować się do obiektu data ActivatedRoute, a nie snapshota. Drobna pomyłka w ostatnim listeningu:
this.route.data.subscribe((data) => this.user = data['user'])
oczywiście amsz rację, dzięki za wyłapanie błędu!