Search code examples
angularjsangularjs-directiveangular-ngmodel

Data doesn't trickle down to directive using ngModel and a data transformation with $formatters and $parsers


I'm having issues with a directive not updating from the parent data on which it is based. It might be related to said directive using both ngModel inputs and a data transformation.

I've set up a plunker example that mimics the structure for the app that I'm working on:

plnkr example

The precise problem: When "Shuffle Numbers" is clicked, properties in the myData object in the root scope are updated. However, the myObjectInput child directives, which contain transformed data based on myData, do not update.

Any help would be greeeeeatly appreciated! Code pasted in below if you'd rather sift through that than look at the plunker example.


index.html

<!DOCTYPE html>
<html>

  <head>
    <script data-require="angular.js@1.4.5" data-semver="1.4.5" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="main.js"></script>
    <script src="my-object-inputs.js"></script>
  </head>

  <body>
    <h1>Data Binding with Directive Transformation</h1>

    <p>Problem: When "Shuffle Numbers" is clicked, properties in the myData object in the root scope are updated. However, the myObjectInput child directives, which contain transformed data based on myData, do not update.<p>

    <div ng-app="myApp" ng-controller="MainCtrl">
      <div class="left">
        <h2>Directives Data (editable)</h2>
        <div class="group" ng-repeat="group in myData.myGroups">
          <h4>{{ group.myGroupName }} 
            <a class="btn" ng-click="duplicateGroup( group )">+</a>
            <a class="btn" ng-click="removeGroup( group )">-</a>
          </h4>
          <my-object-inputs ng-model="group.myObjs"></my-object-inputs>
        </div>
        <div class="group"><a class="btn" ng-click="shuffle()">Shuffle Numbers</a></div>
      </div>
      <div class="right">
        <h2>Root Data</h2>
        <textarea disabled='disabled'>{{ myData | json }}</textarea>
      </div>
    </div>
  </body>
</html>

main.js

var myApp = angular.module('myApp',[]);

myApp.controller('MainCtrl', ['$scope', '$filter',
  function ($scope, $filter) {
    $scope.myData = {
      myGroups : [
        {
          myGroupName: 'First',
          myObjs : [
            {
              order: 0,
              number : 'one'
            },
            {
              order: 0,
              number : 'two'
            },
            {
              order: 1,
              number : 'three'
            },
            {
              order: 1,
              number : 'four'
            }
          ]
        },
        {
          myGroupName: 'Second',
          myObjs : [
            {
              order: 0,
              number : 'five'
            },
            {
              order: 0,
              number : 'six'
            },
            {
              order: 1,
              number : 'seven'
            },
            {
              order: 1,
              number : 'eight'
            }
          ]
        },
        {
          myGroupName: 'Third',
          myObjs : [
            {
              order: 0,
              number : 'nine'
            },
            {
              order: 0,
              number : 'nine'
            },
            {
              order: 1,
              number : 'nine'
            },
            {
              order: 1,
              number : 'nine'
            }
          ]
        }
      ]
    };

    $scope.shuffle = function() {
      // gather all numbers
      var numbersArr = [];
      for ( var i = 0; i < $scope.myData.myGroups.length; i++ ) {
        for ( var j = 0; j < $scope.myData.myGroups[i].myObjs.length; j++ ) {
          numbersArr.push( $scope.myData.myGroups[i].myObjs[j].number );
        }
      }

      // shuffle list of all numbers
      numbersArr = numbersArr.sort(function() { return 0.5 - Math.random() });

      // assign shuffled numbers to original data
      var k = 0;
      for ( var i = 0; i < $scope.myData.myGroups.length; i++ ) {
        for ( var j = 0; j < $scope.myData.myGroups[i].myObjs.length; j++ ) {
          $scope.myData.myGroups[i].myObjs[j].number = numbersArr[k++];
        }
      }
    }

    $scope.duplicateGroup = function( group ) {
      $scope.myData.myGroups.push( angular.copy( group ) );
      $scope.myData.myGroups[$scope.myData.myGroups.length-1].myObjs = angular.copy(group.myObjs);
    };

    $scope.removeGroup = function( group ) {
      if ( $scope.myData.myGroups.length > 1 ) {
        var index = $scope.myData.myGroups.indexOf( group );
        $scope.myData.myGroups.splice(index, 1);
      }
    };
  }
]);

my-object-inputs.html

<ul>
    <li class="my-object-inputs" ng-repeat="arr in transformedObjs">
        <input type="text" ng-repeat="item in arr" ng-model="item.number" />
    </li>
</ul>

my-object-inputs.js

myApp.directive('myObjectInputs', function() {
  var controller = ['$scope', function ($scope) {
    $scope.transformedObjs = [];
    $scope.showTransformedObjs = function() {
      console.log('transformedObjs = ');console.dir($scope.transformedObjs);
    };
  }];

  return {
    restrict: 'E',
    replace: true,
    require: 'ngModel',
    templateUrl: 'my-object-inputs.html',
    controller: controller,
    scope: {
      ngModel: '='
    },
    link: function( $scope, element, attrs, ngModelCtrl ) {

      // transform to new data format
      ngModelCtrl.$formatters.push( function(modelValue) {
        var transformedData = [[],[]];

        for (var i=0; i<modelValue.length; i++) {
          var transformed;
          if (modelValue[i].number == "zero") { transformed = 0 }
          else if (modelValue[i].number == "one") { transformed = 1 }
          else if (modelValue[i].number == "two") { transformed = 2 }
          else if (modelValue[i].number == "three") { transformed = 3 }
          else if (modelValue[i].number == "four") { transformed = 4 }
          else if (modelValue[i].number == "five") { transformed = 5 }
          else if (modelValue[i].number == "six") { transformed = 6 }
          else if (modelValue[i].number == "seven") { transformed = 7 }
          else if (modelValue[i].number == "eight") { transformed = 8 }
          else if (modelValue[i].number == "nine") { transformed = 9 }
          if (transformed) {
            transformedData[ modelValue[i].order ].push({
              order: modelValue[i].order,
              number: transformed
            });
          }
        }

        return transformedData;
      });

      // transform back to original data format
      ngModelCtrl.$parsers.push( function(viewValue) {
        var untransformedData = [];

        for (var i=0; i<viewValue.length; i++) {
          for (var j=0; j<viewValue[i].length; j++) {
            var untransformed;
            if (viewValue[i][j].number == 0) { untransformed = "zero" }
            else if (viewValue[i][j].number == 1) { untransformed = "one" }
            else if (viewValue[i][j].number == 2) { untransformed = "two" }
            else if (viewValue[i][j].number == 3) { untransformed = "three" }
            else if (viewValue[i][j].number == 4) { untransformed = "four" }
            else if (viewValue[i][j].number == 5) { untransformed = "five" }
            else if (viewValue[i][j].number == 6) { untransformed = "six" }
            else if (viewValue[i][j].number == 7) { untransformed = "seven" }
            else if (viewValue[i][j].number == 8) { untransformed = "eight" }
            else if (viewValue[i][j].number == 9) { untransformed = "nine" }
            if (untransformed) {
              untransformedData.push({
                order: viewValue[i][j].order,
                number: untransformed
              })
            }
          }
        }

        return untransformedData;
      });

      // watch for updates to data
      $scope.$watch('transformedObjs', function() {
        ngModelCtrl.$setViewValue( angular.copy( $scope.transformedObjs ) );
      }, true);

      // update view
      ngModelCtrl.$render = function() {
        $scope.transformedObjs = ngModelCtrl.$viewValue;
      };
    }
  }
});

style.css

h2,h4{margin:0 0 .5em}.right,p{max-width:600px}.left,.right{clear:none;float:left}a{color:#00f;cursor:pointer}h2{font-size:1.2em}h4{font-size:1em}ul{margin:0;padding:0}li{list-style:none}input{width:50px}textarea{border:1px solid #ccc;box-sizing:border-box;height:400px;width:100%}.btn{background:#ddd;padding:0 5px}.group{margin:0 0 1em}.left{padding:0;width:250px}.right{width:calc(100% - 300px)}

Solution

  • You do not need the controller. You can pass in the scope, watch it and transform it from the directive.

    http://plnkr.co/edit/QtBMgKVWfknlZ8m9O7uc?p=preview

     myApp.directive('myObjectInputs', function() {
      return {
        restrict: 'E',
        replace: true,
        require: 'ngModel',
        transclude: true,
        templateUrl: 'my-object-inputs.html',
        scope: {
          model: '=ngModel'
        },
        link: function( $scope, element, attrs, ngModelCtrl ) {
          var transform = function(modelValue) {
                    var transformedData = [[],[]];
    
            for (var i=0; i<modelValue.length; i++) {
              var transformed;
              if (modelValue[i].number == "zero") { transformed = 0 }
              else if (modelValue[i].number == "one") { transformed = 1 }
              else if (modelValue[i].number == "two") { transformed = 2 }
              else if (modelValue[i].number == "three") { transformed = 3 }
              else if (modelValue[i].number == "four") { transformed = 4 }
              else if (modelValue[i].number == "five") { transformed = 5 }
              else if (modelValue[i].number == "six") { transformed = 6 }
              else if (modelValue[i].number == "seven") { transformed = 7 }
              else if (modelValue[i].number == "eight") { transformed = 8 }
              else if (modelValue[i].number == "nine") { transformed = 9 }
              if (transformed) {
                transformedData[ modelValue[i].order ].push({
                  order: modelValue[i].order,
                  number: transformed
                });
              }
            }
    
            return transformedData;
          }
    
          var untransform = function (viewValue) {
            var untransformedData = [];
    
            for (var i=0; i<viewValue.length; i++) {
              for (var j=0; j<viewValue[i].length; j++) {
                var untransformed;
                if (viewValue[i][j].number === 0) { untransformed = "zero" }
                else if (viewValue[i][j].number == 1) { untransformed = "one" }
                else if (viewValue[i][j].number == 2) { untransformed = "two" }
                else if (viewValue[i][j].number == 3) { untransformed = "three" }
                else if (viewValue[i][j].number == 4) { untransformed = "four" }
                else if (viewValue[i][j].number == 5) { untransformed = "five" }
                else if (viewValue[i][j].number == 6) { untransformed = "six" }
                else if (viewValue[i][j].number == 7) { untransformed = "seven" }
                else if (viewValue[i][j].number == 8) { untransformed = "eight" }
                else if (viewValue[i][j].number == 9) { untransformed = "nine" }
                if (untransformed) {
                  untransformedData.push({
                    order: viewValue[i][j].order,
                    number: untransformed
                  });
                }
              }
            }
    
            return untransformedData; 
          }
    
          // watch for updates on parent to data
          $scope.$watch('model', function() {
            $scope.transformedObjs = angular.copy(transform($scope.model));
          }, true);
    
          // watch for updates on directive to data
          $scope.$watch('transformedObjs', function() {
            $scope.model = angular.copy(untransform($scope.transformedObjs));
          }, true);
        }
      }
    });