Why routing from a form button broke my app

This week, I was making a demo app for our internal company trade show at G-Research. The app mimicked our restricted simulation viewer to demo our team’s work without revealing sensitive information. To make it more interactive, we added a simulation game in which the user, upon completion, could enter their name and have the app route to the high score table.

Page reloading

However, I had a problem. Every time I tried to route from the game to the high score table, the whole app reloaded. This was incredibly frustrating and would be a terrible user experience!

My template had a form with a text box for the name and a button to save the score.

<form #scoreForm>
   <input  type="text" id="name" />   
   <button (click)="saveScore(scoreForm)" > Save Score </button>
</form>

In the component, I saved the score before triggering a router navigation to the high score page.

    saveScore(scoreform) {
        this.scoreService(this.score, scoreform.name.value);
        this.router.navigate(['/highscore']);
    }

Upon clicking the Save Score button, the routing animation would begin, but it would be immediately followed by a full-page reload and end up on the high score page. I thought I must have made a routing error, but after many searches, I realized that the same routing logic worked in other parts of the app, just not when the button was placed inside the form.

Once I added the form into my Google search, I came across a vast number of posts describing my exact issue.

Alt Text

It all boils down to the submit behavior of HTML forms. By default, on submit, the form data is posted to the action endpoint. If you have not set the action attribute, the form data will be posted to the current page and cause a refresh.

Missing FormsModule? Not me…

The most popular Angular answers on StackOverflow relate to a missing FormsModule import. If you import this module, Angular will prevent the default form submit behavior on all your forms, which will stop the page from reloading.

However, even with the FormsModule imported, my page was still reloading. What was going on?!

Routing interference

After some more digging, I discovered that the click handler was called before the submit action; so, if I commented out the router.navigate call, the page would not reload. It also didn’t route, but clearly Angular was preventing the default form action in this case.

It appears that calling router.navigate in a click handler stops Angular from preventing the default submit action. This is why the page reloads partway through the routing navigation. I found that if I wrapped router.navigate in a setTimeout, the form submit method would be called, and Angular could prevent the default action. We can do better though.

The better solution

A better solution is to move the saveScore handler to the submit action on the form. This simple change has two major benefits.

 <form #scoreForm (submit)="saveScore(scoreForm)">
   <input type="text" id="name" />   
   <button> Save Score </button>
</form>       
  1. Angular now prevents the default action, whilst still calling router.navigate, with no setTimeout required.
  2. Pressing the Enter key will now also submit the form and trigger the navigation to the high scores.

My takeaway is to use the form submit event instead of a button click handler. Hopefully, by recording the pitfalls of my initial approach, I will save someone/myself time in the future. If not, I have learned some more about forms by digging into this issue, so it has been worthwhile.