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();
}
Related
EDIT: See Kurt Hamilton's answer for the solution.
I'm calling an API to return the values of some settings in settings.service.ts.
In settings.component.ts these need to be returned to fill a form - it displays loading when the API call isn't finished yet.
It's working with the 'return of(fakeData)'. However, I can't figure out how to return the 'realData'.
Instead of console.log(realData) I want to return that instead of the fakeData.
Some help would be nice, thanks in advance!
Beneath are the relevant parts of the code.
settings.service.ts:
export interface Settings {
setting1: boolean;
setting2: string;
}
const fakeData = {
setting1: true,
setting2: 'test'
};
#Injectable()
export class SettingsService {
defaultSettings: DefaultSettings[];
constructor(private apiService: ApiService) { }
loadSettings(): Observable<Settings> {
this.apiService.getDefaultSettings().subscribe( defaultSettings => {
// defaultSettings is needed for when value1 or value2 is 'null'
// not implemented yet, but therefore this nested subscription structure
this.defaultSettings = defaultSettings;
const value1 = this.apiService.getSpecificSetting('setting1');
const value2 = this.apiService.getSpecificSetting('setting2');
forkJoin([value1, value2]).subscribe( result => {
const realData = {
setting1: result[0],
setting2: result[1],
};
console.log(realData);
// return of(settingsFound); not possible here ...
});
});
return of(fakeData);
}
}
settings.component.ts
settings: Observable<Settings>;
ngOnInit() {
this.settings = this.settingsService.loadSettings().pipe(
tap(settings => {
this.settingsForm.patchValue(settings);
})
);
}
Use concatMap or switchMap to run a new observable (in your case a forkJoin) after another observable.
#Injectable()
export class SettingsService {
defaultSettings: DefaultSettings[];
constructor(private apiService: ApiService) { }
loadSettings(): Observable<Settings> {
return this.apiService.getDefaultSettings().pipe(
// save default settings
// this may not be required if you only need default settings for the forkJoin
tap(defaultSettings => this.defaultSettings = defaultSettings),
// now run the next observable
concatMap(defaultSettings => {
return forkJoin({
setting1: this.apiService.getSpecificSetting('setting1'),
setting2: this.apiService.getSpecificSetting('setting2')
});
}),
// now map the result of the forkJoin to the value to want to return
// map won't be required in this case,
// as the arg going into forkJoin matches the desired return structure
// I left it in for completeness
map(result => {
const realData = {
setting1: result.setting1,
setting2: result.setting2,
};
console.log(realData);
return realData;
})
);
}
}
Condensed version
Without my annotations and the redundant calls, the finished result looks like this:
#Injectable()
export class SettingsService {
constructor(private apiService: ApiService) { }
loadSettings(): Observable<Settings> {
return this.apiService.getDefaultSettings().pipe(
concatMap(defaultSettings => forkJoin({
setting1: this.apiService.getSpecificSetting('setting1'),
setting2: this.apiService.getSpecificSetting('setting2')
}))
);
}
}
I follow this guide, and i try to do something similar at Unrelated Components: Sharing Data with a Service paragraph
Data Service:
#Injectable()
export class MyDataService{
private messageSource = new BehaviorSubject(null);
currentMessage = this.messageSource.asObservable();
constructor(private http: HttpClient) {
setInterval(() => { this.changeMessage(this.resultFromRestCall()); }, 10 * 1000);
}
changeMessage(message: object) {
this.messageSource.next(message);
}
resultFromRestCall(){
const json;
this.http.get<object>(myApiUrl).subscribe(res =>
json['data'] = res['data'] //this is an example
);
return json;
}
Component:
export class MyComponent implements OnInit {
constructor(private dataservice: MyDataService) {}
ngOnInit() {
this.dataservice.currentMessage.subscribe(
message => {this.handleVarChange(message); }
);
}
handleVarChange(message) {
console.log(message.data);
}
With this code i got "undefined" in handleVarChange log
Instead of calling this.handleVarChange(message); in subscribe I write console.log(message) i got my result correctly.
So, my question is if it's possible use the value coming from data service in some function of my component.
Thanks in advance
With:
resultFromRestCall(){
const json;
this.http.get<object>(myApiUrl).subscribe(res =>
// takes x amount of time to populate json
json['data'] = res['data'] //this is an example
);
// executed instantly after above request has been called
return json;
}
You are returning json before it has been populated, since the request is asynchronous.
Instead you can flip it around a bit, and call resultFromRestCall() first, and when you get the response, then call changeMessage():
setInterval(() => {
this.resultFromRestCall().subscribe((data) => {
this.changeMessage(data);
});
}, 10 * 1000);
where resultFromRestCall simply returns an observable:
resultFromRestCall(){
return this.http.get<object>(myApiUrl);
}
Also remember to clearInterval in OnDestroy!
DEMO
Omit the .data in handleVarChange:
Instead of
handleVarChange(message) {
console.log(message.data);
}
write
handleVarChange(message) {
console.log(message);
}
How do I use a method starting a listener on an observable which it returns in an if statement?
I'm in an Angular 5 project, I have this sort of setup in one of my components with an timeline where double click opens up a modal and you can type in the name for the item you're creating into that modal.
for the modals I used a reworked version of this answer. (I needed more up to date syntax and imports).
I've got it all nearly working now, here's my setup,
(timeline component which opens the modals) :
#Component({
selector: 'app-planning',
templateUrl: './planning.component.html',
styleUrls: ['./planning.component.css']
})
export class PlanningComponent implements AfterViewInit {
options = {
onAdd: (item, callback) => {
if(this.timeline.getCurrentTime() > item.start){
this.errorTimelineItemModal();
callback(null);
} else {
if (this.createNewTimelineItemModal()) { // <-- currently I have no return but
// having one would be meaningless
// anyways because the if wouldn't wait
// for the observable response and as a
// result, it would always assess false.
callback(item);
} else callback(null);
}
}
}
constructor(
private _element: ElementRef,
private modalService: BsModalService
) {}
ngAfterViewInit(){
this.container = this._element.nativeElement.querySelector('#timeline');
if (!this.items) {
this.items = new vis.DataSet(this.mydataset);
this.timeline = new vis.Timeline(this.container, this.items, this.groups, this.options);
}
}
createNewTimelineItemModal() {
const initialState = {
title: 'Ajouter',
multipleChoice: 'Bid',
choices: ['Bid', 'D.C.', 'Kickoff'],
accceptBtnName: 'Ajouter',
closeBtnName: 'Annuler',
};
this.bsModalRef = this.modalService.show(Modal, {initialState});
this.bsModalRef.content.onClose.subscribe(result => {
this.createItemResult = result;
console.log(JSON.stringify(result));
})
}
updateTimelineItemModal(name) {
const initialState = {
title: 'Nouveau Nom ?',
itemCurrentName: name,
accceptBtnName: 'Rennomer',
closeBtnName: 'Annuler',
};
this.bsModalRef = this.modalService.show(Modal, {initialState});
this.bsModalRef.content.onClose.subscribe(result => {
this.createItemResult = result;
console.log(JSON.stringify(result));
})
}
deleteTimelineItemModal() {
const initialState = {
title: 'Êtes-vous sûr de vouloir supprimer cet element?',
accceptBtnName: 'Supprimer',
closeBtnName: 'Annuler',
};
this.bsModalRef = this.modalService.show(Modal, {initialState});
this.bsModalRef.content.onClose.subscribe(result => {
this.createItemResult = result;
console.log(JSON.stringify(result));
})
}
errorTimelineItemModal() {
const initialState = {
title: 'Erreur',
list: ['Désolé, créer des éléments avant la date d\'aujourd\'hui est désactivé']
};
this.bsModalRef = this.modalService.show(Modal, {initialState});
this.bsModalRef.content.onClose.subscribe(result => {
this.createItemResult = result;
console.log(JSON.stringify(result));
})
}
}
(modal component) :
export class Modal implements OnInit {
onClose: Subject<Object>;
constructor(
private formBuilder: FormBuilder,
public _bsModalRef: BsModalRef) {}
ngOnInit(): void {
this.onClose = new Subject();
}
public onConfirm(): void {
this.onClose.next(true);
this._bsModalRef.hide();
}
public onCancel(): void {
this.onClose.next(false);
this._bsModalRef.hide();
}
}
As you can see I am getting an answer from validating or not the modal. I can console log it.
Now is where I'm stuck. How can I get the code execution to just halt until an observable has been received by that method so as to assess correctly within the if?
this is actually very important for the correct execution of my code because the callback(null); and callback(item); that you might have noticed are the syntaxe one must have to either finalize the item creation or prevent it.
see : http://visjs.org/docs/timeline/#Methods
I had this working with alerts but I'm trying to switch to something with more functionalities and cleaner.
If I can understand you correctly, you need to synchronize two separate events. It is usually a bad practice to do so.
Try to re-organise your code. It is an async process, so you should divide the process into sub-"transactions", that can happen separately.
Separate the logic for opening up your modal.
Wait for the user to enter the data
Process the answer from the modal.
Something like this:
createNewTimelineItemModal() {
const initialState = {
...
this.bsModalRef.content.onClose.subscribe(result => {
this.createItemResult = result;
this.continueToCreateItem(result);
});
}
private continueToCreateItem(result: any){
<insert your code here>
}
Or other solution can be to return observable objects and hande it within the onAdd
options = {
onAdd: (item, callback) => {
...
this.createNewTimelineItemModal().subscribe(result => {
if(result is something){
callback(item);
} else callback(null);
}
}
}
}
To "halt" the process is a pretty bad practice, but can be achived with Promise object.
this.myPromiseReturningMethod(params).then(() => {
but this will block all your application for the time being (with the user being unable to do anything) so I recommend to alter the structure instead.
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.
I have this method:
export class PeriodicData {
public checkForSthPeriodically(): Subscription {
return Observable.interval(10000)
.subscribe(() => {
console.log('I AM CHECKING');
this.getData();
});
};
public getData(): Observable<MyObject> {
let objects: MyObject[] = this.filterData();
return Observable.from(objects);
}
public filterData(): MyObject[] {
let someData;
// someData = filter(...) // logic to filter data
return someData;
}
}
Now I subscribe to getData() in another class:
class Another {
constructor(private periodicData: PeriodicData ) {
this.periodicData.getData().subscribe(obj => {
console.log('IN ANOTHER CLASS');
});
}
}
This is not working. The "IN ANOTHER CLASS" is not being logged. Am I missing something ?
If you tested this only with live TypeScript transpiler, then it doesn't throw an error when you don't specifically include Observable and the from operator (even though I don't know why).
I added to the top of app.component.ts and it works now:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
See your demo on plnkr.co: http://plnkr.co/edit/uVnwG3bo0N8ZkrAgKp7F