This resource contains a collection of AngularJS best practices and AngularJS tips provided by our Toptal network members. As such, this page will be updated on a regular basis to include additional information and cover emerging AngularJS techniques. This is a community driven project, so you are encouraged to contribute as well, and we are counting on your feedback.
Unlike some other JavaScript frameworks, AngularJS requires the developer to do things the “Angular” way, which is a set of rules and best practices that are critical to building AngularJS web applications properly. Here we will embrace those best practices that will make you a better AngularJS developer.
Check out the Toptal resource pages for additional information on AngularJS common mistakes, AngularJS job description and AngularJS interview questions.
How to Use controllerAs
Syntax properly?
Angular is a very powerful framework, sometimes too powerful causing some developers to make some architecture mistakes. The two-way data binding and the power of directives are awesome, but you need to think about what are you doing and try to use some best practices to avoid common pitfalls during the development process.
Controllers are class-like objects to “control” the model and update the view, and as you know everything is based around the magic and mystic
$scope
property.
A good practice is to avoid binding everything to
$scope
, because too many bindings crowd the watch list of the $digest
loop. To avoid that, Angular give us the controllerAs
property.Writing controllers as classes
A class in Javascript (at least in ES5 for now) is something like this:
var aClass = function () {
this.name = 'Class name';
};
var instance = new aClass();
With this you can use the
instance
variable to access methods and properties.
Using the
controllerAs
property we write our Controllers in the same way, using this
instead of $scope
angular.module('myApp')
.controller('MyCtrl', function () {
this.name = 'Controller Name';
});
Now this can be instantiated in the template with something like the following:
<div ng-controller="MyCtrl as vm">
<h1 ng-bind="vm.name"></h1>
</div>
To access the properties and methods of the controller you use the
vm
instance.
With this you are namespacing the scopes, making the code cleaner and readable. Think about nested scopes.
<div ng-controller="BaseCtrl">
{{ name }}
<div ng-controller="SectionCtrl">
{{ name }}
<div ng-controller="FinalCtrl">
{{ name }}
</div>
</div>
</div>
Here you can see that each controller is accessing the
name
property, but the question is: which one? That code looks very confusing and is probably that one controller takes precedence over another, but you don’t know which one.
Using the
controllerAs
syntax this will be much cleaner:<div ng-controller="BaseCtrl as base">
Base scope: {{ base.name }}
<div ng-controller="SectionCtrl as section">
Section scope: {{ section.name }}
Base scope: {{base.name}}
<div ng-controller="FinalCtrl as final">
{{ final.name }}
</div>
</div>
</div>
As we can see in the above code, using
controllerAs
syntax allows us access to parent scopes without the hassle of scope collision and without using $parent
to access it.How to set watchers
One question that comes to mind when you use this kind of syntax is how to use a
$watch
call because you need to inject $scope
. We fight to remove the use of $scope
, and now we need to inject it anyway.
Well, we can keep using
controllerAs
and keep binding methods and properties to the this
object that is binded to the current $scope
. At the same time, we can keep the separation of concerns using $scope
only for special cases, like $watch
, $on
, or $broadcast
.
Keep in mind that using
controllerAs
the syntax for $watch
method changes a little bit. Normally you would do something like the following:app.controller('Ctrl', function ($scope) {
$scope.name = 'name';
$scope.$watch('name', function (newVal, oldVal) {
});
});
But that doesn’t work now, because
$watch
is looking for the watched property inside the $scope
, and you don’t directly bind that property to $scope
. Instead watched property is binded to this
. The correct way to do it now is as shown in the following example:app.controller('Ctrl', function ($scope) {
this.name = 'name';
$scope.$watch(function () {
return this.title
}.bind(this), function (newVal, oldVal) {
});
});
Alternative is using
angular.bind
:app.controller('Ctrl', function ($scope) {
this.name = 'name';
$scope.$watch(angular.bind(function () {
return this.title
}), function (newVal, oldVal) {
});
});
How can I declare controllerAs
without using the DOM attributes?
In the case of directives, you have the
controllerAs
property inside the directive signature:app.directive('Directive', function () {
return {
restrict: 'EA',
templateUrl: 'template.html',
scope: true,
controller: function () {},
controllerAs: 'vm'
}
});
Or for controllers in the
$routeProvider
:app.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'main.html',
controllerAs: 'main',
controller: 'MainCtrl'
})
});
Use bindToController
in Your Directives.
One of the Angular best practices when writing directives is to use the
controllerAs
syntax. This syntax provides us a native namespacing in the controller.
Just to remember, this change was introduced in Angular version 1.2. Before, everyone was writing controllers using the mystical
$scope
object, which quickly resulted with $parent
problems and the properties flying around the DOM.
The
controllerAs
syntax enables us to instantiate our controllers as class-like objects. In vanilla JavaScript, this would look like this:var aClass = function () {
this.name = "My class";
};
var instance = new aClass();
Here, we can use the
instance
variable to access the methods and properties from the aClass
. In Angular, with controllerAs
we get similar syntax for our controllers.
By covering the basics, we can now return to our main topic. When writing controllers using the
controllerAs
syntax we can face some issues. We are writing the components as class-like objects, but we keep injecting $scope
only to get access to the isolated scope properties:function controller ($scope, getNameFromSomeService) {
this.foo = {};
this.do = function () {
getNameFromSomeService.get().then(function (response) {
this.foo.bar = 'something';
$scope.name = response; //reference to the isolated property
}.bind(This));
}
}
function directive () {
return {
restrict: 'E',
scope: {
name: '='
},
controller: 'controller',
controllerAs: 'vm',
templateUrl: 'some/template/file.html'
}
}
angular.module('app')
.controller('controller', controller)
.directive('myDirective', directive);
As you can notice, our controller was in need to inject the
$scope
only to get access to name
property. We want to get rid of this object and adopt Angular best practices.
bindToController
to Rescue
The
bindToController
property allows us to bind isolated properties to the controller object instead of the $scope
. Quote from the documentation:When an isolate scope is used for a component (see above), and controllerAs is used, bindToController: true will allow a component to have its properties bound to the controller, rather than to scope. When the controller is instantiated, the initial values of the isolate scope bindings are already available.
Having this in mind, we can refactor our controller like this:
function controller (getNameFromSomeService) {
this.foo = {};
this.do = function () {
getNameFromSomeService.get().then(function (response) {
this.foo.bar = 'something';
this.name = response; //reference to the isolated property but using `this`
}.bind(This));
}
}
function directive () {
return {
restrict: 'E',
scope: {
name: '='
},
bindToController: true
controller: 'controller',
controllerAs: 'vm',
templateUrl: 'some/template/file.html'
}
}
It is worth noting this new property has more power than described in the documentation. You can use object to define the property which will allow you to move the property from the
scope
definition to bindToController
{% highlight javascript %}
function directive () {
return {
restrict: 'E',
scope: {},
bindToController: {
name: '='
},
controller: 'controller',
controllerAs: 'vm',
templateUrl: 'some/template/file.html'
}
}
Now our controllers don’t need the injection of
$scope
(we will need that only for events or watchers).
After this refactor we need to change our template too. The original template probably looks something like this:
<div>
<label ng-bind="name"></label>
<label ng-bind="vm.foo.bar"></label>
</div>
We need to change this template to the following one:
<div>
<label ng-bind="vm.name"></label>
<label ng-bind="vm.foo.bar"></label>
</div>
Now the
name
property is accessed from the vm
namespacing.How To Make ‘ng-click’ Conditional?
Say you have an element that must always be visible, no matter if the user is logged in or not. If the user is logged in, the element needs to be clickable. Example pseudo code would look like this:
<div ng-click=”somefunction() if currentUser.signedIn()”>Click me</div>
Unfortunately,
ng-if
is not an option. If ng-if is set to false, the element will not be seen at all. You could let it always be clickable, and then check if the user is logged in when you enter ng-click function, but this is not the best solution.
Toptal engineers have suggested two solutions to this problem.
One solution is to precede the function call with a boolean expression, as in this example:
<div ng-click=“currentUser.signedIn() && somefunction()”>Click me</div>
The solution works, but it is not easily readable and therefore not recommended. Using non standard techniques and applying tricks to the code can become a big code maintenance problem. Even the developer who wrote the code could forget what problem he was solving, and why it was written this way. And what if a new developer takes over?
Another solution involves writing more HTML code and adding two identical elements that have different behaviors.
Example pseudo code in that case would look like this:
<div ng-if="currentUser.signedIn()" ng-click="somefunction()">Click me</div>
<div ng-if="!currentUser.signedIn()">You can’t click me</div>
This should work well, especially when the duplicated elements are small, but it is not recommended when working with larger amounts of DOM elements. One way to eliminate this drawback is to put the elements into separate templates, so, even if the code is duplicated, it becomes more readable.
What Is One-Time Binding?
Angular 1.3 introduced an awesome new performance boost feature - one-time binding. Official Angular documentation describes it as:
One-time expressions will stop recalculating once they are stable, which happens after the first digest if the expression result is a non-undefined value.
So, how does it work?
Let’s remember the
$digest
cycle is basically a loop that runs under the hood of Angular where every binded data is added to a watcher list. The loop checks list for changes, it ends when no further change is found, and then triggers a DOM re-rendering.
When regular binding is used, that property is binded to the watcher list and it’s never removed from the list. This makes the
$digest
loop run through all the binded data. The bigger the application, the bigger list of bindings and watchers is, and finally the bigger the size of our $digest
is. With bigger loops, performances decrease, and in the case of a really big code base, the application could become even unresponsive.
As mentioned, starting from Angular 1.3 we can declare a value with the new one-time binding syntax. Angular will render this new value when it becomes defined and immediately unbind it from the watchers. This way, the number of bindings inside the
$digest
loop will be reduced.So, how can I use it?
The syntax for this is very simple: just add
::
in front of any value and it will be considered as an one-time binding.
Usually you would do something like this:
<label>{{'BASE_STRING' | translate}}</label>
New one-time binding versions look like this:
<label>{{::'BASE_STRING' | translate}}</label>
The translation value will not change, so it is not necessary to have it inside the
$digest
loop forever. By unbinding translation value, we increase application performance.
Another use case is with
ng-repeat
, like in the following example:<ul>
<li ng-repeat="item in ::vm.list"></li>
</ul>
This will unbind items from the list when the
ng-repeat
finish the population process, removing a lot of watchers from our $digest
loop.
Go ahead, upgrade to Angular 1.3 and try this to improve your application.
How To Deploy An AngularJS Apps?
There’s more than one way to skin a system architecture. How do the AngularJS professional engineers at Toptal do it?
One Toptal developer recently described a system architecture he had successfully implemented. Each service required by the application, be it a load balancer, a database, or the app server, is run in its own Docker image as a sort of miniaturized Service-Oriented Architecture. This way, only a few of the images are redeployed for each production push, leaving the rest up and running without interruption.
The advantage of this approach is that you don’t need to worry about server configuration. With Docker, you can easily create the production environment on your laptop, test it, play with it, and then launch the same virtual machine-images on the production server and still be 100 percent sure that they will behave the same way. You can also easily scrap the whole server at any time and re-build it from a script. In this scenario, the software will always run in a pristine environment. For example, sometimes you configure a server, edit a system configuration file and then forget about it and your software works fine. But when you try to run it on another server (which doesn’t have the configuration change you’ve forgotten about), your software will not run and you will spend a lot of time chasing this ”weird” problem. With Docker, such things do not exist; servers are re-built with 100 percent repeatability.
Another advantage working with Docker is that it easily allows you to separate dependencies in various parts of your architecture. For example, if your web server requires Ruby 2.x and some other part of the app (like some background worker) requires Ruby 1.x, typically, it is difficult to have that working on a single server. With Docker, you bundle Ruby 2.x with the app server, and Ruby 1.x with the worker on another image, and they happily live with each other, not interfering at all.
How do you manage so many images? Deployment configuration tools such as Ansible do the trick nicely. By giving you easy and comprehensive control over system configurations, version tracking, and deployments, managing the entire build is a breeze. You can write Ansible snippets for setting up Docker remotely, transferring machine-images to the server, and then starting containers remotely, along with some minimal supervisory configuration to keep them alive in case of problems. So basically, you end up deploying whole app simultaneously onto several servers in parallel, without doing single SSH manually to any of those servers. By making sure you execute the Ansible scripts carefully, and in the right order, it is relatively easy to achieve zero-downtime by performing rolling upgrades ith a microservice architecture, all parts are duplicated and behind the load balancer.
The only disadvantage is the complexity of this setup, so it is not for novice developers.
No comments:
Post a Comment