I recently added some advanced filtering to one of the screens in our AngularJS application. . Users could select checkboxes or values from a dropdown and the search results will automatically update. Everything worked great in development, but as soon as we pushed to our stating environment, our QA tests reported getting the incorrect results. A quick check of the network pane in Chrome showed the problem, our requests were coming back out-of-order.
Asynchronous communication means that results can return out of order. We made it quick for users to change the filter, which also meant we made it likely that the earlier requests to
team_index would come back after the later ones. The user interface was showing the current filters, but the results were from the filter that the user had selected before, creating a lot of confusion.
ng-resource but this pattern can be applied universally.
The old code looked like this:
This code is pretty straight forward. The state of the filter is stored in
$ctrl.filter_cmd, whose properties are mapped to interface elements in an Angular component.
$ctrl.is_loading gets flipped to true when there is a request, so that we know to show the user a loading indicator. When the response comes back, we update
$ctrl.result_list and turn the loading indicator off.
What happens in the situation above is that two requests are ongoing at once. The last request (the one the users wants), returns first, the loading indicator is turned of and for a few milliseconds, the correct information is shown. But then the first request returns and
$ctrl.result_list is overridden with the wrong information.
One way to solve the problem is to disable the UI elements while the page is loading. This solution is sub-optimal. A quick query might mean that the user's click gets blocked without a perceptible UI change, which breaks the principle of least surprise.
A better solution is to display the content that matches the users expectation. Thankfully this is easy to do.
The code sample above has been changed to the following:
Why Does This Work?
At first glance this could be a little confusing. Why are we checking the equality of something we defined only a few lines before?
load() function is the one that will match the user's input.
active_query is defined. There's the scope in
my_query is defined, and finally there's the scope in the success callback that has access to both. We can illustrate what's happening by adding a counter and a log statement like the following:
console.log("Request #: ", num_requests, ", my_query: ", my_query, ", active_query: ", active_query)
We'll add a line before and after the call to
User.team_index. If the first request takes longer to execute than the second, we'll get something like the following output:
Pre Request #: 1, my_query: foo, active_query: foo Pre Request #: 2, my_query: bar, active_query: bar Post Request #: 2, my_query: bar, active_query: bar Post Request #: 1, my_query: foo, active_query: null
if check will now fail and the user won't be presented with the wrong results.
Did you like this? Please share:
The Rise and Fall of a Personal Cargo-Cult
Cargo-cults are common when confronted with the unfamiliar and new. This is not that story. Instead this is a story about forgotten knowledge around a complex interaction.
The Pitfalls of Meritocracy and the Presidency of Harry S. Truman
Harry S. Truman is currently considered a great President, but he left office deeply unpopular. What does this tell us about meritocracy?
Compiler Errors - A forgotten scourge of early developers
Compiler errors weren't always trivial to resolve. Error handling has gotten much better.