AngularJS - Tips may you don't know

AngularJS gonna to celebrate the sixth birthday in this October and I think spending time to talk about's just waste time. It's too famous and influence to many modern Front-End framework and its fan number's increasing significantly day by day. However, there's no problem when your code's a puppy then after serveral months expanding application, you realize that your cute dog turn to horriable monster. In this post, I wanna share some tips after serveral months keep a monster application under the control

1. extend(), copy() and merge()

Data processing's quite interesting section of AngularJS but sometimes you'll see that working with data object's really paintful with many many object properties including primetive data and funciton. Let see and example

$http({
  method: 'GET',
  url: '/users',
}).success(function(data) {
  //data = {firstName: 'Bond', lastName: 'James', phoneNum: '777777777'};
  $scope.firstName = data.firstName;
  $scope.lasteName = data.lastName;
  $scope.phoneNum  = data.phoneNum;
});

For now, we got just three properties and I think that many people can skip this line. But in a bad day, your customer want to show more than just basic informaiton, what do you gonna to do? Continue to do in the same old way sounds painful if props number reachs hundreds. To tackle this obstacle, we need a swiss knife and AngularJS provide us some sharp function

angular.extend()

Just like its name, this method extends an object by stealing properties from other object. Comming back the first example, three attributes of data will be added to $scope in a single line


angular.extend($scope, data);

Basically, angular.extend(dst, src1, src2, ...) shallow copies the properties of source object from right to left and finally, the target's destionation object

var src1 = { firstName: 'Bond', age: 30 };
var src2 = { lastName: 'James', age: 26, skill: {} };
var dst = {};

console.log(angular.extend(dst, src1, src2));
// => {firstName: "Bond", age: 26, lastName: "James", skill: {}}

console.log(src2.skill === dst.skill);
// => true src2 and dst share the same skill object due to shallow copy.

angular.merge()

merge() has been available since releasing of AngularJS 1.4 and at the glance, it makes reader so confused with extend(). Main purpose of merge() is deep (recursively) copy the properties of the source objects to the destination object. Back to above example

var src1 = { firstName: 'Bond', age: 30 };
var src2 = { lastName: 'James', age: 26, skill: {} };
var dst = {};

console.log(angular.extend(dst, src1, src2));
// => {firstName: "Bond", age: 26, lastName: "James", skill: {}}

console.log(src2.skill === dst.skill);
// => false  src2 and dst point to different object due to deep copy.

However, it's still vague to image the differences between extend() and merge() so let see another example

//merge()
var src1 = {assets: {car: 'Rolls-Royce', girl: 'Halle Berry', gun: 'Beretta 418'}};
var src2 = {assets: {car: 'Aston Martin', girl: 'Monica Bellucci'}};
var dst = {};

console.log(angular.extend(dst, src1, src2));
//=> {assets: {car: 'Aston Martin', girl: 'Monica Bellucci'}}
//merge()
var src1 = {assets: {car: 'Rolls-Royce', girl: 'Halle Berry', gun: 'Beretta 418'}};
var src2 = {assets: {car: 'Aston Martin', girl: 'Monica Bellucci'}};
var dst = {};

console.log(angular.merge(dst, src1, src2));
//=> {assets: {car: 'Aston Martin', girl: 'Monica Bellucci', gun: 'Baretta 418'}};

We can clearly see that the properties from src2 was copied to src1 and replace the older ones so gun's reserved and now, 007 got everything he need from super car, hot girls and his favorite gun

angular.copy()

Easy to understand, this method clone an object to another object

var oo7 = { firstName: 'Bond', age: 30 , assets: {}};
var spy = angular.copy(oo7);

console.log(oo7 === spy);
// => false
console.log(oo7.assets === spy.assets);
//=> false

Both return false because simply reason, two of them are totally different objects

2. ng-bind and one-time binding

ng-bind

When working with Angular, we're familiar with double curly braces syntax ({{}}) for embeding data from controll to view. That's original expression of AngularJS but not good for health of application due to performance reason.

<div>
  {{user.name}}
<div>

The devil face of curly braces come from it procedure when it store entire expression in memory then perform dirty-checking then refreshing the expression in every digest cycle even if it is not required. To simplify this, we can use ng-bind

<div ng-bind="user.name">
<div>

Different to {{}}, ng-bind, which acts like an attribute tag, creates a watcher for variable passed to it then only applying when the value passed is changing actually

One-time binding

Back to fundamental, each time a $scope variable's created, AngularJS will add a $watcher for checking the changes and when it happens, the digest cycle will perform to guarantee that new value will be updated on whole application. Actually, in some context, the $scope variable doesn't change anything since it was born and the $watcher now turn to heavy backpack of soldier. From Angular verion 1.3, the one-time binding feature has opened a smart way for this kind of variable. To employ this, just add double colons before your varibale

<div ng-bind="::user.name"></div>

But you need to make sure that this variable will never changes in any circumstances if you don't want stupid bugs appear

3. ng-if/switch instead of ng-hide/show

I will start wil ng-hide/show first.

<div id="hello" ng-show="isCurrentUser">
  Hello {{user.name}}
<div>

Basically, all the nested content in ng-show/hide will be rendered and showing only satisfy condition. If you familiar with jQuery, it just like

if(isCurrentUser) {
  $("#hello").show();
} else {
  $("#hello").hide();
}

And as you can see, there're not too much improvement and your DOM structure doesn't lessen. But AngularJS's smarter than we thought, they provide ng-if/switch for you to chose to render whole nested content or not. ng-if/switch removes or recreates a portion of the DOM tree based on an expression. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.

4. Move DOM filter to controller

Filter module's tryly cool feature of AngularJS that helps us to present data on view follow some format

{{ filter_expression | filter : expression : comparator : anyPropertyKey}}

Easy to use, nobody can deny it but placing filter on view likes a black hole of performance because AngularJS need to run filter twice in each $digest cycle if there're something change. The first run is from $$watchers to detect any changes and the second one is to verify if there are further changes that need updated values What we need now's move filter from view to controller by injecting $filter to your controller then we'll execute filter action in controller like this

$filter('filter')(array, expression, comparator, anyPropertyKey)

When using $filter, we can format your data to achieve requirement format then parse it to a $scope variable when ready. This's not only separate data model and DOM view but also reduce number of watchers because everything we place in view's accompanied by a watcher

5. $scope.$digest instead of $scope.$apply()

This two functions seems so familiar and so many people get confused when using it, espeacially when add more plugins or change something not in data-model. To deeply understand these methods, It's much better for devs know about $scope in AngularJS (https://github.com/angular/angular.js/wiki/Understanding-Scopes)

$scope.$apply()

We use $apply() to tell AngularJS that a model changes has occured outside of its lifecycle then let Angular update itself with these new value. Sound too complicated, right? Let see an example

$('#myElement').myPlugin({
  onchange: function (newValue) {
    // model changes outside of Angular
    $(this).val(newValue);
    // tell Angular values have changed and to update via $digest
    $scope.$apply();
  }
});

In this example, we're mixing both jQuery plugin and AngularJS (I don't recommend to do it in your app) and it's clearly that setting new value of jQuery action doesn't belong to angular model. When $scope.$apply() is called, AngularJs fires event for whole application run '$digest' loop and in turn run $rootScope.$digest(). This is what actually kicks off the internal $digest cycle when processing all watchers of $scope in whole application from top to bottom level. If new value affects to all application, we don't have another choice but if it works in just limited area of application, this proceess's truly giant pain in the ass. For this case, we can use $scope.$digest() to call exactly $digest() in that scope. It's notiable that we should define which $scope we're working on so in a application has many clone scopes, it perfectly fit for using $scope.$digest()

6. Conclusion

Back to fundamental, when working with AngularJs, we need to separate two part of application: data-model and DOM elements. Try to do everything related logic and operators in your data model and in view, just for presentating.