Getting form controls from FormController

robert.bo.roth picture robert.bo.roth · Jan 3, 2014 · Viewed 28.6k times · Source

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?

Answer

faitha picture faitha · Dec 6, 2015

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.