Not long ago I posted an article about upgrading an app from Ionic v3 to v4. While adding some more features, I found that there are some gotchas that are not entirely intuitive, particularly when updating the UI after dismissing things like alerts. The issue is easy enough to work around, but it took me some time to figure it out. This post describes one solution I discovered.
The Issue
The code shown below is a simple confirmation dialog, as written with Ionic v3. The user is being asked “are you sure you want to delete this?” If the user selects Cancel
, nothing happens. However, if the user selects Delete
, the delete happens in the handler function, which uses async
and await
to wait for the delete to finish. The delete function returns the updated list of games, and the list is bound to the component’s markup with an *ngFor
.
async deleteGame(game: Game) {
let alert = this.alertCtrl.create({
header: 'Confirm delete',
message: 'Are you really sure you want to delete this game?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: () => {
console.log('Cancel clicked');
}
},
{
text: 'Delete',
handler: async () => {
console.log('Delete clicked');
await this.games = this.gameService.delete(game.id);
}
}
]
});
alert.present();
}
In Ionic v3, this function worked properly. Clicking the Delete
button caused the game to be deleted and the UI to refresh as expected.
However, as soon as I dropped it into my Ionic v4 project, I immediately started having problems. First, I got a type error in the create
line. With v4, the AlertController’s create
function returns Promise<HTMLIonAlertElement>
rather than the object directly. Fixing that was as simple as adding an await
to the front of the create
line.
No UI Refresh
Once the compiler error was addressed, it seemed like everything would just work. Unfortunately, it did not. Though it compiled just fine, the UI would not refresh after the game was deleted. At first, I thought there was an error with my delete code, but that was not the case.
In fact, when I clicked a blank space anywhere else in the app, the deleted game would suddenly vanish. That behavior got me on track to finding the root cause.
What I discovered was that the delete was happening and the games array was being updated properly, but somehow outside of the rendering process. I had immediate flashbacks to problems dealing with the Angular 1.x digest cycle.
My first thought was to add a window.setTimeout
to Delete
button’s handler function. Two things stopped me. First, it felt like a kludge; and second, I was not sure it would even work.
The Fix
I looked around online for others having the same problem, but it seemed I was the only one. That is never encouraging. So obviously I was missing some subtlety with the code, and maybe it working in v3 was a happy accident.
The clue to my fix came from the fact that almost all of the AlertController
functions return promises, and not just its create
function. What if I have to await more of them. I added an await
to the present
function, but the behavior did not change.
From there, I reviewed the AlertController
API docs a little more closely, and noticed it has some extra functions I could call. The one that seemed relevant is onDidDismiss
, which returns a promise that resolves (surprise!) after the alert has been dismissed.
The updated code is below. I added three lines and changed two.
async deleteGame(game: Game) {
let updatedGames: Game[]; // NEW
let alert = await this.alertCtrl.create({
header: 'Confirm delete',
message: 'Are you really sure you want to delete this game? This cannot be undone.',
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: () => {
console.log('Cancel clicked');
}
},
{
text: 'Delete',
handler: async () => {
console.log('Delete clicked');
updatedGames = await this.gameService.delete(game.id); // CHANGE
}
}
]
});
await alert.present(); // CHANGE
await alert.onDidDismiss(); // NEW
this.games = updatedGames || this.games; // NEW
}
Instead of immediately updating my component’s games
array, I created a local array to hold the results of the delete
function. From there, I decided to await
the present
function, and then immediately await onDidDismiss()
. After it returns, the code sets the component’s games
array to either the new list, if it exists, or itself. Yes, I could have used a conditional, but I prefer the more concise syntax.
Conclusion
Is this the right solution? Are there better or cleaner solutions? Maybe. This approach seemed simple enough, and it works. Thus, I am sharing with the world in the hope that it might help someone else. If you find a better solution, feel free to let me know.
I ran into an identical problem with the PickerController
. No UI refresh after the picker was dismissed. It, too, has a similar API, and the solution was the same.
References
Do you have any comments, questions, or just want to see more? Please follow me on Twitter and let me know.
Did I make any mistakes in this post? Feel free to suggest an edit.