I'm trying to build a directive around an input element that responds when the model is dirtied or touched. The required ngModel seems to reflect the changes in the value and view of the input model, but none of the other attributes.
I'm suspecting it has something to do with the fact that I'm including ng-model in two elements, but I haven't figured out how to use it just once.
Ideally, I would like something that is created like this:
<input test-directive label="'My Label'" type="text" ng-model="testObject.text"/>
And results in something like:
<label>
<div>My Label</div>
<input ng-model="testObject.text" ng-blur="input.focus=false" ng-focus="input.focus=true"/>
Focused: true (input.focus)
Pristine: false (ngModel.$pristine)
</label>
Here is what I have so far: fiddle
<div test-directive ng-model="testObject.text" l="'Test Input'" f="testObject.focus">
<input type="text" ng-model="testObject.text" ng-blur="testObject.focus=false" ng-focus="testObject.focus=true" />
</div>
The directive watches ngModel.
app.directive('testDirective', ['$compile',
function ($compile) {
'use strict';
return {
restrict: 'A',
require: "ngModel",
scope: {
l: '=',
f: '='
},
link: function (scope, element, attr, ngModel) {
var input = element.find('input');
scope.$watch(function () {
return ngModel;
}, function (modelView) {
scope.modelView = modelView
});
},
template:
'<div>' +
'<label>' +
'{{l}}' +
'<div class="iwc-input" ng-transclude></div>' +
'</label>' +
'focus: {{f}}' +
'<pre>{{modelView|json}}</pre>' +
'</div>',
transclude: true,
replace: false
};
}]);
I found that in Angular it is rather complex to have a directive "self-wrap", while still having other directives work properly alongside it. So, the answer below works, and I will try to explain why it is more complicated than it ought to be.
There are a number of ways to approach this. I will use the approach with transclude: "element"
- this transcludes the entire element and allows you to place it anywhere (including wrapping).
.directive("wrapper", function($compile){
return {
scope: { l: "@" },
transclude: "element",
require: ["ngModel"],
link: function(scope, element, attrs, ctrls, transclude)
scope.ngModel = ctrls[0];
// transclude: "element" ignores the template property, so compile it manually
var template = '<label ng-class="{dirty: ngModel.$dirty}">{{l}}: \
<placeholder></placeholder>\
</label>';
$compile(template)(scope, function(prelinkedTemplate){
transclude(function (clonedElement){
prelinkedTemplate.find("placeholder").replaceWith(clonedElement);
// element here is only a comment after transclusion
// so, need to use .after() - not .append()
element.after(prelinkedTemplate);
});
})
}
})
So, the above compiles the template and links against the isolate scope (where $scope.l
and $scope.ngModel
are available), and then trascludes the element and replaces the <placeholder>
.
That should have been enough, but there is a problem. When Angular compiled our directive, the element has been transcluded and is now a comment <!-- wrapper -->
, and not <input>
- this is what ngModel
directive "sees" in its prelink function, so things start to break.
To fix, our directive needs to have a higher priority than ngModel
(which is 1), and in fact, higher priority than ngAttributeDirective
(which is 100) for things like ng-maxlength
to work. But if we did that, then we could not just require: "ngModel"
, since it would not yet be available at our priority level.
One way to fix this is to make 2 passes - one with higher priority and one with lower. The lower priority pass will "hang" the captured ngModel
controller on to the directive's controller. Here's how:
// first pass
app.directive("wrapper", function($compile) {
return {
priority: 101,
scope: {
l: "@"
},
transclude: "element",
controller: angular.noop, // just a noop controller to attach properties to
controllerAs: "ctrl", // expose controller properties as "ctrl"
link: function(scope, element, attrs, ctrls, transclude) {
// notice the change to "ctrl.ngModel"
var template = '<label ng-class="{dirty: ctrl.ngModel.$dirty}">{{l}}: \
<placeholder></placeholder>\
</label>';
$compile(template)(scope, function(prelinkedTemplate) {
transclude(function(clonedElement) {
prelinkedTemplate.find("placeholder").replaceWith(clonedElement);
element.after(prelinkedTemplate);
});
});
}
};
})
// second pass
.directive("wrapper", function($compile) {
return {
priority: -1,
require: ["wrapper", "ngModel"],
link: function(scope, element, attrs, ctrls, transclude) {
var wrapperCtrl = ctrls[0],
ngModel = ctrls[1];
// "hang" ngModel as a property of the controller
wrapperCtrl.ngModel = ngModel;
}
};
});
There are other approaches as well. For example, we could have made this directive with very high priority (say, priority: 10000
) and terminal: true
. Then, we could then take the element, wrap it, apply another directive that has require: "ngModel"
to actually keep track of $pristine
, $touched
, etc... and recompile the contents (not forgetting to remove the original directive to avoid an infinite loop).