HTML5 Zone is brought to you in partnership with:

Victor works on the Angular team at Google. He is interested in functional programming, the Web platform, and client-side applications. Being a language nerd he spends a lot of my time playing with Smalltalk, JS, Dart, Scala, Haskell, Clojure, Ruby, and Ioke. Victor is a DZone MVB and is not an employee of DZone and has posted 45 posts at DZone. You can read more from them at their website. View Full User Profile

Intro to AngularDart: The Best Angular Yet

01.08.2014
| 7635 views |
  • submit to reddit

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:

  • Dart developers who have some experience with AngularJS.
  • AngularJS developers thinking about trying out AngularDart.
  • AngularJS developers who are not going to switch to Dart, but want to know more about the future of the framework. According to Angular folks, many of AngularDart’s features will be ported to AngularJS at some point. So learning about the Dart version of the framework can be interesting, even if you are not planning to use it.


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.

//JS:
// 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(){/* ... */}
  }
}]);


//DART:
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
filterRegistering filters
directiveRegistering directives
controllerRegistering controllers
value, constantRegistering configuration objects
service, factory, providerRegistering 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, factoryRegistering all objects

It can be done as follows.

  //DART:

  // 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.

//DART:

@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.

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

Which maps pretty closely to AngularDart.

//DART:
final users = new Module()
  ..type(UsersRepository)
  ..install(new CommonErrors());

ngBootstrap(module: users);

Another way to do that is by extending Module.

//DART:
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.

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

Same can be done in Dart.

//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.

//JS:
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.

//DART:
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.

//JS:
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.

//JS:
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.

//DART:
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:

  • Directives encapsulate all interactions with the DOM. They are declarative, and can be viewed as a way to extend html.
  • Controllers are imperative. They are unaware of the DOM, and can contain application logic.

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:

//JS:
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.

//DART:
@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:

  • NgDirective tells Angular that this class is a directive.
  • The selector property defines when this directive is activated. In this case, it is when an element has the vs-match attribute.
  • In addition to being able to inject any service into a directive, you can also inject the element the directive is applied to. That is what Match(this.el) is doing.
  • Bindings can be set up by passing a map, similar to AngularJS. But you can also do that by using annotations, which I personally find much easier to read and understand.
  • When the constructor of the directive is invoked, the pattern value has not been bound yet. The solution is to implement NgAttachAware. It provides the attach method, which will be invoked when the next digest occurs. At that point all the attribute mappings are processed, so I can safely construct the regular expression.
  • Finally, there are no link or compile functions.

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:

//JS:
directive("toggle", function(){
  return {
    restrict: 'E',

    replace: true,
    transclude: true,

    scope: {button: '@'},

    template: "<div><button ng-click='toggle()'>{{button}}</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:

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

  bool showContent = false;
  toggle() => showContent = !showContent;
}
  • NgComponent tells Angular that this class is a component.
  • publishAs is the name we can use in the template to access the toggle object. It is worth noting that t is available only in the template of this component, not in the inserted content.
  • template, not surprisingly, defines how this custom element is rendered.

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:

AngularJS Component

AngularDart:

AngularDart Component

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.

//JS:
<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.

//DART:
<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.

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

and the Dart version:

//DART:
@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.

//JS:
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:

//JS:
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.

//DART:
<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

  • AngularDart APIs are class-based.
  • The framework uses injection by type instead of injection by name.
  • The types of objects are decoupled from how they are registered with the DI system.
  • Annotations such as NgController and NgDirective are used to configure injectable objects.
  • Directives, filters, components, controllers, and services all can be registered using value, type, and factory.
  • Directives are used to augment DOM elements.
  • Components are a lightweight version of web-components, and they are used to create custom elements.
  • Components use shadow DOM to render their templates.
  • Controllers are directives that create a new scope at the element they are applied to.
  • The scope is digested automatically through Dart zones, eliminating the need for scope.$apply.

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:

  • Type-based injection
  • Using annotations for defining objects
  • Using shadow DOM
  • Zones

Learn More

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









Published at DZone with permission of Victor Savkin, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)