Route Resolver not firing observable without subscribe - javascript

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.

Related

What makes a component fail to update its template when a Boolean value changes, in the Angular app?

In an Angular 11 app, I have a simle service that mekes a get request and reads a JSON.
The service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Promo } from '../models/promo';
#Injectable({
providedIn: 'root'
})
export class PromoService {
public apiURL: string;
constructor(private http: HttpClient) {
this.apiURL = `https://api.url.com/`;
}
public getPromoData(){
return this.http.get<Promo>(`${this.apiURL}/promo`);
}
}
In the the component, I need to compare the array of products with the array of campaign products (included in the JSON mantioned above) and higlight the promoted products:
export class ProductCardComponent extends DestroyableComponent implements OnInit, OnChanges
{
public promoData: any;
public promoProducts: any;
public isPromoProduct: boolean = false;
public ngOnInit() {
this.getCampaignData();
}
public ngOnChanges(changes: SimpleChanges): void {
this.getCampaignData();
}
public getPromoData() {
this.promoService.getPromoData().pipe(takeUntil(this.destroyed$)).subscribe(data => {
this.promoData = data;
this.promoProducts = this.promoData.products;
let promoProduct = this.promoProducts.find((product:any) => {
return this.product.unique_identifier == product.unique_identifier;
});
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
}
});
}
}
In the component's html file (template), I have:
<span *ngIf="isPromoProduct" class="promo">Promo</span>
There are no compilation errors.
The problem
For a reason I have been unable to understand, the template does not react to the change of the variable isPromoProduct and the template is not updated, despite the fact that I call the function inside ngOnInit and ngOnChanges.
Questions:
Where is my mistake?
What is a reliable way to update the template?
subscribing to Observable inside .ts file it's mostly not a best practice.
try to avoid it by using async pipe of Angular.
you need to store the observable in the variable and not the data returned from the observable, for example:
// this variable holds the `observable` itself.
this.promoData$ = this.promoService.getPromoData()
and then in the template you can do it like this:
<div *ngIf="promoData$ | async as promoData">
here you can access the promoData
</div>
you can still use pipe() to map the data etc but avoid the subscribe()
The isPromoProduct boolean is not an input. The ngOnChanges gets triggered for changes on your properties that are decorated with the #Input decorator. For your particular case, you can inject the ChangeDetectorRef and trigger change detection manually:
constructor(private cdr: ChangeDetectorRef) {}
// ...
public getPromoData() {
this.promoService.getPromoData().subscribe(data => {
// ...
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
this.cdr.detectChanges();
}
});
}
You also don't need to manage httpClient subscriptions. The observables generated by a simple get or post request will complete after they emit the response of the request. You only need to explicitly manage the unsubscribe for hot observables (that you create from subjects that you instantiate yourself).

How do I grab a specific router value?

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');
}

AngularJS Version 6.* Service method is coming up undefined

So I am simply trying to write a component and service that from one view takes the user input and passes it to an api for validation. The problem is that in my component, it's saying that the service essentially has no method login and is coming up undefined. However I've checked and rechecked following Angular.io's documentation very closely but can't get anything to work.
LoginComponent.ts
import { Component, OnInit } from '#angular/core';
import { UserService } from '../../../services/user.service';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
constructor(private userService: UserService) {
console.log('userService', userService);
}
ngOnInit() {}
handleSubmit(data) {
// https://api-test.sarahlawrence.edu:82/
this.userService.login(data)
.subscribe(user => console.log('user', user));
}
}
user.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { Observable, of } from 'rxjs/index';
import { catchError, map, tap } from 'rxjs/internal/operators';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
interface CredsInterface {
username: string;
password: string;
};
interface LoggedIn {
token: string;
}
#Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = '<apiUrl>';
constructor(
private http: HttpClient
) { }
login (creds: CredsInterface): Observable<any> {
console.log('UserService.login()', creds);
return this.http.post<any>(`${this.apiUrl}/signin`, creds, {})
.pipe(
tap((loggedIn: LoggedIn) => {
console.log(`Login: ${loggedIn}`);
}),
catchError(this.handleError('login()', []))
);
}
/**
* Handle Http operation that failed.
* Let the app continue.
* #param operation - name of the operation that failed
* #param result - optional value to return as the observable result
*/
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
console.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
}
I don't understand why I get this error:
So I logged the service out to see the object and weirdly the method is being placed in the prototype:
I don't get it, am I doing something wrong?
How do you call that handleSubmit method?
The error says that it can't read login property of undefined which means that this.userService is undefined. The fact that login method is inside prototype is okay. Remember that gets are deep and sets are shallow
I think that you call handleSubmit with some tricky way which makes this to refer other object than you think it is.
I've just saw stackblitz. You pass reference to your function using [onHandleSubmit]="handleSubmit" but when it's executed this is not your LoginComponent anymore.
Add this to component constructor
this.handleSubmit = this.handleSubmit.bind(this)
For more details see this post: Angular pass callback function to child component as #Input

Why is ngOnInit() being called before canActivate()?

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();

Why is the service called twice in this angular 2 component?

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.

Categories