pixelbandito
pixelbandito

Reputation: 555

After using $setValidity at the form level, validity is stuck on false

I'm writing a fairly complicated search form in Angular.

The form is broken into sections - you can search by ID, url, text string or location. When you search by text string and location at the same time, I need to validate that you submit latitude and longitude together.

In other words, searching by a text string is fine. Searching by lat / lng is fine. Searching by a text string and latitude but omitting longitude is not ok.

Whenever possible, I'm using HTML5 and angular directives for validating individual fields' contents, but I'm trying to validate a particular combination of values by using a scope watcher, looking at the form object, and using $setValidity() if I discover that the current search mode is incompatible with a particular combination of fields.

My current issue is that, once I've used $setValidity() once, that validation state is "stuck". When the user switches out of 'textOrLocation' search mode, I want to let angular go back to its default validation behavior. I don't understand why it's not doing that - I only call $setValidity() on scope change after checking the form's in 'textOrLocation' mode.

Javascript:

$scope.search = {
    mode: 'id'
};

$scope.$watch(textOrLocationValid, function() {});

function textOrLocationValid() {
    var usingTextOrLocation = $scope.search.mode == 'textOrLocation';
    if (usingTextOrLocation) {
        var textModel = $scope.form.searchText || {},
            textValid = textModel.$valid,
            textValue = textModel.$modelValue,
            latModel = $scope.form.searchLat || {},
            latValid = latModel.$valid,
            latValue = latModel.$modelValue,
            lngModel = $scope.form.searchLng || {},
            lngValid = lngModel.$valid,
            lngValue = lngModel.$modelValue,
            formValid = (textValid && latValid && lngValid) && // No invalid fields
            ((latValue && 1 || 0) + (lngValue && 1 || 0) != 1) && // Either both lat and long have values, or neither do
            (textValue || latValue); // Either text or location are filled out
        if (formValid) {
            // Explicitly set form validity to true
            $scope.form.$setValidity('textOrLocation', true);
        } else {
            // Explicitly set form validity to false
            $scope.form.$setValidity('textOrLocation', false);
        }
    }
}

HTML

<form name="form">
    <div ng-if="search.mode == 'id'">
        <input type="text" name="searchId" required>
    </div>
    <div ng-if="search.mode == 'textOrLocation'">
        <input type="text" name="searchText">
        <input type="number" name="searchLat" min="-90" max="90" step="0.000001">
        <input type="number" name="searchLng" min="-180" max="180" step="0.000001">
    </div>
    <button ng-disabled="form.$invalid">Submit</button>
</form>

Upvotes: 0

Views: 3374

Answers (3)

Stepan Kasyanenko
Stepan Kasyanenko

Reputation: 3186

To create complex tests, you can use a small directive use-form-error, which may also be useful to you in the future.

With this directive, you can write:

<div ng-form="myForm">
  <div>
    <input type="text" ng-model="searchText" name="searchText">
    <input type="number" ng-model="searchLat" name="searchLat" min="-90" max="90" step="0.000001">
    <input type="number" ng-model="searchLng" name="searchLng" min="-180" max="180" step="0.000001">
    <span use-form-error="textOrLocation" use-error-expression="textOrLocationValid()" use-error-input="myForm"></span>
  </div>
  {{myForm.$error}}
  <br>
  <button ng-disabled="myForm.$invalid">Submit</button>
</div>

And JS

angular.module('ExampleApp', ['use']).controller('ExampleController', function($scope) {
$scope.search = {
  mode: 'textOrLocation'
};
$scope.textOrLocationValid = function() {
  var usingTextOrLocation = $scope.search.mode == 'textOrLocation';
  if (usingTextOrLocation) {
    var textModel = $scope.myForm.searchText || {},
      textValid = textModel.$valid,
      textValue = textModel.$modelValue,
      latModel = $scope.myForm.searchLat || {},
      latValid = latModel.$valid,
      latValue = latModel.$modelValue,
      lngModel = $scope.myForm.searchLng || {},
      lngValid = lngModel.$valid,
      lngValue = lngModel.$modelValue,
      formValid = (textValid && latValid && lngValid) && // No invalid fields
      ((latValue && 1 || 0) + (lngValue && 1 || 0) != 1) && // Either both lat and long have values, or neither do
      (textValue || latValue); // Either text or location are filled out
    return !formValid;
  } else {
    // Explicitly set form validity to true because form is not active
    return false;
  }
}

});

Live example on jsfiddle:

Upvotes: 1

oKonyk
oKonyk

Reputation: 1476

I got it working, you can find code in this Plunker

There were several problems (I'm making assumption, you are trying handling your form in controller, not in custom directive):

  1. $scope.form that we are trying to access in controller is not same form that we have in the view. Form gets its own scope. which is not directly accessible in controller. To fix this, we can attach form to $scope.forms - object, that is declared in controller (read more on inheritance patterns here)
  2. we should attach ng-model to inputs, so we can $watch them, since it not possible to watch form directly (read more here)
  3. in addition, we have to watch $scope.search changes.

It's definitely not the most elegant solution to handle custom validation... Will try to come up with custom directive for that.

Upvotes: 0

Andrew Tomlinson
Andrew Tomlinson

Reputation: 181

My understanding is that because the function is being watched, it's actually being evaluated by Angular periodically during each digest. A simple solution might be to set the validity of textOrLocation to true when that particular form is not in focus. This would allow the button state to depend on the validity of the field in the id form.

function textOrLocationValid() {
    var usingTextOrLocation = $scope.search.mode == 'textOrLocation';
    if (usingTextOrLocation) {
        var textModel = $scope.form.searchText || {},
            textValid = textModel.$valid,
            textValue = textModel.$modelValue,
            latModel = $scope.form.searchLat || {},
            latValid = latModel.$valid,
            latValue = latModel.$modelValue,
            lngModel = $scope.form.searchLng || {},
            lngValid = lngModel.$valid,
            lngValue = lngModel.$modelValue,
            formValid = (textValid && latValid && lngValid) && // No invalid fields
            ((latValue && 1 || 0) + (lngValue && 1 || 0) != 1) && // Either both lat and long have values, or neither do
            (textValue || latValue); // Either text or location are filled out
        if (formValid) {
            // Explicitly set form validity to true
            $scope.form.$setValidity('textOrLocation', true);
        } else {
            // Explicitly set form validity to false
            $scope.form.$setValidity('textOrLocation', false);
        }
    }
    else{
        // Explicitly set form validity to true because form is not active
        $scope.form.$setValidity('textOrLocation', true);
    }
}

Upvotes: 1

Related Questions