Reputation: 109
I'm trying to programmatically toggle tooltips (like mentioned here: https://stackoverflow.com/a/23377441) and got it fully functional except for one issue. In order for it to work I must have tooltip-trigger and tooltip attributes hardcoded as follows:
<input type="text" tooltip-trigger="show" tooltip="" field1>
In my working directive, I'm able to change the tooltip attributes and trigger a tooltip, but if I try to leave those two attributes out and attempt to set them dynamically, ui-bootstrap doesn't pick them up and no tooltip gets displayed.
html
<input type="text" field2>
js
myApp.directive('field2', function($timeout) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('errors', function() {
var id = "field2";
if (scope.errors[id]) {
$timeout(function(){
// these attrs dont take effect...
attrs.$set('tooltip-trigger', 'show');
attrs.$set('tooltip-placement', 'top');
attrs.$set('tooltip', scope.errors[id]);
element.triggerHandler('show');
});
element.bind("click", function(e){
element.triggerHandler('hide');
});
}
});
},
};
});
I'd prefer not to hardcode these attributes in the html, so how do I go about setting these attributes dynamically and get ui-bootstrap to pick them up?
Here is a plunker that has a working (field1) and non working (field2) directive: http://plnkr.co/edit/mP0JD8KHt4ZR3n0vF46e
Upvotes: 3
Views: 6277
Reputation: 17387
You can do this, but you have to change a couple of things in your approach.
Directive
app.directive("errorTooltip", function($compile, $interpolate, $timeout) {
return {
scope: true,
link: function($scope, $element, $attrs) {
var errorObj = $attrs.errorTooltip;
var inputName = $attrs.name;
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var content = startSym+errorObj+'.'+inputName+endSym;
$element.attr('tooltip-trigger', 'show');
$element.attr('tooltip-placement', 'top');
$element.attr('tooltip', content);
$element.removeAttr('error-tooltip');
$compile($element)($scope);
$scope.$watch(errorObj, function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true);
$element.on('click', function(e){
$element.triggerHandler('hide');
});
}
};
});
Okay, so from the top: @Travis is correct in that you can't just inject the attributes after the fact. The tooltip attributes that you place on the element are directives themselves, so the tooltip needs to be compiled when it's appended. That's not a problem, you can use the $compile service to do this, but you need to do it just once for the element.
Also, you need to bind the tooltip text (the value given to the tooltip attribute) to an expression. I do that by passing in a concatenated value of $interpolate.startSymbol()
+ the scope value that you want to display (in the demo it is the fieldx property of the errors object) + the $interpolate.endSymbol()
. This basically evaluates to something like: {{error.field1}}
. I use the $interpolate service start and end symbols because it just makes the directive more componentized, so you can use it on other projects where you might have multiple frameworks and be using something other than double curly-braces for your Angular expressions. It's not necessary though and you could instead do: '{{'+errorObj+'.'+inputName+'}}'
. In this case, you don't have to add the $interpolate service as a dependency.
As you can see, to make the directive truly reuseable, rather than hard-coding the error field, I set the value given to the directive attribute to the name of the object that will be watched and use the input name value as the object property.
The chief thing you need to remember is that before you compile, you have to remove the error-tooltip attribute from the element because if you don't you'll wind up in an infinite loop and crash hard! Basically, the compile service is going to take the element that the directive is attached to and compile it with all of the attributes your directive added, if you leave the error-tooltip attribute, it's going to try and recompile that directive too.
Lastly, you can take advantage of the fact that the tooltip will not display if its text value is empty or undefined (see line 192). That means you only have to watch the errors object not the individual property on the error associated with the tooltip. Make sure that you set the equality operator on the $watch
to true, so that it will trigger if any of the object's properties are changed:
$scope.$watch('errors', function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true); //<--equality operator
In the demo, you can see the effect of changing the errors object. If you click the Set Errors button the tooltip will display for both the first and second inputs. Click the Change Error Values and the tooltip displays for the first and third inputs.
Add the directive to your markup by setting the value to be the name of the object that will contain all of the errors. Make sure to give the field a name attribute that corresponds to the property key name in the object that will contain the errors for that input, such as:
<input class="form-control" ng-model="demo.field1" name="field1" error-tooltip="errors" />
Upvotes: 6