angularjstwitter-bootstrapangularjs-directiveangular-cookies

Custom directive not picking up model initialization


I have written a custom AngularJS directive that implements a toggle checkbox based on Bootstrap Toggle. I added support for angular-translate, but that is beyond my actual proplem. Furthermore, I wanted to use angular-cookies to save and restore the current state of a particular checkbox.

However, my directive does not pick-up the initial value of the data model properly.

This is my directive:

app.directive('toggleCheckbox', ['$rootScope', '$translate', '$timeout', function($rootScope, $translate, $timeout) {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, element, attributes, ngModelController) {

      // Change model, when checkbox is toggled
      element.on('change.toggle', function(event) {
        var checked = element.prop('checked');
        console.log('change.toggle was called: ' + checked + ' vs. ' + ngModelController.$viewValue);
        if (checked != ngModelController.$viewValue) {
          scope.$apply(function changeViewModel() {
            ngModelController.$setViewValue(checked);
            console.log('change.toggle:', checked);
          });
        }
      });

      // Render element
      ngModelController.$render = function() {
        element.bootstrapToggle(ngModelController.$viewValue ? 'on' : 'off')
      };

      // Translate checkbox labels
      var updateLabels = function() {
        var offLabel = (attributes['off'] ? $translate.instant(attributes['off']) : 'Off');
        var onLabel  = (attributes['on']  ? $translate.instant(attributes['on'])  : 'On');

        angular.element(document).find('label.btn.toggle-off').html(offLabel);
        angular.element(document).find('label.btn.toggle-on').html(onLabel);
      };

      // Update labels, when language is changed at runtime
      $rootScope.$on('$translateChangeSuccess', function() {
        updateLabels();
      });

      // Initialize labels for the first time
      $timeout(function() {
        updateLabels();
      });

      // Clean up properly
      scope.$on('$destroy', function() {
        element.off('change.toggle');
        element.bootstrapToggle('destroy');
      });

      // Initialize element based on model
      var initialValue = scope.$eval(attributes.ngModel);
      console.log('initialValue:', initialValue);
      element.prop('checked', initialValue);
    }
  };
}]);

This is how I initialize the data model from cookie:

mainController.controller('MainCtrl', ['$scope', '$cookies', 'Main', function($scope, $cookies, Main) {

  this.$onInit = function() {
    $scope.settings.foobar = $cookies.get('foobar');
    console.log('$onInit(): ', $scope.settings.foobar);
  };

  // ...

}]);

And this is how I eventually use my directive:

<div id="foobar-switcher" ng-if="isAdmin()">
  <label for="foobar_toggle"><span translate="foobar"></span>:</label>
  <input id="foobar_toggle" type="checkbox"
    ng-model="settings.foobar" ng-change="setFoobarCookie(settings.foobar)" toggle-checkbox
    data-off="foo_label" data-offstyle="success"
    data-on="bar_label" data-onstyle="danger" />
</div>

Ultimately, I get this debug output:

controllers.js:33 $onInit(): true

directives.js:76 initialValue: true

directives.js:37 change.toggle was called: false vs. false

So in case the value true is stored in the cookie, the model gets initialized properly in the global scope and even the directive uses the correct value to initialize element. The change.toggle event handler is triggered as well, but there the element's value is now false.

Why is that and how can I fix this problem?


Solution

  • It turned out that $cookies.get('foobar') returns a String and the checkbox cannot handle a ng-model of that type. Instead, the cookie value must be evaluated such, that a boolean value can be set in the data model:

    var cookie = $cookies.get('foobar');
    $scope.settings.foobar = (cookie === 'true');
    

    The checkbox is then correctly initialized on page load.