I have 2 Subscription - one is a subscription of my ActivatedRoute and another from ngrx Store.
ngOnInit() {
this.menuItems$ = this.store.select('menuItems');
this.menuItems$.subscribe(data => {
this.menuItems = data.menuItems;
});
}
ngAfterViewInit() {
this.fragmentSubscription = this.route.fragment.pipe(
filter((fragment: string) => typeof fragment === 'string')
).subscribe((fragment: string) => {
setTimeout(() => {
const element: ElementRef = this.menuItems.find(menuItem => menuItem.link === fragment).element;
if(element !== undefined) {
element.nativeElement.scrollIntoView({ behavior: "smooth", block: "start", inline: "center" });
}
});
});
}
As my ActivatedRoute (fragment) subscription depends on my store subscription data, I want to delay my ActivatedRoute (fragment) subscription till my Store is subscribed for the first time
Is there any rxjs operator for this?
Tnx
Based on your comment...
If the first time when the Store emits has completed, I no longer want to wait for it when the ActivatedRoute subscribes
You are looking for combineLatest, which...
When any observable emits a value, emit the last emitted value from each.
So I suggest the following:
import { Subscription, combineLatest } from 'rxjs';
// ...
mySubscription: Subscription;
ngOnInit() {
this.mySubscription = combineLatest(
this.store.select('menuItems'),
this.route.fragment.pipe(
filter((fragment: string) => typeof fragment === 'string')
)
// DON'T USE ANY, type your data...!
).subscribe((data: [any, any]) => {
this.menuItems = data[0];
const fragment = data[1]
// do your thing...
})
}
ngOnDestroy() {
this.mySubscription.unsubscribe();
}
Don't get your data in init from the store if possible. Get them from a resolver.
You can ofcourse get them from the store in the resolver and then access them in the init function as a value.
Look for more information here https://angular.io/api/router/Resolve
Related
i'm trying to call a service every 10seconds but also want to execute some code only on the first emission, the problem here is that the first entry is duplicated, here is the code :
ionViewWillEnter() {
this.isLoading = true;
const Obs = timer(0, 10000).pipe(
switchMap(() => {
return this.serviceName.someFunc();
})
);
this.timerSub = Obs.subscribe();
this.timerSub = Obs.pipe(first()).subscribe(() => {
this.isLoading = false;
});
}
i also noticed another problem which is that even though i unsubscribe when i leave the page, the service is still being called every 10 seconds, any help is appreciated.
UPDATE
i found a solution, but it's more of a workaround, basically what i did was put a setTimeout on the subscription :
this.timerSub = Obs.pipe(first()).subscribe(() => {
this.isLoading = false;
});
setTimeout(() => {
this.timerSub = Obs.subscribe();
}, 10000);
and apparently the unsubscribe problem was solved as well, although i would appreciate the feedback with some more elegant solutions, thanks in advance.
The answer provided by Nilesh Patel should work fine, but I still wanted to add this answer to share some minor tips and improvements that you may need to use in your app.
Please take a look at this Stackblitz demo.
The first thing to notice is that if you're using the timer operator and you're interested in doing something the first time it emits, you can check the value returned by that operator and see if it's 0:
timer(0, 10000).pipe(
tap(currentTimer => {
if (currentTimer === 0) {
this.someFunctionToRunOnlyOnce();
}
}),
// ...
);
The second thing to keep in mind is that instead of storing every subscription in a variable (to then unsubscribe from all of them) you can create a subject and use the takeUntil operator like this:
private unsubscribe$: Subject<void> = new Subject<void>();
// ...
timer(0, 10000).pipe(
// ... other operators
takeUntil(this.unsubscribe$) // <-- like this
).subscribe();
// ...
ngOnDestroy() {
this.unsubscribe$.next(); // <-- this will clean the streams
this.unsubscribe$.unsubscribe(); // <-- this will clean the unsubscribe$ stream
}
And another very minor thing to keep in mind is that you can "pause" and "resume" the stream whenever you want without "destroying" it. For example, you can pause it when leaving the page and then resume it again when the user is about to enter to the page again by using the filter operator:
private isInPage: boolean = true;
// ...
timer(0, 10000).pipe(
filter(() => this.isInPage),
// other operators ...
);
// ...
ionViewWillEnter() {
this.isInPage = true;
}
ionViewWillLeave() {
this.isInPage = false;
}
So putting all that together it'd be something like this:
import { Component, OnInit } from "#angular/core";
import { NavController } from "#ionic/angular";
import { Observable, of, Subject, timer } from "rxjs";
import { delay, filter, switchMap, takeUntil, tap } from "rxjs/operators";
#Component({
selector: "app-home",
templateUrl: "./home.page.html",
styleUrls: ["./home.page.scss"]
})
export class HomePage implements OnInit {
private isInPage: boolean = true;
private unsubscribe$: Subject<void> = new Subject<void>();
constructor(private navCtrl: NavController) {}
ngOnInit() {
timer(0, 10000)
.pipe(
filter(() => this.isInPage),
tap(currentTimer => {
if (currentTimer === 0) {
this.someFunctionToRunOnlyOnce();
}
}),
switchMap(() => {
return this.someAsynFunction();
}),
takeUntil(this.unsubscribe$)
)
.subscribe();
}
ionViewWillEnter() {
this.isInPage = true;
}
ionViewWillLeave() {
this.isInPage = false;
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.unsubscribe();
}
public openDetailsPage(): void {
this.navCtrl.navigateForward("details");
}
private someAsynFunction(): Observable<number> {
const randomNumber = Math.floor(Math.random() * 10000) + 1;
console.log("==> Running someAsynFunction method");
return of(randomNumber).pipe(delay(1000));
}
private someFunctionToRunOnlyOnce(): void {
console.log("==> Running someAsynFunctionToRunOnlyOnce method");
}
}
better solution:
create variable firstSub at the the top.
ionViewWillEnter() {
this.isLoading = true;
this.timerSub = timer(0, 10000).pipe(
switchMap(() => {
return this.serviceName.someFunc();
})
);
this.firstSub = this.timerSub.pipe(first());
this.firstSub.subscribe(() => {
// only emit once(first)
this.isLoading = false;
});
}
unsubscribing before component view destroyed.
ngOnDestroy(){
this.timerSub.unsubscribe();
this.firstSub.unsubscribe();
}
I need to detect the component change.
First check my code which work.
The problem here is that this is called init and it is unnecessary to call all the time ... you should only call for a change or when its data ( response ) is okay.
ngOnInit() {
this.calendarData();
}
detectChangeUser(){
this.sharedService.selectedUserType$.subscribe(
data => {
if(data === 'changed-view-user-trigger'){
this.calendarData();
this.calendarUsers();
}
},
err => console.log(err)
)
}
I need to detect only when data has a response.
Check also my service.
export class SharedService {
public latestViewSwither: any = null;
selectedUserType$ = new BehaviorSubject<any>(this.latestViewSwither);
training$ = this.selectedUserType$.asObservable();
constructor(
private http: HttpClient
) { }
swithViewChanged(e){
this.latestViewSwither = e;
this.selectedUserType$.next(e);
}
}
only to detect when data has value.
data === 'changed-view-user-trigger' don't worry about this. I send it from another component only a string...this is not important.
Only important thing is any hook which detects change... I am also trying with ngAfterViewChecked but my software crashes after this...
You can use BehaviorSubject for this. The BehaviorSubject has the characteristic that it stores the “current” value. This means that you can always directly get the last emitted value from the BehaviorSubject.
See the example below:
import * as Rx from "rxjs";
const subject = new Rx.BehaviorSubject();
// subscriber 1
subject.subscribe((data) => {
console.log('Subscriber A:', data);
});
subject.next(Math.random());
subject.next(Math.random());
// subscriber 2
subject.subscribe((data) => {
console.log('Subscriber B:', data);
});
subject.next(Math.random());
console.log(subject.value)
// output
// Subscriber A: 0.24957144215097515
// Subscriber A: 0.8751123892486292
// Subscriber B: 0.8751123892486292
// Subscriber A: 0.1901322109907977
// Subscriber B: 0.1901322109907977
// 0.1901322109907977
I would try something like that to solve the problem.
service:
export class SharedService {
public latestViewSwither: any = null;
selectedUserType$ = new BehaviorSubject<any>(this.latestViewSwither);
training$ = this.selectedUserType$.asObservable();
constructor(private http: HttpClient) { }
swithViewChanged(e){
this.latestViewSwither = e;
if (!!e) {
this.selectedUserType$.next(e);
}
}
}
I just want to know if is there some rxjs operator that can delay my observable to start emitting when my variable is set to true.
Something like this on Angular:
loading: boolean;
constructor () {
this.subscriptions.push(
this._messageService.loadMyEvent$.pipe(
// here the operator that emitts data when loading is set to true
).subscribe(response => {
// load data
this.loadData();
this.loading = false;
})
);
}
ngAfterViewInit(): void {
this.loading = true;
}
So with all of this I want to loadData() when loading is set to true, this means all my content is rendered (AfterViewInit). Thank you.
You could use a multicast observable like ReplaySubject instead of a boolean primitive to emit the values. It could then be combined with the source observable using RxJS combineLatest function. It'll emit when any of it's source emits. But note all sources must've emitted at least once for the combineLatest to start emitting. You could also use filter operator to go forward only when loading is true.
Try the following
Controller
loading: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
constructor () {
this.subscriptions.push(
combineLatest(
this._messageService.loadMyEvent$,
this.loading
).pipe(
filter(([response, loading]) => loading) // <-- emit only when `loading` is true
).subscribe({
next: ([response, loading]) => {
// load data
this.loadData();
this.loading = false;
},
error: error => {
// handle error
}
})
);
}
ngAfterViewInit(): void {
this.loading.next(true); // <-- push `true`
}
It really depends on what exactly you want to do but you can merge EMPTY:
import { EMPTY } from 'rxjs';
...
this._messageService.loadMyEvent$.pipe(
switchMap(loading => loading ? someObservable : EMPTY),
).subscribe(response => {
// load data
this.loadData();
this.loading = false;
});
I have subscribed to data which I want to pipe. But somehow it does not work. I get this error :
The property pipe is not available for type "OperatorFunction<unknown,
[unknown, boolean, any]>"
This is my code:
auth.service
authenticationState = new BehaviorSubject(false);
checkToken() {
this.storage.get(TOKEN_KEY).then(access => {
if (access) {
this.user = this.helper.decodeToken(access);
this.authenticationState.next(true);
}
});
page.ts (where I get the error at pipe)
ngOnInit() {
this.subscription = combineLatest (
this.authService.authenticationState,
from(this.storage.get(USER_ID)))
.pipe(
switchMap(
([isAuthenticated, id]) => isAuthenticated
? this.userService.getUserDetails(id)
: of(null)
)
).subscribe(result => {
if (result) {
this.information = result;
console.log(this.information);
} else {
}
},
error => {}
);
}
Error suggest that you are importing combineLatest from the wrong place, you need to import the combineLatest from rxjs instead of rxjs/operators. You need the "observable creation" version of combineLatest, which resides in rxjs.
Also you don't need from inside combineLatest, for the Storage, combineLatest accepts a Promises as well.
Change your import of combineLatest to:
import { combineLatest } from 'rxjs';
I am on Angular 2.3.1 and I am fairly new to both Angular and event based programming. I have two subscriptions, route.params.subscribe and engineService.getEngines(). In my onInit I want to call getEngineName after this.route.params.subscribe and this.engineService.getEngines().subscribe complete.
Reason for this: getEngineName functions depends on the engineId from the queryParams and the engines array which is populated after the completion of getEngines() call.
I did look at flatMap and switchMap but I did not completely understand them.
This is the code in the component:
export class ItemListComponent implements OnInit {
items: Item[];
engines: Engine[];
private engineId: number;
constructor(
private router: Router,
private route: ActivatedRoute,
private itemService: ItemService,
private engineService: EngineService
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
});
this.engineService.getEngines()
.subscribe(engines => this.engines = engines);
// This should only run after engineId and engines array have been populated.
this.getEngineName(this.engineId);
}
getEngineName(engineId: number) {
this.engines.find((engine) => {
return engine.id === engineId;
})
}
}
Why don't you just move the logic inside the route.params callback?
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
//this.engineId is defined here (1)
this.engineService.getEngines()
.subscribe((engines) => {
this.engines = engines;
//this.engines is defined here (2)
this.getEngineName(this.engineId);
});
});
with flatMap and forkJoin:
this.route.params.flatMap((params: Params) => {
this.engineId = +params['engineId'];
return Observable.forkJoin(
this.itemService.getItems({engineId: this.engineId}),
this.engineService.getEngines()
)
}).subscribe((data)=>{
let items = data[0];
let engines = data[1];
this.items = items;
this.engines = engines;
this.getEngineName(this.engineId);
});
switchMap is recommended in this scenario.
this.route.params.pluck('engineId') //pluck will select engineId key from params
.switchMap(engineId => {
this.getItems(engineId);
return this.engineService.getEngines().map(engines => {
/*this.engineService.getEngines() emits the list of engines.
Then use map operator to convert the list of engines to engine we are looking for
*/
return engines.find((engine) => {
return engine.id === engineId;
})
})
}).subscribe(engine => {
//engine
})
getItems(engineId) {
this.itemService.getItems({engineId: engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
}
Suppose the engineId in the params changes, the first observable this.route.params.pluck('engineId') will emit data, which will cause the next observable this.engineService.getEngines() to get fired. Now suppose the route changes before this observable emits the data. Here you need to cancel getEngines observable to prevent error. This is done by switchMap.
switchMap cancels inner observable if outer observable is fired.
PS: I have avoided keeping any states like this.engineId etc.