Why is $timeout needed when dynamically nesting ng

2019-06-05 18:32发布

问题:

I cannot seem to avoid the need to generate dynamic sub forms in the application I am working on. The sub form is working as expected and the sub form shows $invalid=true when one or more of it's inputs are invalid. The parent form however has $invalid=false.

I have seen people achieve nested forms where invalid sub forms invalidate the parent form, but I can't seem to do it dynamically without wrapping the dynamic compiling of the sub form in a $timeout.

See Plunker HERE

In the above link I have recreated the scenario. I have three forms. The parent form, a sub form created at the same time as the parent form, and a dynamically created sub form.

If you clear the bottom existing sub form's input, it will invalidate the parent form (parent form turns red).

If you clear the top dynamic form's input, it will not invalidate the parent form (parent form remains green).

It will begin to work if you stick the addForm method in a $timeout:

// WORKS! : When you delete the dynamic added sub form input
// the parent form also becomes invalid
//timeout(addForm,0); 

// FAILS! : When you delete the dynamic added sub form input text
// the parent form does NOT become invalid
addForm();

It's great that I have a workaround, but I would like to understand why I need the $timeout and if there is a solution that avoids the use of a $timeout.

回答1:

As Michael said, the correct place to do any DOM manipulations is the link function and not the controller function of the directive. Some extra information on why what you already have does / does not work depending on the use of $timeout:

According to the Angular documentation of the $compile service for directive definitions the controller

is instantiated before the pre-linking phase

while the link function

is executed after the template has been cloned

You can observe this yourself if you include a link function in your directive and write two console.log statements, one in the controller function and one in the link function. The link function is always executed after the controller. Now, when you include addForm(); in your controller this will be executed at the time the controller is instantiated, ie. before the linking phase, at which time, as it is mentioned in the documentation it is

not safe to do DOM transformation since the compiler linking function will fail to locate the correct elements for linking.

On the other hand, if you call the addForm() function in the $timeout, this will actually be executed after the linking phase, since a $timeout call with a zero timeout value causes the code in the timeout to be called in the next digest cycle, at which point the linking has been performed and the DOM transformation is performed correctly (once again you can see the timing of all these calls by adding console.logs in appropriate places).



回答2:

DOM manipulations should be done in the link phase and not in the controller. See $compile

The link function is responsible for registering DOM listeners as well as updating the DOM. It is executed after the template has been cloned. This is where most of the directive logic will be put.

Detailed explanation:

The problem lies in the the angular FormController. At intialization it will look for a parent form controller instance. As the sub form was created in the controller phase - the parent form initialization has not been finished. The sub form won't find it's parent controller and can't register itself as a sub control element.

FromController.js

//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
function FormController(element, attrs, $scope, $animate, $interpolate) {
  var form = this,
      controls = [];

  var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;

Plunker



回答3:

Usually altering dom elements inside of a controller is typically not ideal. You should be able achieve what you are looking for without the need for '$compile' and make things a bit easier to handle if you introduce a second directive an array of items to use 'ng-repeat'. My guess is that $timeout() is working to signal angular about the new elements and causes a digest cycle to handle the correct validation.

var app = angular.module('app',[]);
app.directive('childForm', function(){
  return{
    restrict: 'E"',
    scope:{
      name:"="
    },
    template:[
                '<div ng-form name="form2">',
                    '<div>Dynamically added sub form</div>',
                    '<input type="text" name="input1" required ng-model="name"/>',
                    '<button ng-click="name=\'\'">CLEAR</button>',
                '</div>'
              ].join('')

  }
});
app.directive('myTest', function() {

    return {

        restrict: 'E',

        scope: {},

        controller: function ($scope, $element, $compile, $timeout) {
          $scope.items = [];
          $scope.items.push({
            name:'myname'
          });
          $scope.name = 'test';
            $scope.onClick = function () {
                console.log("SCOPE:", $scope, $childScope);
            };
            $scope.addItem = function(){
              $scope.items.push({name:''});
            }
        },

        template: [
            '<div>',
              '<div>Parent Form</div>',
              '<div ng-form name="form1">',

                  '<div class="form-container">',
                    '<div ng-repeat="item in items">',
                      '<child-form/ name="item.name">',
                    '</div>',
                  '</div>',

                  '<div>Existing sub form on parent scope</div>',
                  '<div ng-form name="form3">',
                    '<input type="text" name="input2" required ng-model="name"/>',
                    '<button ng-click="name=\'\'">CLEAR</button>',
                  '</div>',
              '</div>',
              '<button ng-click="addItem()">Add Form</button>',
              '<button ng-click="onClick()">Console Out Scopes</button>',
            '</div>'
        ].join('')
    };
});

Updated plunkr