Getting form controls from FormController
Asked Answered
S

4

31

I need a way to loop through the registered controls of an AngularJS form. Essentially, I'm trying to get all the $dirty controls, but there's no array of the controls (the FormController has a number of different properties/functions in addition to containing the controls themselves - each as its' own object).

I've been looking at the source code, and I see that there is a controls array in the FormController that is exactly the array I'm looking for. Is there a way to get access to this value, or extend the FormController to include a function that returns this controls array?

Edit: Plnkr demo

Also, I realized that technically I could check the first character in the key string for "$", but I'd like to avoid that in case the FormController/directive changes in a future version of Angular.

Edit 2: Another clarification: My goal in all of this is to be able to determine which specific fields are $dirty, whether by looping through the entire list of controls (not including the $dirty, $invalid, $error, $name, and other properties that live in the Form object as it is) or by extending the FormController and creating a function that returns only the controls which are currently dirty (and not equal to their starting values)

Edit 3: The solution I'm looking for needs to be applicable to forms/models of different structures. The models on the scope are generated via AJAX, so their structure is already set (I'd like to avoid having to hardcode a new structure for all the data I'm already receiving via AJAX). Also, I'm looking to use this form submission process across multiple forms/models, and each of them have differing JSON structures - as they apply to different entities in our Object Model. This is why I've chosen to ask for a way to get access to the controls object in the FormController (I'll post the code from FormController below), because it's the only place where I can get a flat array of all of my fields.

function FormController(element, attrs) {


var form = this,
      parentForm = element.parent().controller('form') || nullFormCtrl,
      invalidCount = 0, // used to easily determine if we are valid
      errors = form.$error = {},
      controls = [];

  // init state
  form.$name = attrs.name || attrs.ngForm;
  form.$dirty = false;
  form.$pristine = true;
  form.$valid = true;
  form.$invalid = false;

  parentForm.$addControl(form);

  // Setup initial state of the control
  element.addClass(PRISTINE_CLASS);
  toggleValidCss(true);

  // convenience method for easy toggling of classes
  function toggleValidCss(isValid, validationErrorKey) {
    validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
    element.
      removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
      addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
  }

  /**
   * @ngdoc function
   * @name ng.directive:form.FormController#$addControl
   * @methodOf ng.directive:form.FormController
   *
   * @description
   * Register a control with the form.
   *
   * Input elements using ngModelController do this automatically when they are linked.
   */
  form.$addControl = function(control) {
    controls.push(control);

    if (control.$name && !form.hasOwnProperty(control.$name)) {
      form[control.$name] = control;
    }
  };

  /**
   * @ngdoc function
   * @name ng.directive:form.FormController#$removeControl
   * @methodOf ng.directive:form.FormController
   *
   * @description
   * Deregister a control from the form.
   *
   * Input elements using ngModelController do this automatically when they are destroyed.
   */
  form.$removeControl = function(control) {
    if (control.$name && form[control.$name] === control) {
      delete form[control.$name];
    }
    forEach(errors, function(queue, validationToken) {
      form.$setValidity(validationToken, true, control);
    });

    arrayRemove(controls, control);
  };

  // Removed extra code
}

As you can see, the form itself has the controls array privately available. I'm wondering if there's a way for me to extend the FormController so I can make that object public? Or create a public function so I can at least view the private array?

Sericin answered 3/1, 2014 at 16:21 Comment(4)
Do you not have models associated with the controls? You could just watch the models to see if they've changed rather than looping through the form controls.Schmeltzer
I do have models, I'm just also trying to avoid doing watches because there are already watches in the Form directive to handle all of the dirty checking, and for large forms this can perform slowly :(Sericin
Nice question, it's the same I am looking for (my goal is to stop validation on submit after first error shows). In this time, have you found an alternative solution, better than checking for first char ($)?Sectarian
Unfortunately I'm still using that to check. If I find a better way, I'll update the post.Sericin
L
20

For a direct solution to the question, modify @lombardo's answer like so;

     var dirtyFormControls = [];
     var myForm = $scope.myForm;
     angular.forEach(myForm, function(value, key) {
         if (typeof value === 'object' && value.hasOwnProperty('$modelValue') && value.$dirty)
             dirtyFormControls.push(value)                        
     });

The array 'dirtyFormControls' will then contain the form controls that are dirty.

You can also use this trick to show error messages on form submission for 'Required' validations and all others. In your submit() function you will do something like;

 if (form.$invalid) {
     form.$setDirty();              
     angular.forEach(form, function(value, key) {
         if (typeof value === 'object' && value.hasOwnProperty('$modelValue'))
             value.$setDirty();                        
     });
    //show user error summary at top of form.
     $('html, body').animate({
         scrollTop: $("#myForm").offset().top
     }, 1000);
     return;
 }

And in your form you will show error messages with

    <span ng-messages="myForm['subject-' + $index].$error" ng-show="myForm['subject-' + $index].$dirty" class="has-error">
        <span ng-message="required">Course subject is required.</span>
    </span>

The above solution is useful when you have dynamically generated controls using 'ng-repeat' or something similar.

Lizarraga answered 6/12, 2015 at 18:44 Comment(4)
I totally forgot I had submitted this question, and ended up doing something very similar to what you posted here. I actually ended up extending the ngSubmit directive so that it runs all of my validation code for ALL of my <form> elements that have ngSubmit functions.Sericin
I hope that solved your issue without getting in your way! Using directives is probably the best way of doing custom stuff in AngularJS, but there are lots of built in stuffs that can be taken advantage of.Lizarraga
Just take care of ng-form. This method does not handle them.Tubman
I believe this solution works only for controls that have the "name" attribute set.Skiplane
M
11

You can use the following code to iterate the controls:

    var data = {};
    angular.forEach(myForm, function (value, key) {
        if (value.hasOwnProperty('$modelValue'))
            data[key] = value.$modelValue;
    });
Meagan answered 20/4, 2014 at 5:28 Comment(2)
I had to add typeof value === 'object' && to your if condition. Otherwise you can get errors like "TypeError: Cannot read property 'hasOwnProperty' of undefined"Orian
@BryanLarsen Better use angular.isObject(value) for this. It also checks for null values, because typeof null is also an object.Discounter
S
2

try simply with from within your controller:

$scope.checkForm = function(myFormName){
     console.log(myFormName.$invalid);
}

UPDATE:

<div ng-controller="MyController">
                <form name="form" class="css-form" novalidate>
                    <input type="text" ng-model="user.uname" name="uname" required /><br />
                    <input type="text" ng-model="user.usurname" name="usurname" required /><br />
                    <button ng-click="update(form)">SAVE</button>
                </form>
              </div>

app.controller('MyController',function($scope){
                $scope.user = {};
                $scope.update = function (form){
                    var editedFields = [];
                    angular.forEach($scope.user, function(value, key){
                        if(form[key].$dirty){
                           this.push(key + ': ' + value); 
                        }

                    }, editedFields);
                    console.log(editedFields);
                }
        });
Sedition answered 3/1, 2014 at 16:33 Comment(2)
I don't think my question was clear, so I added a plnkr demo. I don't have any problems detecting if the form is $dirty or $invalid. My problem is that I don't have a good way of looping through all of the form controls (the fields themselves) to determine which specific fields are $dirty. My main goal is to submit only the fields that have been edited.Sericin
This is pretty much what I've got at the moment, but my problem is that I'm having to iterate over an unstructured/multi-dimensional JSON object in my scope, which is why I'd prefer to be able to loop through the flat array of fields that's in the FormControllerSericin
S
0

I've come up with a somewhat decent solution, but it still isn't what I was looking for. I've salvaged some code from another problem involving creating JSON objects from strings, and come up with the following:

Essentially I'm naming my fields in the same way they're tied to the model, and then building a new object for submission when the form_submit is called.

Plnkr demo

In the demo, if you change either of the form fields, then hit submit, you'll see the object pop up with only the dirty values.

Sericin answered 3/1, 2014 at 17:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.