“I will kill myself until the stat is working”
- Lead developer on the eve of the next update
Introduction
AngularJS continues to gain popularity, and there are more and more articles and lessons that tell you how to work most effectively with this framework. Unfortunately, they rarely give examples from real projects, and there is no description of the subtleties that one has to deal with in the process. I want to devote this article to exactly such things, so if you are just going to work with “angular”, some things may be difficult to understand.
As is often the case, I began my acquaintance with AngularJS, finding a new job. I was given the task to quickly explore the front-end. And since the project is a game, a single-page application was required. Therefore, in fact, "Angulyar". I set to work.
ng-repeat in a rapidly changing world
I was interested in the list of players on the server during the match. All code, of course, was packed into a controller, but inside it was a mess. Comments, of course, was not. The essence was approximately as follows:
- create N list items
- add spans to them
- every 5 seconds proivzodit update
')
All this was also bolted own scrollbar that I was somewhat surprised.
Having a little rummaged in the dock, I decided to use ng-repeat instead of this heap of code. I was pleased with the result: I got the same thing, but much clearer and more compact. However, the triumph was short: it turned out that if this list was on the screen, there was a performance drop every 5 seconds. It looks like I did something wrong after all.
It turned out that every time I update the array, the framework redraws all 100,500 elements. And since operations with DOM are traditionally heavy, the browser starts to hang. It also turned out that the custom scrollbar was added, including for speed. Those. Not the whole array got to the page, but only a part of it. The position of the scroll asked the number to retreat to. And on the screen displayed the desired interval. In addition, the elements were not completely redrawn, only the text value changed within them. It became a little clearer to me what was really going on.
I did not want to return everything back, so I decided to add a pagination, good, here it happens very simply:
- Add the currentPage, pageSize and pageCount variables to scope.
- Add buttons to the template with the ng-click directive that change currentPage
- we add in the ng-repeat filter limitTo, equal to pageSize.
Voila. The result began to look a little better. The browser is no longer dying, but still small failures remain. The main problem I still did not solve.
And then it dawned on me. Due to the fact that during the upgrade, I redefined the array with the list of players, “Angulyarka” believed that it receives new objects, even if the values in their keys were completely identical. This stupid mistake cost me more than one hour of digging into the docks and furious treys. The only thing that was required of me was to change the values inside the objects of the array.
In the end, I decided to make a hybrid version: I returned the scroll, but I updated the elements at the mercy of the bindings, and finally got what I wanted: a fast and concise controller that could later be easily expanded.
The final template looked like this:
<ul id="list-players"> <li ng-repeat="player in listPlayers" ng-class-odd="'odd'" ng-class="{ 'highlighted': player.highlighted, 'obs': player.observing, 'empty': player.empty }" ng-click="ObservePlayer(player.nickname)"> <span class="place" ng-bind="player.place"> </span><span class="nickname" ng-bind="player.nickname"> </span><span class="kills" ng-bind="player.kills"> </span><span class="deaths" ng-bind="player.deaths"> </span> </li> </ul>
In the controller, too, everything is quite simple:
for (var i = 0, index, player, $li; i < $scope.listSize; i++) { index = startIndex + i; var $player = $scope.listPlayers[i]; $player.empty = !player; if (!$player.empty) { $player.highlighted = player.id == focusOnPlayer $player.observing = player.status == 3; $player.place = !$player.empty ? index + 1 : ''; $player.nickname = player.nickname || ''; $player.deaths = player.deaths !== undefined ? player.deaths : ''; $player.kills = player.kills !== undefined ? player.kills : ''; } }
ng-class and ng-style against sprites
Having finished this small but rather tedious refactoring, I finally proceeded to the main thing: adding features. My task was to display the rank icon next to the player’s nickname, and also the medals at the end of the round if he took the first three places. With medals, everything is quite simple, I just added the value of medal to $ player: 0 - no medal, 1 - gold, etc., the following element was added to the template:
<i class="medal-icon-small" ng-class="{ 'medal-gold': player.medal == 1, 'medal-silver': player.medal == 2, 'medal-bronze': player.medal == 3 }"> </i>
Those. I just add a class depending on the value received. There is no class - and instead of an icon only emptiness.
But what to do if you need to choose not from three icons, but from ten. And if out of a hundred or a thousand? There are 14 ranks in the game, and creating a separate class for each rank is at least time consuming. A string of classes will clutter up both the template and the controller if we transfer it to the function. However, the function can be used, and here the ng-style came to the rescue:
<i class="rank-icon-small" ng-style="getRankStyle(player.rank)"></i>
In getRankStyle, the rank value is simply transferred, and it already returns the desired style:
$scope.getRankStyle = function(index) { return { 'background-position': '0px ' + -(index * 16) + 'px' }; }
If desired, this feature can be made even more universal. Instead of magic, pass the height to which you should retreat. Or even the width of the sprite, if you want to group the icons in the form of a square. If we use one big sprite for all graphics on the site, then we can set the initial indent. Application is limited only by common sense and imagination of the developer.
In the same way, by the way, I made a presentation of the character skins and visualization of the server status. However, the first one came out even a little more difficult, because along with the background-position I also returned background-image, because the way to the picture from which preview sprites were taken had to come from the server.
Underwater rocks
For all its merits, AngularJS has a number of significant drawbacks, and ironically, they are not inside the framework itself, but inside the heads of its developers. On the one hand, this is a complete lack of desire to bring the documentation to a readable form. You can start acquaintance with it only after you have already understood the basic principles of the framework, and want to get a better understanding of the details. On the other hand, there is too much difference between the stable and unstable versions, and even more: some features may simply not work in the compressed version provided on the official website.
It should also be attentive to the work of binding. In the first example, I made a rather stupid mistake by turning to an array, and not to its elements. This applies to everything: instead of a large number of values in the scope itself, it is better to group them into categories in separate objects (the same forms, for example).
It would probably be worthwhile to talk about how to effectively organize the structure of the project, but this is already a topic for a separate article.
PS: someone probably already guessed that the article deals with the
Bombermine project, in which, as I said, I have been working recently.