Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
850 views
in Technique[技术] by (71.8m points)

angularjs - Issue registering form control with interpolated name

I have the following code attempting to apply some validation styling using ng-class.

<tr ng-repeat="item in items">
  <td>
    <select name="itemName{{$index}}" ng-model="item.name" ng-options="o for o in nameOptions"
      ng-class="{ 'custom-error': myForm.itemName{{$index}}.$invalid && !myForm.itemName{{$index}}.$pristine }"
      required>
        <option value="">SELECT</option>
    </select>
  </td>
</tr>

My class is not being applied and I cannot see why. The generated html looks as expected with the name matching its counterpart in the ng-class expression.

When I replicate this outside of an ng-repeat (not using $index for naming) it works as expected.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

This happens because the control's name (the one with which it is registered on its parent form) is retrieved during the ngModelController's instantiation, which according to the docs takes place before the pre-linking phase* (so no interpolation yet).

If you inspect the myForm's properties, you will find out it indeed has a property with key "itemName{{$index}}".


*UPDATE

The docs on $compile are a great resource for understanding what makes a directive tick and what is going on "behind the scenes".

In plain words there are two main phases: the compiling phase and the linking phase.

During the compiling phase, the template is being prepared (e.g. it might need to be cloned etc) and is made Angular-aware (e.g. the directives are compiled and the expressions are parsed and ready to be evaluated), but it is not bound to a scope yet (thus there is nothing to evaluate against).

The compile function deals with transforming the template DOM. Since most directives do not do template transformation, it is not used often. Examples that require compile functions are directives that transform template DOM, such as ngRepeat, or load the contents asynchronously, such as ngView.

The linking phase is further devided into two sub-phases: pre-linking and post-linking.

During this phase, a scope comes into play and the interpolated expressions (such as your name attribute) can be evaluated against the scope's properties/functions.

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.

Pre-linking function
Executed before the child elements are linked. Not safe to do DOM transformation since the compiler linking function will fail to locate the correct elements for linking.

Post-linking function Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function.


So, in your case, here is what happens:

  1. The ngModel directive, which is responsible for registering the element on its parent form's FormController, calls formCtrl.$addControl(modelCtrl); in its post-linking function.

  2. The FormController uses the specified controller's $name property to register the control:
    form[control.$name] = control;

  3. In the case of ngModel the controller is an instance of ngModelCntroller and it's $name property is defined like this:
    function(..., $attr, ...) { ... this.$name = $attr.name;

  4. Since the controller is instantiated before the pre-linking phase, $attr.name is bound to the un-interpolated string (i.e. "itemName{{$index}}").


UPDATE 2

Now that we know what the issue is, it only seems logical to go ahead and fix it :)

Here is an implementation that would solve the issue:

  1. Do not set a name attribute, so nothing is registered with myForm (we will take care of the registering manually).

  2. Create a directive that registers the control with the parent form's FormController only after evaluating the expression against the element's scope (let's call the directive later-name).

  3. Since the controls are registered to the FormController through their ngModelController, our directive must get access to those two controllers (through its require property).

  4. Before registering the control, our directive will update the ngModelController's $name property (and set a name on the element).

  5. Our directive must also take care of removing the control "manually" (since we are adding it manually).

Easy as pie:

<select later-name="itemName{{$index}}"                  <!-- (1) -->

app.directive('laterName', function () {                   // (2)
    return {
        restrict: 'A',
        require: ['?ngModel', '^?form'],                   // (3)
        link: function postLink(scope, elem, attrs, ctrls) {
            attrs.$set('name', attrs.laterName);

            var modelCtrl = ctrls[0];                      // (3)
            var formCtrl  = ctrls[1];                      // (3)
            if (modelCtrl && formCtrl) {
                modelCtrl.$name = attrs.name;              // (4)
                formCtrl.$addControl(modelCtrl);           // (2)
                scope.$on('$destroy', function () {
                    formCtrl.$removeControl(modelCtrl);    // (5)
                });
            }            
        }
    };
});

See, also, this short demo.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...