I'm using route guards, specifically the canActivate() method, but Angular is calling ngOnInit() of my root AppComponent before canActivate is called.
I have to wait on some data in canActivate before the AppComponent can render it in the template.
How can I do this?
I was dealing with such cases, and here is what I usually do:
1. I create a Resolver service (which implements Resolve interface). It allows you to get all necessary data before activating the route:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '#angular/router';
import { DataService } from 'path/to/data.service';
#Injectable()
export class ExampleResolverService implements Resolve<any> {
constructor(private _dataService: DataService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<any> {
return this._dataService.anyAsyncCall()
.then(response => {
/* Let's imagine, that this method returns response with field "result", which can be equal to "true" or "false" */
/* "setResult" just stores passed argument to "DataService" class property */
this._dataService.setResult(response.result);
})
.catch(err => this._dataService.setResult(false););
}
}
2. Here is how we can deal with AuthGuard, which implements CanActivate interface:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '#angular/router';
import { DataService } from 'path/to/data.service';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private _dataService: DataService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
/* "getResult" method operates with the same class property as setResult, it just returns the value of it */
return this._dataService.getResult(); // will return "true" or "false"
}
}
3. Then you can include the Resolver and the AuthGuard to your routes config, here is just a part (the structure of routes can differ, here is an example with activating the parent component):
const routes: Routes = [
{
path: 'app',
component: AppComponent,
resolve: {
result: ExampleResolverService // your resolver
},
canActivate: [AuthGuard], // your AuthGuard with "canActivate" method
children: [...] // child routes goes inside the array
}
];
How it works
When you're navigating to /app, the ExampleResolverService starts, makes API call and stores the necessary part of response to class property in DataService via setResult method (it's the usual setter). Then, when the resolver finished the work, it's time for our AuthGuard. It gets stored result from DataService via getResult method (it's the usual getter), and returns this boolean result (our AuthGuard expects boolean to be returned, and the route will be activated if it returns true, and will not be activated if it returns false);
It's the simplest example without any additional operations with data, the logic is more complex usually, but this skeleton should be enough for basic understanding.
For me, I listened for ROUTE_NAVIGATED events in my app component like below
I am using ngrx/router-store to be able to listen to these router actions.
// app.component.ts
public ngOnInit(): void {
// grab the action stream
this.actions$.pipe(
// Only pay attention to completed router
ofType(ROUTER_NAVIGATED),
// Now I can guarantee that my resolve has completed, as the router has finsihed
// Switch
switchMap(() => {
// Now switch to get the stuff I was waiting for
return this.someService.getStuff();
})
// Need to subscribe to actions as we are in the component, not in an effect
// I suppose we should unsubscribe, but the app component will never destroy as far as I am aware so will always be listening
).subscribe();
Related
I have a component which needs to show the data in the grid on the component/page Load and when a button is clicked from parent component it needs refresh the grid with new data. My component is like below
export class TjlShipdateFilterComponent implements DoCheck {
tljShipDate: ShipDateFilterModel[];
constructor(private psService: ProjectShipmentService) {
}
ngDoCheck() {
// this data is from the service, trying to get it on Page load
}
#Input() filter: ShipDateFilterModel[];
//Load or refresh the data from parent when the button clicked from parent component
ngOnChanges(changes: SimpleChanges) {
}
The ngOnChanges works fine, it gets the data from the parent component and displays when the button is clicked from the parent component. But on load of the page/component the grid it doesn't show anything and says this.psService.tDate; is undefined.
Below is the service where I get the tDate
export class ProjectShipmentService {
......
constructor(service: DataService, private activatedRoute: ActivatedRoute) {
service.get<ShipDateFilterModel[]>(this.entityUrl).subscribe(x => this.tDate = x);
}
I am unsure what am I missing here. How can I achieve this scenario
It happened because when the component is loaded, the request in your service may not completed and the data may not return yet, that why tDate is undefined, try subscribe to it inside your component, also use ngOnInit() instead of ngDoCheck().
In your service:
tDate: Observable<ShipDateFilterModel[]>
constructor(service: DataService, private activatedRoute: ActivatedRoute) {
...
this.tDate = service.get<ShipDateFilterModel[]>(this.entityUrl)
}
In your component:
export class TjlShipdateFilterComponent implements OnInit, OnChanges {
tljShipDate: ShipDateFilterModel[];
constructor(private psService: ProjectShipmentService) {
}
ngOnInit() {
// this data is from the service, trying to get it on Page load
this.psService.tDate.subsribe(x => this.tljShipDate = x);
}
#Input() filter: ShipDateFilterModel[];
//Load or refresh the data from parent when the button clicked from parent component
ngOnChanges(changes: SimpleChanges) {
if (changes.filter && changes.filter.currentValue)
{
this.tljShipDate = this.filter;
}
}
}
You have a couple options here.
NgOnInit will run when the component is created, before it is rendered. This is the most common way to load data on component initialization.
If you need the data even before the component is initialized, then you may need to utilize a Resolver.
Here's an example:
import { Injectable } from '#angular/core'
import { HttpService } from 'services/http.service'
import { Resolve } from '#angular/router'
import { ActivatedRouteSnapshot } from '#angular/router'
#Injectable()
export class DataResolver implements Resolve<any> {
constructor(private http: HttpService) { }
resolve(route: ActivatedRouteSnapshot) {
return this.http.getData(route.params.id);
}
}
Then, in your route config:
{
path: 'data/:id',
component: DataComponent,
resolve: { data: DataResolver }
}
The inclusion of the ActivatedRouteSnapshot is optional, you only need it if you're using route data, like params.
Edit:
Looking at your example closer, is it possible that the ngDoCheck is firing before the psService subscription does?
So when a post is clicked I do this which sends me to another page with the postId in the router:
this.router.navigate(['/annotation', postId]);
This navigates me to the annotations page where only that single post will be shown. In order for this to work, I need to get the postId which is now in the router link:
http://localhost:4200/annotation/5b3f83b86633e59b673b4a4f
How can I get that id: 5b3f83b86633e59b673b4a4f from the router and put it into my TS file. I want this id to only load posts with this ID.
Can anyone point me in the right direction to be able to grab the link http://localhost:4200/annotation/5b3f83b86633e59b673b4a4f take of everything and only get the ID at the end and store that in my TS file.
Sorry, I'm new to angular/web dev hence why I'm asking, many thanks in advance for your time.
You can read params of activated route via params observable, subscribe on it and you will get access to route params:
import { Component, OnInit, OnDestroy } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'selector',
template: ``,
})
export class LoanDetailsPage implements OnInit, OnDestroy {
private paramsSubscription$: Subscription;
constructor(private _route: ActivatedRoute) {}
ngOnInit() {
this.paramsSubscription$ = this._route.params.subscribe(params => {
console.log(params); // Full params object
console.log(params.get('paramName')); // The value of "paramName" parameter
});
}
ngOnDestroy() {
this.paramsSubscription$.unsubscribe();
}
}
PS: Don't forget to unsubscribe() in OnDestroy lifecycle hook.
You have to inject the ActivatedRoute service and subscribe to the paramMap:
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// subscribe to the parameters observable
this.route.paramMap.subscribe(params => {
this.foo = params.get('paramName');
});
}
Try in the component which you load something like:
id: string;
ngOnInit() {
this.id = this.route.snapshot.paramMap.get('postId');
}
I have a route which needs some data from my Firebase db before the route is loaded. It feels like the Route is not calling subscribe so the request is never being fired off. Am I missing a step?
(Angular 5)
My router:
{
path: 'class/:idName',
component: ClassComponent,
resolve: {
classData: ClassResolver
}
},
My Resolver:
#Injectable()
export class ClassResolver implements Resolve<any> {
constructor(
private db: AngularFireDatabase
) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> | Promise<any> | any {
// return 'some data'; //This worked fine
return this.db
.list('/')
.valueChanges() // Returns Observable, I confirmed this.
//.subscribe(); // This returns a Subscriber object if I call it and I never get any data
}
// I tried this and it didnt work either
//const list = this.db
// .list('/')
// .valueChanges();
//console.log('list', list); // Is a Observable
//list.subscribe(data => {
// console.log('data', data); // returned data
// return data;
//});
//return list; // never gets to the component
}
My Component:
public idName: string;
// Other vars
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private db: AngularFireDatabase
) {
// Form stuff
}
ngOnInit() {
// Never makes it here
this.idName = this.route.snapshot.params.idName;
const myclass = this.route.snapshot.data.classData;
console.log('myclass', myclass);
}
I never makes it to the component. It waits for the component to load, which it never does. If I add the subscribe and console.out the data it returns quite quickly with the correct data, so its not the service.
After calling .subscribe() in my Resolver that now returns a Subscriber object. Because my return signature allows for any its returning this Subscriber as if it was the data. This seems obvious now.
My question now becomes why isn't it resolving my Observable?
Your resolve function is returning an Observable that never completes. The Observable is indeed firing (and this can be verified by adding a tap to its pipeline with some console-logging)—but the resolve phase won't end (and therefore your component won't load) until the Observable completes. (The docs are not great at highlighting this.)
Obviously you don't want your Observable to complete either, because then you wouldn't get further data updates.
The simplest “fix” is to wrap your Observable in a Promise:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
return this.db.list('/').valueChanges();
}
but this won't guarantee that Firebase has emitted its initial response, which I feel is what you're trying to ensure before the route loads.
The only approach I can see that would:
ensure that the component doesn't load until Firebase has returned data at least once; and
prevent two different Firebase reads (one by the resolver and then one by the component) for one effective operation
is to wrap your Firebase Observable in a service:
import { Injectable, OnDestroy, OnInit } from '#angular/core';
import { AngularFireDatabase } from '#angular/fire/database';
import { Subscription } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
#Injectable({
providedIn: 'root',
})
export class DataService implements OnInit, OnDestroy {
constructor(private readonly db: AngularFireDatabase) {}
/**
* Observable to the data.
* shareReplay so that multiple listeners don't trigger multiple reads.
*/
public readonly data$ = this.db
.list('/')
.valueChanges()
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
/**
* To trigger the first read as soon as the service is initialised,
* and to keep the subscription active for the life of the service
* (so that as components come and go, multiple reads aren't triggered).
*/
private subscription?: Subscription;
ngOnInit(): void {
this.subscription = this.data$.subscribe();
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
and then your resolver would look like this:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
// ensure at least one emission has occurred
await this.dataService.data$.pipe(take(1)).toPromise();
// ...then permit the route to load
return this.dataService.data$;
}
By wrapping your Firebase Observable in a service, you get OnInit and OnDestroy lifecycle hooks, which you can use to ensure that the observable "lives on" between component loads (and prevent multiple Firebase reads where one would suffice). Because the data is then hanging around, subsequent loads of the data would also be quicker. Lastly, this still enables you to use a resolver to ensure that the data will be instantly available before proceeding to load the component.
Your code looks to be correct. Have you been passing a parameter to your class route? It wont resolve without a parameter, that might be why you are not reaching your ngOnInit function. I would suggest console logging your route snapshots as well to make sure you are grabbing the right objects. I'll also post a resolve example that I got working:
Component.ts
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
public data: Observable<any>;
constructor(private router: ActivatedRoute) { }
ngOnInit() {
this.data = this.router.snapshot.data.test;
}
}
Routing.ts
{ path: 'home/:id', component: HomeComponent, resolve: { test: ResolverService } },
ResolverService
import { Injectable } from '#angular/core';
import { Resolve } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
#Injectable()
export class ResolverService implements Resolve<Observable<any>> {
constructor() { }
public resolve(route: ActivateRouteSnapShot): Observable<any> {
return Observable.of({test: 'Test Observable'});
}
}
HTML
{{this.data.test}}
You just need to add a take(1) operator to the Observable the resolver returns so that it completes.
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.db.list('/').valueChanges()
.pipe(take(1)); // <-- The Magic
}
#AlexPeters was on the right track, but you don't have to go so far as to return a promise. Just force the completion with take(1). Alex is also spot-on that the docs are not very clear on this. I just spent an couple hours debugging this same issue.
I have here the component code, when I am subscribing to the observable the service is called twice, however if I subscribe to the Behaviorsubject it is only triggered once,
I can see on my logs that those are the result, please see my code below for my component
the method subscribeToMap() method is called on ngOninit.
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
// Observable class extensions
import 'rxjs/add/observable/of';
// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { HeroSearchService } from './hero-search-service';
import { Hero } from './../hero';
#Component({
selector: 'hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ],
providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(
private heroSearchService: HeroSearchService,
private router: Router) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
console.log("new " + term);
}
ngOnInit(): void {
this.heroes = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => {
return term // switch to new observable each time the term changes
// return the http search observable
? this.heroSearchService.search(term)
// or the observable of empty heroes if there was no search term
: Observable.of<Hero[]>([])})
.catch(error => {
// TODO: add real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
this.subscribeToMap();
}
subscribeToMap(): void{
this.heroes.subscribe(() => console.log("called twice"));
this.searchTerms.subscribe(() => console.log("called once"));
}
gotoDetail(hero: Hero): void {
let link = ['/detail', hero.id];
this.router.navigate(link);
}
}
Here is the code for my service
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Hero } from './../hero';
#Injectable()
export class HeroSearchService {
constructor(private http: Http) {}
search(term: string): Observable<Hero[]> {
console.log("service is called");
return this.http
.get(`api/heroes/?name=${term}`)
.map(response => response.json().data as Hero[]);
}
}
thank you ver much!!!
When subscription is implemented properly it has nothing to do with "unsubscribe" method, Observable, etc. This behavior is by design of Angular itself.
https://www.reddit.com/r/Angular2/comments/59532r/function_being_called_multiple_times/d95vjlz/
If you're running in development mode, it will run the function
at least twice. since in development mode it does a check, changes,
then rechecks to verify, where production mode only does the first
check, assuming you've done your quality assurance and resolved any
values the get changed post checking.
P.S. This is probably the next issue you will face to in Dev Mode :)
Angular2 change detection "Expression has changed after it was checked"
Try replacing this line:
this.heroes = this.searchTerms
With this one:
this.heroes = this.searchTerms.asObservable()
to ensure that heroes is an observable and your code can't accidentally invoke next() on it.
Your code casts hero to a Subject so you can still do next() on it.
So, I am loosing my mind over this
I have a page with many components... but for some reason I am having problems with one...
it is for mains search in the header of the page... for debugging purposes I stripped it down to bare minimum, and still doesn't work
This is my search component
import { Component, OnInit } from '#angular/core';
import { ROUTER_DIRECTIVES } from '#angular/router';
import { Router, ActivatedRoute } from '#angular/router';
#Component({
selector: 'main-search',
template: `<div></div>`,
})
export class MainSearch implements OnInit {
private sub: any;
constructor(private route: ActivatedRoute){
}
ngOnInit(){
this.sub = this.route.params.subscribe(params => {
console.log('PARAMS FROM MAIN SEARCH', params);
});
}
}
as you can see, I am trying to log the params from the URL (f.e. http://localhost:8080/indices;search=test)
NOT populating
I have a similar component with exact behaviour (subscribing to params onInit...
this.sub = this.route.params.subscribe(params => {
console.log('PARAMS FROM INDICES: ', params);
})
And that one actually logs the bloody params!
From console:
PARAMS FROM MAIN SEARCH Object {} => main-search.ts?8502:24
Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode. => lang.js?c27c:360
null => index.service.ts?0bf5:40
FROM API => index.service.ts?0bf5:49
PARAMS FROM INDICES: Object {search: "test"} => indicesList.component.ts?5ff1:63
The weird thing is that only the mainsearch gets logged to the console before Angular2 disclaimer
What could be the issue that main-search doesn't get the params?
I think you need to use the ActivatedRoute.
This should work in your case:
constuctor(
private _activatedRoute: ActivatedRoute,
) {}
ngOnInit()
this._activatedRoute.params.subscribe(params => console.log(params));
}
The thing is your 'main-search' is a few components deep and the router params observable emits params from the root url. Whereas the ActivatedRoute emits params from the current route.