Re-evaluate directive attribute expression - angularjs

I'm having troubles in re-evaluating an expression passed as attribute of a custom angular (1.2.28) directive.
I've tried all the possible combination of $eval, $parse as well as isolated and non-isolated scope. I can't wrap my mind around this.
I have something like this:
<div ng-repeat="item in dataset">
<my-directive
show-tooltip="user.level=='visitor' && item.memberOnly"
content-tooltip="isAdded(item) && 'Remove Me' || 'Add Me'">
<my-directive>
</div>
The problem is that user.level can change because for example the user logged in and the (enclosing) scope function isAdded can returns different values depending if the items was already added to a list or not.
The directive:
angular.module("MyModule", [])
.directive("myDirective", () {
return {
restrict: 'E',
priority: 999,
link: function(scope, elm, attrs) {
showTooltip = scope.$eval(attrs.showTooltip);
contentTooltip = scope.$eval(attrs.contentTooltip);
// This works
scope.$watch(attrs.contentTooltip, function(value) {
if( value and value != contentTooltip)
contentTooltip = value
});
// This never works
scope.$watch(attrs.showTooltip, function(value) {
if( value and value != showTooltip)
showTooltip = value
});
// Do things..
}
}
});
I don't know why but the first watch will work, the second will never work. I've used a similar approach with $parse but couldn't get it to work either.
Maybe I'm doing this totally wrong

Look into using attrs.$observe instead:
attrs.$observe('showTooltip', function(){
})

Related

Removing element from DOM via ng-if bound to attribute directive scope property

I assumed this would be straightforward, but it's seemingly not!
I'm trying to create a generic attribute directive that will call a method in one of my services and conditionally cause the element in which it is placed to not be added to the DOM if the service method returns false. Basically, ng-if, but an ng-if that internally calls a service method and acts on that
Link to Plunker
I have an element containing an attribute directive: e.g
<p ng-if="visible" my-directive>Hi</p>
I set visible to true in the myDirective directive. I was expecting the <p> element to be removed from the DOM when visible was falsy and added to the DOM when it's truthy. Instead, the ng-if never seems to spot that visible has been set to true in the directive's link function and, hence, the <p> element never displays.
I wasn't 100% sure it would work since the directive is removing the element on which it exists, bit of a catch 22 there.
I've spent far too long on this and have so far tried (unsucessfully):
Adding an ng-if attribute in the link function via these two methods
attr.ngIf = true;
element.attr('ng-if', true);
Changing the ng-if in the <p> to ng-show, thereby not removing the element (which I really want to do)
I'm wondering if it's something as simple as scope? Since the ng-if is bound to a property of the <p> element, is setting visible in the directive scope setting it on the same scope?
On the other hand, I may be drastically over-simplifying, I have a nasty feeling I may have to consider directive compilation and transclusion to get a solution for this.
Does anyone have any feel for where I might be going wrong?
tldr: apparently you want your directive to be self-contained and it should be able to remove and add itself to the DOM. This is possible and makes the most sense via isolated scope or manual manipulation of the DOM (see below).
General
When you do <p ng-if="visible" my-directive>Hi</p> angular looks for the visible on the current scope, which is the parent scope of the directive. When visible is defined, the directive is inserted in the DOM, e.g. taken from your plunker
<body ng-controller="MainCtrl">
<p my-directive="showMe" ng-if="visible">I should be shown</p>
</body>`<br>
app.controller('MainCtrl', function($scope) {
$scope.visible = 3;
});
would make the directive being shown. As you defined an isolated scope on your directive
app.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
link: function(scope, element, attr, ctrl) {
scope.visible = (scope.myDirective == 'showMe') ? true : false;
}
}
});
scope.visible in the directive does not affect the visible taken into account for ngIf.
Child Scope
You could define a child scope to get access to the parent scope. If you do that, you can actually affect the right visible property, but you have to put it on an object so that the directive can follow the scope prototype chain.
<body ng-controller="MainCtrl">
<p my-directive ng-if="visibleDirectives.directive1">I should be shown</p>
</body>
The $timeouts are there for demonstration purposes. Initially the ngIf has to evaluate to true else the directive is not being created at all.
app.controller('MainCtrl', function($scope) {
$scope.visibleDirectives = { directive1 : true };
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : true,
link: function(scope, element, attr, ctrl) {
console.log(scope);
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
}, 2000);
}, 2000);
}
}
});
Like this the directive has to know about the property that defines it's visibility beforehand (in this case scope.visibleDirectives.visible1), which is not very practical and prohibits several directives.
Isolated Scope
In your example you used an isolated scope. This allows reusing the directive. In order for the directive to be able to modify the appropriate property for ngIf you have to again give it the right reference.
<body ng-controller="MainCtrl">
<p my-directive="directive1" ng-if="directive1.visible">I should be shown</p>
</body>
Again you have to provide the property on an object so that the directive can follow the object reference to modify the right visible.
app.controller('MainCtrl', function($scope) {
$scope.directive1 = {
visible : true
};
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : {
myDirective : '='
},
link: function(scope, element, attr, ctrl) {
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
}, 2000);
}, 2000);
}
}
});
In these cases the directive gets recreated everytime ngIf evaluates to true.
Manual manipulation of the DOM
You can also just manually remove and append the node of the directive without consulting angular.
<body ng-controller="MainCtrl">
<p my-directive>I should be shown</p>
</body>
In this case you don't need the angular version of setTimeout and can even use a setInterval as the Interval is created only once, but you have to clear it.
app.controller('MainCtrl', function($scope) { });
app.directive('myDirective', function() {
return {
restrict: 'A',
scope : { },
link: function(scope, element, attr, ctrl) {
var el = element[0];
var parent = el.parentNode;
var shouldBeShown = false;
var interval = setInterval(function() {
var children = parent.children;
var found = false;
for(var i = 0; i < children.length; i++) {
if(children[i] === el) {
found = true;
break;
}
}
if(shouldBeShown) {
if(!found)
parent.appendChild(el);
}
else {
if(found)
parent.removeChild(el);
}
shouldBeShown = !shouldBeShown;
}, 2000);
scope.$on('$destroy', function() {
clearInterval(interval);
});
}
};
});
If you want an element to be removed, use ng-show="visible" this will evaluate as a Boolean and show the element if it evaluates to true. Use "!visible" if you need to flip it.
Also, but adding the scope attribute to your directive you are adding an additional scope, think alternate timeline, that your controller scope that is tied to the page cannot see. That would explain why ng-show may not have worked for you before.

Scope variables are not binded in directive for jQuery plugin

I'm trying to bind 2 values from an input field to a scope variable. First is an input's value (color, written as text), and the second one is an attribute value (opacity value). I want them to change, and, as well, I want so their value be outputted.
myApp.directive( 'watchOpacity', function() {
return {
restrict: 'A',
link: function( scope, element, attributes ) {
scope.$watch( attributes.opacity, function(value) {
console.log( 'opacity changed to', value );
});
}
};
})
Plunker demo
The problem is that neither the input's value, nor the attribute's value is displayed/binded.
Use the change callback jQuery MiniColors provides. It will have hex and opacity passed in as arguments, which you can use to set your scope.data properties.
You need to wrap the setting of those properties in a scope.$apply callback to ensure a digest cycle is run afterwards, so that your view is updated:
.directive( 'watchOpacity', function($timeout) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
watchOpacity: '='
},
link: function( scope, element, attributes, ngModel ) {
$timeout(function(){
element.attr('data-opacity', scope.watchOpacity);
$(element).minicolors({
opacity: true,
defaultValue: ngModel.$modelValue || '',
change: function(hex, opacity) {
ngModel.$setViewValue(hex);
scope.$apply(function() {
scope.watchOpacity = opacity;
})
}
});
});
}
}
})
Using this directive, your view would look like this (ng-init is optional depending upon whether or not you require default values or if you've placed them in the controller):
<input type="text" watch-opacity="data2.opacity" ng-model="data2.color"
ng-init="data2.color = '#0000FF'; data2.opacity = 0.5;" />
Working fork of your demo
Firstly : You've defined data-opacity in your element , while you want to use it as attributes.opacity , which is not even logically correct
Secondly : in your directive , if you want to read the value of your current directive , I mean this :
watch-opacity="data.opacity"
You need to say something like this in your directive :
link:function(scope,element,attributes){
var data_opacity = attributes.watchOpacity// you have written attribute.opacity!
}
I dont know what you are going to do :(

Angular Directive attrs.$observe

I found this Angular Directive online to add a twitter share button. It all seems staright forward but I can't work out what the attrs.$observe is actually doing.
I have looked in the docs but can't see $observe referenced anywhere.
The directive just seems to add the href which would come from the controller so can anyone explain what the rest of the code is doing?
module.directive('shareTwitter', ['$window', function($window) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.share = function() {
var href = 'https://twitter.com/share';
$scope.url = attrs.shareUrl || $window.location.href;
$scope.text = attrs.shareText || false;
href += '?url=' + encodeURIComponent($scope.url);
if($scope.text) {
href += '&text=' + encodeURIComponent($scope.text);
}
element.attr('href', href);
}
$scope.share();
attrs.$observe('shareUrl', function() {
$scope.share();
});
attrs.$observe('shareText', function() {
$scope.share();
});
}
}
}]);
Twitter
In short:
Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.
From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)
$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.
From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)
$compile.directive.Attributes#$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.
<input type="text" ng-model="value" >
<p sr = "_{{value}}_">sr </p>
.directive('sr',function(){
return {
link: function(element, $scope, attrs){
attrs.$observe('sr', function() {
console.log('change observe')
});
}
};
})

How to prefill a calculation form with initial values?

I've a long calculation form and want to prefill some values for displaying initial results. These initial values should live in the template to have these easily editable. It seems, the angular way is to use a directive which reads the input value fields and initializes the app with these values.
Here is one way to to set the model value from the input field:
<input name="card[description]" value="Visa-4242" ng-model="card.description" ng-initial>
Coffeescript:
app = angular.module 'forms', []
app.directive 'ngInitial', ->
restrict: 'A'
controller: ['$scope', '$element', '$attrs', '$parse', ($scope, $element, $attrs, $parse) ->
val = $attrs.sbInitial || $attrs.value
getter = $parse($attrs.ngModel)
setter = getter.assign
setter($scope, val)
]
Source: Angular js init ng-model from default values
Unfortunately this doesn't work in my app. The values are displayed but the results aren't calculated. I have to manual fill out the fields in the browser to start the calculation.
The input fields are watched in my controller, like so:
$scope.$watch(
'inputValues.permeatCapacity',
function (newValue, oldValue) {
if (newValue === oldValue) {
return;
}
permeatCapacity = $scope.inputValues.permeatCapacity;
permeatPerDay = permeatFactory.getPermeatPerDay(permeatCapacity);
$scope.permeatPerDay = $filter('number')(permeatPerDay, 2);
}
);
What's the best directive for this problem or is there a better way in Angular.js?
UPDATE
I've just found a really dirty way in the controller to update the input fields after initialization that make use of the bound calculations. It not only feels like an bad idea, the initial values also don't live in the template:
$timeout(function () {
$scope.inputValues.overallRecovery = 40;
$scope.inputValues.permeatCapacity = 7.2;
}, 0);
In comparison, this initial object in my controller fills out the fields but doesn't trigger the watchers (important for the bound calculations):
$scope.inputValues = {
overallRecovery: 40,
permeatCapacity: 7.2
};
But there is a better way to do this, isn't it?
Ok, here's what I found out. The condition:
if (newValue === oldValue) {
return;
}
in my watch methods blocked the initial calculation. I can now use a really simple directive for my usecase:
An input field in the template:
<input type="text" id="overall-recovery" value="40" ng-model="inputValues.overallRecovery" initial-value>
The directive:
angular.module('ksbApp')
.directive('initialValue', function ($compile) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var initialValue = attrs.value;
ngModel.$setViewValue(initialValue);
}
};
});
In this case, the value "40" is pushed to the model during initialization.
Is there anything I could do better?
Not sure why you would need a directive for this, unless I dont understand the question fully. Spin up the model in scope, setting the values where on the controller, and then bind them using ngmodel.
in the controller:
scope.inputValues = {permeatCapacity: 1};
scope.calc = function(){
return permeatFactory(scope.inputValues.perMeatCapacity);
};
in the view:
<input ng-model="inputValues.permeatCapacity" type="text">
<button ng-click="calc()" />
In later versions of angular you can even have the calculation run AS the values in the model change using ng-change.

AngularJS: writing a capatilize directive does not seem to work

I am new to AngularJS so if i'm completely off track i apologize.
Template (which is iterated):
<div class="title">
<input ng-model="menuItem.kind" capitalize-first />
</div>
Directive (to capitalize):
angular.module('phonecat', []).directive('capitalizeFirst', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var capitalize = function(inputValue) {
var capitalized = inputValue.charAt(0).toUpperCase() +
inputValue.substring(1);
if(capitalized !== inputValue) {
modelCtrl.$setViewValue(capitalized);
modelCtrl.$render();
}
return capitalized;
}
modelCtrl.$parsers.push(capitalize);
capitalize(scope[attrs.ngModel]); // capitalize initial value
}
};
});
The result is a blank span. Any ideas?
You can't use scope[attrs.ngModel]. AngularJs will not set scope['menuItem.kind'] = ..., but scope.menuItem.kind = ....
Additionally, the value might had been resolved to a parent scope. So your best choice is to use ngModel.$modelValue to get the current model value of ngModel (capitalize(modelCtrl.$modelValue);).
This leads to another problem. You don't have this value in linking phase, it's lightly async. To workaround it, all you need is to inject $timeout and run this code asynchronously:
$timeout(function() {
capitalize(modelCtrl.$modelValue);
}, 0);
So, summarizing, all you need is to change your directive last line to the one above, and this will solve the problem. All the rest is ok, take a look at this Plnkr.

Resources