Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS - DRY two-way data-binding using controllerAs syntax and service properties

I've stumbled upon a problem that should be common and obvious but I can't seem to wrap my head around it.

I'm working on a small prototype app. My backend developer provides me with profile data in a JSON object. Let's say, it looks like this:

profile = {Name: 'John', Email: '[email protected]', DOB: '1980-11-03'}

I need these values in multiple locations and I also don't want to put backend http calls in the controllers, so I've created a service to handle this:

angular.module('app', [])
.service('ProfileService', ['$http', function ($http) {
    var service = this;

    service.Name = null;
    service.Email = null;
    service.DOB = null;

    service.getProfile = function () {
        return $http.get('/profile').then(function (response) {
                service.Name = response.data.Name;
                service.Email = response.data.Email;
                service.DOB = response.data.DOB;
                return true;
            });
    };

    return service;
}])
.controller('ProfileCtr', ['ProfileService', function (service) {
    var vm = this;

    service.getProfile().then(function () {
        vm.Name = service.Name;
        vm.Email = service.Email;
        vm.DOB = service.DOB;
    });
}]);

There are a number of problems with this solution:

  1. Since the profile data consists of primitives, directly binding to the service properties won't give automagically synchronization of data.
  2. More importantly, it breaks the DRY concept, as I've written data declarations in at least 3 different places (the database schema, in getProfile() and in the controller).

One solution would be to add a layer of indirection and create an object within the service:

angular.module('app', [])
.service('ProfileService', ['$http', function ($http) {
    var service = this;

    service.profile = {};

    service.getProfile = function () {
        return $http.get('/profile').then(function (response) {
                for (key in response.data) {
                    service.profile[key] = response.data[key];
                };
                return true;
            });
    };

    return service;
}])
.controller('ProfileCtr', ['ProfileService', function (service) {
    var vm = this;

    service.getProfile().then(function () {
        vm.profile = service.profile;
    });
}]);

This works in general, but now I get awkward controllerAs syntax:

<div ng-controller="ProfileCtr as ctr">
    <h1> {{ ctr.profile.Name }}</h1>
    <p> Email: {{ ctr.profile.Email }} <br /> DOB: {{ ctr.profile.DOB }}</p>
</div>

I'm wondering whether there is a way that gives me both: clean HTML {{ ctr.Name }} syntax and a DRY programming style.

Thanks for any hints!

like image 692
Michael Schober Avatar asked Sep 28 '22 14:09

Michael Schober


1 Answers

I have a feeling that you want more than this, but this to me is at least DRY:

angular.module('app', [])
.service('ProfileService', ['$http', function ($http) {
    var service = this;
    service.getProfile = function () {
        return $http.get('/profile').then(function (response) {
                return response.data;
            });
    };

    return service;
}])
.controller('ProfileCtr', ['ProfileService', function (ProfileService) {
    var vm = this;

    ProfileService.getProfile().then(function (profile) {
        vm.profile= profile;
    });
}]);

The service gets the data. You could add functionality for caching here too. The controller uses the service to get the data. There is no repeated code.

I like to use the $scope variable, which would remove the one-layer of indirection issue. However, the controllerAs does have it's advantages, particuarly if you are using nested controllers and want to make it clear which controller you are using. And the $scope identifier will be removed in version 2.

Using a directive for this section of html instead of a controller should make you code easier to read and re-use. It also is advised to make it ready to be upgraded to version 2.

Then:

app.directive('isolateScopeWithControllerAs', function () {

  var controller = ['ProfileService', function (ProfileService) {

    var vm = this;

    ProfileService.getProfile().then(function (profile) {
        vm.profile= profile;
    });

  }];    

  return {
      restrict: 'EA', //Default for 1.3+
      controller: controller,
      controllerAs: 'vm',
      bindToController: true, //required in 1.3+ with controllerAs
      templateUrl: // path to template
  };
});

Then your HTML still gives you:

    <h1> {{ vm.profile.Name }}</h1>
    <p> Email: {{ vm.profile.Email }} <br /> DOB: {{ vm.profile.DOB }}</p>

The ProfileCtr as vm would come into more use if you were using the directive for more than one object. For example, if you has a user directive, then you could have:

controllerAs: 'user',

with user.profile.name and ng-repeat='friend in user.friends' etc.

like image 100
trees_are_great Avatar answered Oct 03 '22 02:10

trees_are_great