javascriptangularjsmodelangularjs-directiveangularjs-ng-model

Store an object in an ngModel with a directive in AngularJS


I'm working with angularjs for some weeks now, and I don't get what the angularjs designers thought when designing the $viewValue and $modelValue functionality from the ngModelController. Code example:

index.html:

<!DOCTYPE html>
<html>

  <head>
    <script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.18/angular.js"></script>
    <script src="script.js"></script>
  </head>

  <body ng-app="PlunkerApp" ng-controller="mainController">
    <listfield ng-model="termList"></listfield>
  </body>

</html>

script.js:

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

plunkerModule.directive('listfield', function() {
  'use strict';
  var link = function(scope, element, attrs, ngModelController) {
    console.log('listfield.link():', scope);
    ngModelController.$parsers.push(function(value) {
      console.log('listfield.model.parser:', value);
      return value ? value.join(', ') : undefined;

    });
    ngModelController.$formatters.push(function(value) {
      console.log('listfield.model.formatter:', value);
      return value ? value.split(/,\s*/) : undefined;
    });
  }
  return {
    restrict: 'E',
    link: link,
    require: 'ngModel',
    scope: {
      ngModel: '='
    },
    template: '<input type="text" ng-model="ngModel">'
  };
});

plunkerModule.controller('mainController', function($scope) {
  $scope.termList = "";
  $scope.$watchCollection('termList', function(newValue, oldValue) {
    console.log('mainController.watch.list:', newValue);
  });
});

plunker link: http://plnkr.co/edit/T5a8zEQuRyYWnpsZZV9W?p=preview

So in this application the value from the directives input element is written into the global scope, which works fine! My problem is, I'm not interested in the "raw" string value, i want the array that is generated by the formatter but the input element should still show the string value.

How can I do that??

Looking forward to your answers.


Solution

  • The problem here is that both your <input> and your <listfield> tags have an ngModel, confusing which one is called when. You can use the replace: true option for your directive to remove the <listfield> tag and work only with the <input>, like so:

    var plunkerModule = angular.module('PlunkerApp', []);
    
    plunkerModule.directive('listfield', function() {
      'use strict';
      var link = function(scope, element, attrs, ngModelController) {
        console.log('listfield.link():', scope);
        // Your formatters and parsers seemed to be the other way around
        // The formatter transforms Model -> View
        // Whereas the parser transforms View -> Model
        ngModelController.$formatters.push(function(value) {
          console.log('listfield.model.formatter:', value);
          return value ? value.join(', ') : undefined;
    
        });
        ngModelController.$parsers.push(function(value) {
          console.log('listfield.model.parser:', value);
          return value ? value.split(/,\s*/) : undefined;
        });
      }
      return {
        restrict: 'E',
        link: link,
        require: 'ngModel',
        replace: true, // Removes the <listfield> tag
        scope: {
          model: '='
        },
        template: '<input type="text" ng-model="model">'
      };
    });
    
    plunkerModule.controller('mainController', function($scope, $timeout) {
      $scope.termList = [1,2,3]
      $scope.$watchCollection('termList', function(newValue, oldValue) {
        console.log('mainController.watch.list:', newValue);
      });
      $timeout(function changeTermList() { $scope.termList = [4,5,6]}, 2000)
      // This is to demonstrate the binding used via the isolate scope(=)
    });
    

    And the relevant HTML:

      <body ng-app="PlunkerApp" ng-controller="mainController">
        <listfield model="termList"></listfield>
      </body>
    

    Demo: http://plnkr.co/edit/G0tlC9qhW8AHa58yVSgj?p=preview