AngularDart is a port of the acclaimed framework to the Dart platform. It is being developed by the Angular core team. In this article I will compare the Dart and JS versions of the framework. In particular, I will look into dependency injection, directives, and digesting.

Intended Audience

The article is written for:

Dependency Injection

Injection by Name VS Injection by Type

AngularDart makes interesting use of the Dart optional type system: it uses type information to configure the injector. In other words, the injection is done by type, not by name.

// The name here matters, and since it will be minified in production,
// we have to use the array syntax.
m.factory("usersRepository", ["$http", function($http){
  return {
    all: function(){/* ... */}
  }
}]);
class UsersRepository {
  // Only the type here matters, the variable name does not affect DI.
  UsersRepository(Http h){/*...*/}
  all(){/* ... */}
}

Registering Injectable Objects

In AngularJS an injectable object can be registered with the Angular DI system using filter, directive, controller, value, constant, service, factory, or provider.

MethodsPurpose
filter Registering filters
directive Registering directives
controller Registering controllers
value, constant Registering configuration objects
service, factory, provider Registering services

 

As you can see, there are a lot of ways to register an injectable object, which often confuses developers. Partially, it is due to the fact that filter, directive, and controller are all used for different types of objects, and thus not interchangeable. The service, factory, and provider functions, on the other hand, are all used to register services, with provider being the most generic one.

AngularDart takes a very different approach: it separates the type of an object from how it is registered with the DI system.

Any object can be registered using value, type, or factory.

MethodsPurpose
value, type, factory Registering all objects

It can be done as follows.

// The passed in object will be available for injection.
value(UsersRepositoryConfig, new UsersRepositoryConfig());
// AngularDart will resolve all the dependencies
// and instantiate UsersRepository.
type(UsersRepository);
// AngularDart will call the factory function.
// You will have to resolve the dependencies using the passed in injector
// and then instantiate UsersRepository.
factory(UsersRepository, (Injector inj) => new UsersRepository(inj.get(Http)));

The fact that these functions can be used to register any object is a substantial simplification of the API.

Any class can be used as a service. You just need to register it with the Angular DI system. When the time comes, Angular will instantiate an instance of that class, and inject all the dependencies through constructor arguments.

Other types of objects, however, have to provide some extra information, which you use annotations for.

@NgController(
  selector: '[users-ctrl]',
  publishAs: 'ctrl'
)
class UsersCtrl {
  UsersCtrl(UsersRepository repo);
}

Similarly, special annotations are used for defining filters, components, and directives.

In AngularDart the type of an injectable object and how it is registered with the DI system are two orthogonal concerns.

Creating Modules and Bootstrapping an Application

The following is the standard way of creating an application in AngularJS.

var m = angular.module("users", ['common.errors']);
m.service("usersRepository", UsersRepository);
angular.bootstrap(document, ["users"]);

Which maps pretty closely to AngularDart.

final users = new Module()
..type(UsersRepository)
..install(new CommonErrors());
ngBootstrap(module: users);

Another way to do that is by extending Module.

class Users extends Module {
  Users(){
    type(UsersRepository);
    install(new CommonErrors())
  }
}
ngBootstrap(module: new Users());

This way is preferable when you want to wire up your components differently based on, for instance, the platform the application is running on.

Configuring Injectable Objects

AngularJS provides multiple options for configuring injectable objects. The simplest one is to inject a configuration object using value.

m.value("usersRepositoryConfig", {login: 'jim', password: 'password'});
m.service("usersRepository", function (usersRepositoryConfig){
  //...
});

Same can be done in Dart.

class UsersRepositoryConfig {
  String login;
  String password;
}
class UsersRepository {
  UsersRepository(UsersRepositoryConfig config){/* ... */}
}
type(UsersRepository);
value(UsersRepositoryConfig, new UsersRepositoryConfig()..login="Jim"..password="password");

Now, suppose UsersRepository takes two arguments instead of a hash and we cannot change that. In this case, we would use factory.

m.value("usersRepositoryConfig", {login: 'jim', password: 'password'});
m.factory("usersRepository", function (usersRepositoryConfig){
  return new UsersRepository(usersRepositoryConfig.login, usersRepositoryConfig.password);
});

The AngularDart version, once again, is very similar.

value(UsersRepositoryConfig, new UsersRepositoryConfig()..login="Jim"..password="password");
factory(UsersRepository, (Injector inj){
  final c = inj.get(UsersRepositoryConfig);
    return new UsersRepository(c.login, c.password);
});

Some prefer defining a provider for this purpose.

m.provider("usersRepository", function(){
  var configuration;

  return {
    setConfiguration: function(config)
    configuration = config;
},
$get: function($modal){
  return function(){
     return new UsersRepository(configuration);
  }
}
};
});

The setConfiguration method has to be called during the configuration phase of the application.

m.config(
  function(usersRepositoryProvider){
    usersRepositoryProvider.setConfiguration({login: 'Jim', password: 'password'});
  }
);

Since AngularDart has neither providers nor an explicit configuration phase, the example could not be directly translated into Dart. This is the closest I came up with.

final users = new Module()
  ..type(UsersRepositoryConfig)
  ..type(UsersRepository);

Injector inj = ngBootstrap(module: users);
inj.get(UsersRepositoryConfig)..login = "jim
..password = "password";

Directives, Controllers, and Components

Now, let's switch gears and talk about another pillar of the framework - directives.

Though AngularJS directives are extremely powerful and, in general, easy to use, defining a new directive can be confusing. I think the Angular team realized that, and that is why the API of the Dart version of the framework is very different.

In AngularJS there are two types of objects used to organize UI interactions:

In AngularJS these two types of objects are distinct: different helpers are used to register them, and completely different APIs are used to define them.

The first significant change that AngularDart brings is that these two types of objects are much more alike. Controllers are basically directives that create a new scope at the element.

The second change is the new object type - component. In AngularDart, directives are mostly used for augmenting DOM elements. When you want to define a new custom element, you use components.

Let's look at a few examples.

Directives

The vs-match directive can be applied to an input element. It listens to the changes on that element, and when the value matches the provided pattern, the directive will add the match class to the element.

It can be used as follows:

<input type="text" vs-match="^\d\d$">

This is a very simple AngularJS implementation of the described directive:

directive("vsMatch", function(){
  return {
    restrict: 'A',
    scope: {pattern: '@vsMatch'},
    link: function(scope, element){
      var exp = new RegExp(scope.pattern);
      element.on("keyup", function(){
         exp.test(element.val()) ?
         element.addClass('match') :
         element.removeClass('match');
       }); 
    }
  };
});

Now, let's compare it with the AngularDart version.

@NgDirective(selector: '[vs-match]')
class Match implements NgAttachAware{
  @NgAttr("vs-match")
  String pattern;
  Element el;
  Match(this.el);
  attach(){
    final exp = new RegExp(pattern);
    el.onKeyUp.listen((_) =>
    exp.hasMatch(el.value) ?
    el.classes.add("match") :
    el.classes.remove("match"));
  }
}

Let me walk you through it:

Components

The component we are going to look into next toggles the visibility of its content, and it can be used as follows:

<toggle button="Toggle">
  <p>Inside</p>
</toggle>

This is an AngularJS implementation of this component:

directive("toggle", function(){
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    scope: {button: '@'},
    template: "<div><button ng-click='toggle()'>{% raw %}{{button}}{% endraw %}</button><div ng-transclude ng-   if='showContent'/></div>",
    controller: function($scope){
      $scope.showContent = false;
      $scope.toggle = function(){
        $scope.showContent = !$scope.showContent ;
      };
    }
  }
})

Now, let's contrast it with the Dart version:

@NgComponent(
  selector: "toggle",
  publishAs: 't',
  template: "<button ng-click='t.toggle()'>{% raw %}{{t.button}}{% endraw %}</button><content ng-if='t.showContent'/>"
  )
class Toggle {
  @NgAttr("button")
  String button;
  bool showContent = false;
  toggle() => showContent = !showContent;
}

Though the JS and Dart versions look similar, under the hood there are important differences.

An AngularDart component uses shadow DOM to render its template.

AngularJS:

AngularDart:

Shadow DOM gives us the DOM and CSS encapsulation, which is great for building reusable components. Also, the API has been changed to match the web components specifications (e.g. ng-transclude was replaced with content).

An AngularDart component uses a template element to store its template.

This removes the need of hacks such as ng-src.

To recap, directives are used to augment DOM element. Components are a lightweight version of web-components, and they are used to create custom elements.

Controllers

The following example shows a very simple controller implemented in AngularJS.

<div ng-controller="CompareCtrl as ctrl">
First <input type="text" ng-model="ctrl.firstValue">
Second <input type="text" ng-model="ctrl.secondValue">
ctrl.valuesAreEqual()}
</div>
controller("CompareCtrl", function(){
  this.firstValue = "";
  this.secondValue = "";
  this.valuesAreEqual = function(){
    return this.firstValue == this.secondValue;
  };
});

The Dart version is quite different.

<div compare-ctrl>
First <input type="text" ng-model="ctrl.firstValue">
Second <input type="text" ng-model="ctrl.secondValue">
{ctrl.valuesAreEqual}
</div>
@NgController(
  selector: "[compare-ctrl]",
  publishAs: 'ctrl'
)
class CompareCtlr {
  String firstValue = "";
  String secondValue = "";
  get valuesAreEqual => firstValue == secondValue;
}

As mentioned above, controllers are basically directives that create a new scope at the element. All the options that can be used when defining a new directive can also be used when defining a controller. Having said that, it is still a good idea to avoid putting any DOM manipulation logic into your controllers, even though it is not enforced by the framework.

Filters

Finally, let's look at how you can define a filter.

filter("isBlank", function(){
  return function(value){
    return value.length == 0;
  };
});

and the Dart version:

@NgFilter(name: 'isBlank')
class IsBlank {
  call(value) => value.isEmpty;
}

Zones & $scope.$apply

Experienced Angular developers will appreciate the next feature. One may argue it has the most impact from the ones I covered in this article:

There is no need to call $scope.$apply when integrating with third-party components.

Let me illustrate it with the following example.

<div ng-controller="CountCtrl as ctrl">
  {{ctrl.count}}
</div>

CountCtrl is a controller that just increments the count variable.

controller("CountCtrl", function(){
  var c = this;
  this.count = 1;
  setInterval(function(){
    c.count ++;
  }, 1000);
})

An experienced AngularJS developer will notice right away that the code is broken. Angular just cannot see that the count variable has been changed in the callback. To fix this issue you have to wrap it in $scope.$apply, as follows:

controller("CountCtrl", function($scope){
  var c = this;
  this.count = 1;
  setInterval(function(){
    $scope.$apply(function(){
      c.count ++;
    });
  }, 1000);
})

This is a fundamental limitation of AngularJS - you need to tell Angular to check for changes. The frameworks tries to minimize the number of places where you have to do that by having a futures library bundled, and providing the $interval service. But the moment you start using some other futures library or, in general, integrating with async third-party components, you will have to use $scope.$apply.

Now, let's contrast it with the Dart version.

<div count-ctrl>
  {{ctrl.count}}
</div>
@NgController(
  selector: "[count-ctrl]",
  publishAs: 'ctrl'
)
class CountCtrl {
  num count = 0;
    CountCtrl(){
      new Timer.periodic(new Duration(seconds: 1), (_) => count++);
    }
}

The Dart version works even though there is no $apply, and Timer knows nothing about Angular. That is fantastic! To understand how this works, we need to learn about the concept of Zones.

Dart docs:

A Zone represents the asynchronous version of a dynamic extent. Asynchronous callbacks are executed in the zone they have been queued in. For example, the callback of a future.then is executed in the same zone as the one where the then was invoked.

You can think of a Zone as a thread-local variable in an event-based environment. The environment always has the current zone, and all the callbacks of all async operations go through the current zone. That gives Angular a place to check for changes.

In addition, using this mechanism the framework can collect information about the execution of your program, and, for example, generate long stack traces. So when an exception is thrown, you will see the stacktrace crossing multiple VM turns. Needless to say, it dramatically improves the dev experience.

Wrapping Up

What is going to be ported to JS

Based on the talk Misko and Igor gave at Devoxx, it looks like most of the changes are going to be ported to AngularJS, in particular:

Learn More

I hope this article gives you enough information to get started. If you want to learn more check out: