revolunet blog

web technologies for desktop and mobile

AngularJS Tips'n'tricks Part 2

| Comments

Here’s a new batch of AngularJS tips and gotchas; If you didn’t read it yet, you can check the part 1 and feel free to comment below :)

Access an element scope from outside

This is useful for debugging: in your Chrome console, highlight a node in the Elements tab, then, in the console, to check its scope :

1
angular.element($0).scope();

or even :

1
angular.element(document.getElementById('elementId')).scope();

The Batarang Chrome Extension is much powerful and let you inspect any scope, anywhere :)

Unwatch an expression

Sometimes you want to watch an expression only a few times, and then forget it. The $watch function returns a callback just for that. You just have to execute it back to destruct the watcher.

1
2
3
4
5
6
7
var watcher = $scope.$watch('data.counter', function(newValue, oldValue) {
    iElement.css('width', 50 * newValue + 'px');
    if (newValue >= 10) {
        // autodestruction when data.counter reaches 10
        watcher();
    }
});

Group elements in a select

If you play with selects, the ng-options directive is quite powerful and has many syntaxes.

For example you can easily group a model by attribute to have a nested select menu :

1
<select ng-options="distrib.version group by distrib.name for distrib in distribs"></select>

Of course, just add an ng-model directive to your select to data-bind it to one of the values.

Filter falsy values

You can’t use the builtin filter for falsy attributes or values :/ I don’t know if this is a bug of feature, but a simple workaround is to use a custom filter function that you can expose on the scope. (you can also define your own filter).

1
2
3
4
$scope.testValues = ['a', 'b', 'c', false, true, 0, -1, 5, 42];
$scope.isFalsy = function(val) {
  return !val;
}
1
2
3
4
5
{{ testValues|filter:true }}        //  [true]
{{ testValues|filter:5 }}           //  [5]
{{ testValues|filter:0 }}           //  ['a', 'b', 'c', false, true, 0, -1, 5, 42]
{{ testValues|filter:false }}       //  ['a', 'b', 'c', false, true, 0, -1, 5, 42]
{{ testValues|filter:isFalsy }}     //  [false, 0]

Filter on objects

The builtin filter function is quite powerful and allows you to filter a list of objects on given properties only, or on everything, exactly or not, negated or not, etc…

1
2
3
4
5
{{ games|filter:'street' }}                       //  all games containing "street" in any property
{{ games|filter:'!street' }}                      //  all games NOT containing "street" in any property
{{ games|filter:{name:'street'} }}                //  all games containing "street" in their name
{{ games|filter:{name:'street'}:true }}           //  all games named exactly "street"
{{ games|filter:{name:'street', device:'PS3'} }}  //  all games containing "street" in their name and PS3 in their device

ng-repeat with duplicate items

If for some reason you need to have duplicate items in your ng-repeat you can extend your ng-repeat expression with track by $index and it works :)

Directive : parse attribute without watching it

In one of your directive, if you need to have a read-only access to an attribute model, but without the automatic watch/binding, you can simply use & instead of = when declaring the binding in the scope. You’ll then be able to access the model value . (this may break in future releases). This is a shortcut for using the $parse service on the current scope.

1
2
3
4
5
6
7
8
9
10
11
12
app.directive('rnCounter', function() {
  return {
    scope: {
      count: '&rnCounter'
    },
    link:function(scope, iElement) {
      iElement.bind('click', function() {
        console.dir(scope.count());
      })
    }
  }
})

Data-binding to a boolean

You can bind to a boolean value, but you can’t update that value from your directive.

This won’t work :

1
<div rn-zippy status="true"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.directive('rnZippy', function() {
  return {
    restrict: 'A',
    scope: {
      status: '=?'
    },
    link: function(scope, iElement) {
      function toggle () {
        scope.$apply(function() {
          scope.status = !scope.status;
        });
      }
      iElement.bind('click', toggle);
    }
  }
})

You need to use a real model, OR, initialise a new one and use it as your model instead :

1
<div rn-zippy ng-init="status=true" status="status"></div>

Includes onload

You can trigger a callback when your ng-include partial is loaded :

1
<div ng-inlude="'partials/' + page + '.html'" onload="callback()"></div>

For the ngView, you need to listen to the $viewContentLoaded event.

Express testing

You know you should write serious tests suites ? There’s everything to help you in AngularJS with Karma test runner + Jasmine.

Once you have many tests, they can take some time to execute and there’s a little trick to speed you up : you can limit the test-runner to a given test group by using ddescribe instead of describe and to a single test with iit instead of it.

That’s awesome to focus on specific tests for a while.

Service decorators

You can easily decorate and modify any existing service or directive. That’s how the ngMobile overrides the ngClick directive to handle transparently the FastClick behaviour.

Here’s an exemple that overrides the $sniffer service and fix the animation detection for older androids devices (landed in ef5bc6c) :

1
2
3
4
5
6
7
8
9
app.config(['$provide', function ($provide) {
  $provide.decorator('$sniffer', ['$delegate', function ($delegate) {
    if (!$delegate.transitions||!$delegate.animations) {
      $delegate.transitions = (typeof document.body.style.webkitTransition=== 'string');
      $delegate.animations = (typeof document.body.style.webkitAnimation === 'string');
    }
    return $delegate;
  }]);
}]);

That’s all for today, feel free to ask and comment below :)

Comments