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:
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.
The FormController
uses the specified controller's $name
property to register the control:
form[control.$name] = control;
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;
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:
Do not set a name
attribute, so nothing is registered with myForm
(we will take care of the registering manually).
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
).
Since the controls are registered to the FormController
through their ngModelController
, our directive must get access to those two controllers (through its require
property).
Before registering the control, our directive will update the ngModelController
's $name
property (and set a name on the element).
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.