angulartypescriptpage-refreshmat-dialog

Angular mat-dialog, refresh page upon submit and not close dialog


I'm new to angular (junior here) and in one of my tasks at work, had to fetch data from the database to fill the user grid. I managed to do that and the next step was to use the MatDialog, linked in a create new user button.

I fixed the creation services and linked it with the .NET controller, so now I had to make the component refresh the user data in grid so the new user would appear.

Tried some solutions like this one: How to refresh component after MatDialog close and data updated in database in Angular 6?

But I was unable to get anywhere, so I tried to use the window.location.reload() to achieve page reload and the new records start to appear.

I got a bug though, now when I open up the dialog to fill the fields of the new user information even if I hit the cancel button the page will be reloaded like when the new user submit event call.

Any suggestions on how to make the page refresh only when the submit button event is called (newUser) and the dialog closes and not when the user hit the cancel button?

Here is the open dialog with subscribe and the submit and cancel methods of the dialog:

1)User grid component with subscribe in order to reload the page

onAddUser() {
const dialogRef = this.dialog.open(AddUserRoleDialogComponent, {
  width: '500px',
  data: ''
}).afterClosed().subscribe(result => {
  this.getUsers().subscribe((data) => {
    this.users = data;
    console.log(data, 'data after refresh');
    console.log(this.users, 'users');
    this.dataSource = data;
  })
});
}

2)newUser method in dialog to call the api and create the user

newUser(model: any) {
return this.http.post<any>(url, model).subscribe({
  error: error => {
    this.errorMessage = error.message;
    console.error('There was an error!', error);
  }
});
}

3)Dialog cancel method which still refreshes the page

cancelSave(): void {
this.dialogRef.close();
}

Solution

  • Rolling up the various comments into a single (hopefully) answer for you:

    Per the comments, forcing a window reload on users should be a last last last resort.

    Ideally, in scenarios such as this, you simply update the data client side (you know what you've created/updated/deleted so you can affect that change yourself).

    This is typically the larger piece of work, sadly.

    Next up is to go ahead and just make the API request again. Pretty simple, my personal favourite go-to because it's quick and, depending on what I'm working on, is 'good enough'.

    Only as a last resort would you completely refresh the page. In general there's no need to. Your page(s) should be made up of individual components that you can push new data to, or tell to go re-fetch data. There's very few cases when you need to do a refresh.

    Tackling issue number 1 - the continued force reload even on closing:

    Per comments, this is a case of telling your component whether it should or shouldn't reload when it's closed. This is done with the this.dialogRef.close() method. You can give it something! E.g. a bool to decide if it should reload...

    const dialogRef = this.dialog
        .open(AddUserRoleDialogComponent, 
            {
                width: '500px',
                data: ''
            }
        )
        .afterClosed()
        .subscribe((shouldReload: boolean) => {
            this.dialogRef.unsubscribe();
            if (shouldReload) window.location.reload()
        });
    

    So when closing:

    public onCancelSave(): void {
        this.dialogRef.close(false);
    }
    
    public onSave(): void {
        this.dialogRef.close(true);
    }
    

    For issue #2, the reloading itself:

    Instead of doing a window reload, do your API request again instead of reloading the window...

    if (shouldReload) this.myService.getData(...);
    

    And for the third item - Angular change detection:

    This actually depends on what you're using to display your data. The majority of things are probably fine just adding to the array - under the hood they're some form of loop. It's just a matter of what they check and how often they check it.

    Angular, in general, won't notice property changes (i.e. if an object has a 'name' property and you change the name, Angular won't necessarily react to it).

    What Angular WILL react to, is objects as a whole changing. The newer ES language features provide a nice simplified spread operator to help create 'new' objects that work well for triggering changes, for both objects and arrays:

    this.someObject = {name: 'hello'};
    this.someObject.name = 'world';
    // No object-level change detection, but your template will likely update
    
    this.someObject = {...this.someObject, name: 'world'};
    // This is now a whole new object and everything looking 
    // at it now knows it's something different and will re-evaluate
    

    If, for example, that someObject was an input to a component, the component would have its change detection triggered in the second instance, but not the first.

    Same is true with arrays - this.someArray = [...this.someArray].

    Edit:

    You needed to make those changes because of how you have your new user request hooked up.

    Normally, you would return the observable (this.http.get/post/etc(...)) and then whatever wants to make that request would .subscribe(...) to it. It's the .subscribe() that makes it actually do the thing.

    Instead, what you have is your method doing the post, and immediately subscribing itself to get its own response. It's this subscription that you're returning, not the actual value of the response that you want.

    As a standard, I use this mechanism for all my requests, it keeps things asynchronous without any blocking (using promises and async/await vs observables is a whole thing that I won't get in to).

    A typical service (which I have created a base class to wrap things up and handle all this kind of stuff in a couple of lines) would look like this, expanded out for your viewing pleasure:

    @Injectable()
    export class MyService {
        private readonly someUrl: string = 'some/url';
    
        private obs: Observer<MyType>;
    
        private _someData$: Observable<MyType>;
    
        constructor(private readonly http: HttpClient) {
            this._someData$ = new Obseravable<MyType>((x: Observer<MyType>) => {this.obs = x;}).pipe(share());
        }
    
        public get someData$(): Observable<MyType> { return this._someData$; }
        }
    
        public getMyDataPlease(): void {
            const sub = this.http.get(this.someUrl)
                .subscribe({
                    next: (response: ResponseType) => {
                        sub.unsubscribe();
                        if (this.obs) this.obs.next(response);
                    },
                    error: (err) => {
                        sub.unsubscribe();
                        console.error(err);
                    }
                });
        }
    

    And any component that cares can set up a subscription to it so that whenever new data comes in (anything makes a getMyDataPlease() call), they get the data.

    Note that it's a single-fire request, doing the request, subscribing, and then clearing the subscription (99% of your requests are likely to be such). Obviously, if you wanted to keep an open channel you'd need to arrange it differently, but most of the time you'll want to be sending a single request and getting a single response back.

    @Component({...})
    export class MyComponent implements OnInit, OnDestroy {
        private subs: Subscription[];
    
        constructor(private readonly service: MyService) {
            this.subs = [];
        }
    
        public ngOnInit(): void {
            this.subs.push(this.service.someData$
                .subscribe((x: ResponseType) => this.onSomeDataResponse(x)));
    
            this.service.getMyDataPlease();
        }
    
        public ngOnDestroy(): void {
            this.subs.forEach((x: Subscription) => x.unsubscribe());
        }
    
    
        private onSomeDataResponse(response: ResponseType): void {
            console.log('Get data response: ', response);
        }
    }
    
    

    ngOnInit - set yourself ready and listening to responses, then tell it to go fetch data. ngOnDestroy - don't forget to clean up after yourself.

    A minor note, rather than a treatsie on my lack of async/await usage: async/await blocks.

    Often, there is any amount of setup or whatnot that you want to do on a page, quite possibly in the ngOnInit method, as well as getting data from possibly multiple sources. If you used the standard async/await method for it all, all of that setup would happen sequentially.

    async/await != concurrency.

    async/await aids concurrency by (possibly) freeing up resources to be used whilst the current thread is busy, but the program execution actively stops on that line and doesn't continue until that method/request/whatever has completed.

    With observables, it's a fire and forget, continuing with the next line of code and getting on with life. Once the response has been broadcast, it gets handled appropriately - but the rest of your code has already done its thing without being blocked.

    So long as you're comfortable with an event-based system, and you ensure you always hook it up properly (consistency is key!), you can't go wrong.