Search code examples
angularjsvalidationcustom-validatorsng-messages

Accessing parent array for unique key validation in ngMessages


For an array of key-value pairs ({ key: 'keyName', value: 'value' }), I need to ensure that the keys are unique within the list. I would like to utilize ngMessages to display the message. I am using Angular Material and ES6 with a transpiler, but that should not affect the substance of the question.

The essential problem is that I don't know how to elegantly access the other items of the array within the $validators pipeline. Here is a basic example of my current custom validator:

someModel.directive('unique', () => {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: ($scope, elem, attrs, ngModel) => {
      ngModel.$validators.unique = (currentKey) => {
        const otherItems = scope.$parent.$ctrl.items.slice(); // <== KLUDGE
        otherItems.splice($scope.$index, 1); // ignore current item
        return !otherItems.some(item => item.key === currentKey);
      };
    },
  };
});

Here's what the view template looks like:

<md-list ng-form="$ctrl.keyValuePairs">
  <md-list-item ng-repeat="item in $ctrl.items track by $index">
    <!-- key -->
    <md-input-container>
      <input type="text" name="key_{{::$index}}" ng-model="item.key" required unique />
      <div ng-messages="$ctrl.keyValuePairs['key_'+$index].$error">
        <div ng-message="required">Required</div>
        <div ng-message="unique">Must be unique</div>
      </div>
    </md-input-container>

    <!-- value -->
    <md-input-container>
      <input type="text" name="val_{{::$index}}" ng-model="item.value" ng-required="item.key" />
      <div ng-messages="$ctrl.keyValuePairs['val_'+$index].$error">
        <div ng-message="required">Required if key is present</div>
      </div>
    </md-input-container>
  </md-list-item>
</md-list>

It works, but I don't like having to 1) know the name of the collection (items), and 2) access it by climbing up through the $scope's parent.


Solution

  • The simple yet effective solution is to just pass in the parent array.

    someModel.directive('unique', () => {
      return {
        restrict: 'A',
        require: 'ngModel',
        link: ($scope, elem, attrs, ngModel) => {
          ngModel.$validators.unique = (currentKey) => {
            const otherItems = $scope.$eval(attrs.unique); // <== FIX
            otherItems.splice($scope.$index, 1); // ignore current item
            return !otherItems.some(item => item.key === currentKey);
          };
        },
      };
    });
    

    Abbreviated HTML:

    <input type="text" name="key_{{::$index}}" ng-model="item.key" unique="{{$ctrl.items}}" />
    

    h/t @msarchet