Multiple Subscriptions to same Subject/Observable on multiple method calls - javascript

I have a button, upon clicking which, I subscribe to a "data load". Upon successful data load, I have to display a modal.
Here's my code:
Subject and Observable:
dataSubject = new Subject<boolean>();
isDataLoaded$: Observable<boolean> = this.dataSubject.asObservable();
Fetch Data Method:
(This is a separate method because it is being used in multiple places and not only for populating the modal data.)
fetchData() {
this.dataSubject.next(false);
...
// Fetch Some Data
...
this.dataSubject.next(true);
}
Button Click method:
buttonClick() {
// fetch data
this.fetchData();
// once the data is loaded, display the modal
this.dataSubscription = this.isDataLoaded$.subscribe((isDone) => {
if (isDone) {
this.displayModal();
}
});
}
When i click the button the first time it works fine and displays a modal. When I click second or more times, I get multiple instances of the modal pop up. From what I understand, I am subscribing multiple times and hence the multiple modals.
I have also tried unsubscribing before subscribing, to make sure i have a single subscription, but even then I get atleast 2 modals that pop up.
Is the a clean way to implement my requirement? TIA

You don't need seperate isDataLoaded$ I think. Just subscribe to dataSubject but not inside buttonClick. You trigger the serivce with call to fetchData no need subscription to be inside function.
this.dataSubscription = this.dataSubject.subscribe((isDone) => {
if (isDone) {
this.displayModal();
}
});
buttonClick() {
// fetch data
this.fetchData();
}

Related

Angular, RxJS - How to cancel previous requests using switchMap?

I was trying to figure out that for a long time by myself but I stuck.
My case: I have a table with data, when user enters the page the request goes to REST API. Above the table there is a form that specify/filters results of the table. When user clicks a button, new request with query parameters goes to a service.
I'm trying to find a way to stop a previous request when user clicked a button, to search a table with specific parameters
Here is what I have done so far stackblitz. Maybe my approach is wrong and someone could show me how to write it better, or maybe there are just few thing to correct.
Thank you for any help
You can use the takeUntil operator inside your pipe, and pass a subject to it, and everytime you apply a new filter you can emit something to that subject to unsubscribe from the previous subscription and subscribe again! let me give you an example on how to use this operator:
regardless if your code was working or not!! I added just the part on how to use the takeUntil operator!
...
unsubscribe = new Subject<boolean>();
constructor(private _service: ServiceConnectService) {}
ngOnInit() {
this.clickStream
.pipe(
map(() => console.log(1)),
switchMap(() => {
return this.getMethodServicesInstances();
}),
takeUntil(this.unsubscribe) // this will be executed once you call next with a value!! and the subscription will be stopped!
)
.subscribe(results => {
console.log(2);
});
}
applyFilter(){
this.unsubscribe.next(true);
/*
you can then rerun the subscription and reset the unsubscribe subject state to null!
*/
}
...

prevent user to click button until service response angular 7 rxjs

I am looking for efficient solution to avoid user to click the submit button again and again until they get response from backend API.
I have thought following solution but looking for any other ways to handle this.
use loader which will disable whole html until we get some response.
use exhaustmap to avoid multiple http calls
use throttletime which will manually not allow user to click for specific time period.
Please guide me if there is any other solutions which we can use.
I would recommend to use exhaustMap() as primary solution in this case.
In template:
<button (click)="submitForm()">Submit</button>
then in your component:
submit$: Subject<boolean> = new Subject();
constructor() {
this.submit$.pipe(
exhaustMap(() => this.http.get("api/endpoint"))
).subscribe(res => {});
}
submitForm() {
this.submit$.next(true);
}
You click 'Submit' and all proceeding click events will be ignored by exhaustMap() operator until HttpClient instance returns response and get completed.
You can disable the button itself till you get a response by having a flag in your component's controller file.
// in component's controller
loading = false;
.
...
submit(): void {
loading = true;
this.someserive.getData().pipe(finalize(() => this.loading = false).subcribe(...
.
....
}
// in component's template
<button [disabled]="loading" (click)="submit()">Submit</button>

Angular 7 call ngOnInit method or any other method of component that is contained by component

I have this situation, in which I have a component called user-table-list that is basically a table with entries that represent some users.
The component was designed so that there is another component, called table-list that describes a table and its behavior for several components (and user-table-list is one of them). The html for user-table-list is in fact just like this:
<app-table-list #table [tableConfiguration]="usersTableConfiguration"></app-table-list>
while the html for table-list has all the design for the table, describing columns, header, etc..., while the single component describes in .ts file the configuration for its personal table, meaning which data they load to fill the table, names for the columns, and so on.
In the header there's a button to add a new entry to the table, let it be of users or clients or anything else. The event and the database retrieval are handled inside table-list.component.ts, that performs a different onclick action according to which type of table the event came from. Something like that:
createNewButtonClicked(tableType: string) {
const dialogConfig = new MatDialogConfig();
dialogConfig.width = '60%';
dialogConfig.autoFocus = true;
switch (tableType) {
case 'clients': {
this.dialog.open(ClientDialogComponent, dialogConfig);
break;
}
case 'projects': {
this.dialog.open(ProjectDialogComponent, dialogConfig);
break;
}
case 'users':{
this.dialog.open(UserDialogComponent, dialogConfig);
break;
}
}
}
Inside the dialogs that are opened, a new client/project/user is added to the list. What I want is to refresh the list after closing the dialog, by calling a method of database retrieval. But this retrieval is called inside the single component, that is for example user-table-list. So I'm looking for a way to call the method of retrieval and refresh the list of user-table-list from the outside, without having to refresh the page from the browser. The problem is that I can't achieve that, not in the dialog component nor the table-list component. I've tried importing the component in the constructor and calling the method, both in dialog and table-list component, but it gives a warning for a circular reference and does nothing. Same if I create an external service to call the method, because there is always a circular reference.
I'm out of alternatives; how can this be done?
Unfortunately I can't alter the application design.
Assuming your are use a material dialog the dialogs give you an observable you can watch for to see when the dialog is closed. if you set it to a variable you can watch for when it is closed, for instance:
createNewButtonClicked(tableType: string) {
// your existing logic
// ...
const dialog = this.dialog.open(ClientDialogComponent, dialogConfig);
// example dialog .afterClosed
dialog.afterClosed().subscribe(closeResult => {
// update from database after dialog is closed
});
}
You could use an Output property to tell parent components that something happened inside of the child. Inside of the subscribe function (where I put a comment to update database) is where you would want to emit that the dialog was closed. Your code for list could look something like this:
#Output() dialogClosed = new EventEmitter();
createNewButtonClicked(tableType: string) {
const dialogConfig = new MatDialogConfig();
dialogConfig.width = '60%';
dialogConfig.autoFocus = true;
let dialog;
switch (tableType) {
case 'clients': {
dialog = this.dialog.open(ClientDialogComponent, dialogConfig);
break;
}
case 'projects': {
dialog = this.dialog.open(ProjectDialogComponent, dialogConfig);
break;
}
case 'users':{
dialog = this.dialog.open(UserDialogComponent, dialogConfig);
break;
}
}
// check to see if dialog was opened
if (!!dialog) {
dialog.afterClosed().subscribe(closeResult => {
// emit that dialog was closed
this.dialogClosed.next();
});
}
}
Then in your user-table component watch for the event to be emitted:
<app-table-list (dialogClosed)="onDialogClosed($event)"></app-table-list>
And finally your user tables ts. Note that I am not calling ngOnInit when the dialog is closed as the purprose of ngOnInit is to initialize. So extract the database logic inside of ngOnInit into another method and call this method to update data when the dialog is closed as well:
ngOnInit() {
this.getData();
}
onDialogClose() {
this.getData();
}
private getData() {
// make database calls here and update data from response
}
You should only use ngOnInit to initialize the view as you may be setting some defaults in here that you do not want to reset, so it is recommended to not call ngOnInit again later in the pages life cycle
You must issue an event from the submit button that is inside the Dialog. That is, capture the click event of the Dialog and from table-list notify the parent components that clicked.
Something like that:
table-list.component.ts
#Output() clickedOkDialog = new EventEmitter<boolean>();
onClickOkDialog() {
this.clickedOkDialog.emit(true);
}
Then you can capture the event in user-table-list
In your user-table-list.component.html
<app-table-list (clickedOkDialog)="onClickOkDialog($event)" #table [tableConfiguration]="usersTableConfiguration"></app-table-list>
In your user-table-list.component.ts
onClickOkDialog(event) {
// update from database after dialog is closed
}

Page not refreshed when clicked delete button in angular

I have a page running at "http://localhost:4200/assignmentForAudit" and the UI looks like
When i click delete button then the data gets deleted but the page is not refreshed. I tried to put this.router.navigate(['/assignmentForAudit']); after delete operation but it is not refreshing the page.How can i achieve this refresh method so that the data gets removed?
method to delete in component.ts
onDelete(nas:any){
this.assignmentAudit.id=nas.assignmentAudit[0].id;
console.log(this.assignmentAudit.id);
if(window.confirm('Are sure you want to delete this item ?')){
this.assignmentAuditService.deleteGroupFromSelection(this.assignmentAudit.id)
.subscribe(
data => {
console.log(data);
},
error => console.log(error));
}
this.router.navigate(['/assignmentForAudit']);
}
assignment-audit.service.ts class method to call the api operation
deleteGroupFromSelection(id: number): Observable<any> {
return this.http.delete(`${this.baseUrl}/${id}`, { responseType: 'text' });
}
Delete operation is working fine but the problem is the page is not refreshing.
that's not recommended at all as a best practice when you using angular you can simple emit data list after every delete with that you update the data visually too but if you want such behavior.
First Case by reload the whole page you can do so after every delete
window.location.reload();
Second Case if you need just to reload the component you can work around that and achieve it by a hack (just to trick the component you navigate away and navigate again to it)
this.router.navigateByUrl('/DummyComponent', {skipLocationChange: true}).then(() => this.router.navigate(['Your actualComponent you want to reload']));
/DummyComponent could be a empty component you just gonna use it to trick the actual component you need to refresh/reload
Instead of trying to reload the page, call the http method which is used to populate the entries again on success of the delete http call.
this.assignmentAuditService.deleteGroupFromSelection(this.assignmentAudit.id)
.subscribe(
data => {
this.entries = this.http.get() // something like this. here this.entries refers to data that is used to populate html.
console.log(data);
},
error => console.log(error));
}
You can also use a BehaviorSubject to emit a value, listening to it, you can decide on calling this.entries = this.http.get() again.
You may need to use the 'onSameUrlNavigation' option in Angular routes.
RouterModule.forRoot(routes, {onSameUrlNavigation: ‘reload’})
Can your show how to binding array of object to template ? I thinks you just remove item from array then template it will update data too.
something like this:
this.nas.assignmentAudit = this.nas.assignmentAudit.filter(a => a.id !== this.assignmentAudit.id);

Wait for other interaction to happen before resolving RSVP

I have a component that on a swipe will send an action upward to the parent route/controller to handle some ajax functionality. This component has some UI that gets set to a loading state to show the user things are happening. When working gets set back to false the loading animation in UI stops and user interaction can happen again.
callAjaxAction() {
this.setProperties({working:true});
Ember.RSVP.cast(this.attrs.action()).finally(() => {
this.$('.draggable').animate({
left: 0
});
this.setProperties({working:false});
});
}
In this case the controller catches the action specified on the component definition and calls an ajax function to get some data to display in the page
// in the controller action
return Ember.RSVP.Promise((resolve,reject) => {
Ember.$.ajax({
type: 'get',
dataType: 'json',
url: `http://***/api/paysources/user/697?key=${ENV.APP.api_key}`
}).then((response)=>{
this.setProperties({
'cards':response.user_paysources,
'showCards': true
});
},(reason)=>{
reject();
this.get('devlog').whisper(reason);
})
})
This will show a new popup type of component that will allow the user to pick a card to pay with. If the user clicks away, or if they click a card and the ajax action completes I need to reset the UI not only on this page (change it so it says the cart has been paid for) but also send the swipe component (the one that now has a loading animation) something that tells it it's done loading.
Basically, the way I see it in my head, is there a way to fire an action on a component from the parent controller/route?
To answer your question "is there a way to fire an action on a component from the parent controller/route?": No there is not. However there are two idiomatic ways I can think of for doing this in Ember:
You could potentially get around it by moving the Ajax request into the component.
Follow the 'data down, actions up' pattern. See the following as an example of how to implement it.
You could fire an action on the card component that changes a property on your controller. Then listen to that property into the original component so it can update.
original-component.js
reset: computed('transactionComplete', function() {
// cleanup stuff here...
})
original-component-template.hbs
{{#if transactionComplete}}
{{! stuff to show when transaction is complete... }}
{{/if}}
controller.js
transactionComplete: false,
actions: {
completeTransaction() {
this.toggleProperty('transactionComplete');
}
}
controller-template.hbs
{{original-component
transactionComplete=transactionComplete
}}
{{cart-component
transactionComplete=(action 'completeTransaction')
}}
cart-component.js
actions: {
processTransaction() {
// cleanup stuff here...
this.attrs.transactionComplete();
}
}
There might be different – and better – ways of doing this, but it depends on exactly what you need to do when resetting the original component.
Also, separately, have you considered using ember data and routing for loading the cards?

Categories

Resources