Watching attributes in a custom directive - AngularJS - angularjs

Brief background: I'm trying to write a directive that will listen to my bootstrap dropdown menu's aria-expanded attribute, because I want to do something once its value becomes false. From what I understand, this is the "angularJS" way if you want to monitor class changes inside elements.
The aria-expanded class is in this img element. My directive's name is overlay-monitor:
<img ng-init="displayMainMenu()" overlay-monitor id="nav-burger" uib-dropdown-toggle ng-disabled="disabled" ng-click="sMainMenu=true; isSubMenu=resetMenu(); getLinks(); bStopPropagation=true;" src="img/burger.png">
What I really want it to do is to close the opaque overlay I have on the page if aria-expanded becomes false. But for now, I'm just trying to trigger an alert to see if I did it right:
app.directive('overlayMonitor', function () {
return {
restrict: 'A',
scope: { ariaExpanded: '#' },
link: function ($scope, element, attrs) {
if (element.attrs('aria-expanded') == "true") {
alert('directive');
}
}
}
});
When I tested it out, the alert didn't show. :(
What did I do wrong?
Please advise. Thank you!
P.S. I forgot to mention. We are not allowed to use jQuery. Thanks again for your replies!
Edit: After reading about $watch, I tried the following code:
app.directive('overlayMonitor', function () {
return {
restrict: 'A',
scope: { ariaExpanded: '#' },
link: function ($scope, element, attrs) {
$scope.$watch(function () {
if (!attrs.ariaExpanded) {
alert('false');
}
else {
alert('true');
}
});
}
}
});
Good news is that the alert popped up. Bad news is the alert only said "false". It never fired alert('true'). :/

You may use like this:
if (attrs.ariaExpanded) { // instead of element.attrs('..')
alert('directive');
}

The function link is executed once, when the rendering directives.
Therefore, to track changes in a variable you need to use a $watch.
Try following:
app.directive('overlayMonitor', function () {
return {
restrict: 'A',
scope: { ariaExpanded: '#' },
link: function (scope, element, attrs) {
scope.$watch(function(){ return attrs.ariaExpanded; },
function(val){
if (!val) {
alert('directive');
}
});
}
}
});

Related

Hiding a tooltip via the tooltip-is-open attribute doesn't work

I want to display a clickable element (a font awesome icon) that copies some data into the clipboard. When a click event occurs I also want to display a tooltip which should disappear once the cursor left (mouseleave) the element.
This element is a directive as I use it several times in my application.
Copying the data is not an issue at all, displaying the tooltip neither. However, it doesn't disappear when the mouse leaves the font awesome icon.
To fix this, first I set the scope.tooltipIsOpen to true and as expected it displayed the tooltips by default.
Then I put some $log.info in the code to see if the value was updated to false. It seems that the value is updated. I also checked if the events were triggered and they are. I assume that the view doesn't update as it should so the tooltip remains displayed. I eventually tried to put a scope.$apply() in the post function, without success.
Here is my directive :
app.directive('toClipboard',
['$log', 'ngClipboard',
function ($log, ngClipboard) {
function compile(element, attrs) {
return {
pre: function (scope, element, attrs) {
if (!attrs.tooltipPlacement) {
attrs.tooltipPlacement = 'auto top';
}
},
post: function (scope, element) {
scope.copy = ngClipboard.toClipboard;
// Tooltip hidden by default.
scope.tooltipIsOpen = false;
// Hiding tooltip.
element.on('mouseenter', function () {
scope.tooltipIsOpen = false;
});
// Hiding tooltip.
element.on('mouseleave', function () {
scope.tooltipIsOpen = false;
});
}
}
}
return {
restrict: 'A',
replace: true,
scope: {
'clipboardData': '#',
'tooltipPlacement': '#'
},
compile: compile,
templateUrl: 'elements/_span-clipboard.html'
};
}
]);
NB: ngClipboard is a service to copy data to clipboard.
Here is the associated HTML template:
<span>
<i class="fa fa-copy clickable"
uib-tooltip="Copied"
tooltip-placement="tooltipPlacement"
tooltip-is-open="tooltipIsOpen"
tooltip-trigger="'click'"
ng-click="copy(clipboardData)"></i>
</span>
Do you have any idea or any lead to solve this issue ?
Thanking you in advance,
Plunker : https://plnkr.co/edit/okzxdSz1VvbkycehMT2G?p=preview
I managed to get this works by wrapping my code in $timeout();. Here is the working code:
app.directive('toClipboard',
['$log', '$timeout', 'ngClipboard',
function ($log, $timeout, ngClipboard) {
function compile(element, attrs) {
return {
pre: function (scope, element, attrs) {
if (!attrs.iconClass) {
attrs.iconClass = 'fa-copy';
}
if (!attrs.iconColorClass) {
attrs.iconColorClass = 'text-primary';
}
if (!attrs.tooltipPlacement) {
attrs.tooltipPlacement = 'auto top';
}
},
post: function (scope, element) {
scope.copy = ngClipboard.toClipboard;
// Tooltips hidden by default.
scope.tooltipIsOpen = false;
// Hiding tooltips on mouseenter event.
element.on('mouseenter', function () {
$timeout(
function() {
scope.tooltipIsOpen = false;
}, 200
);
});
// Hiding tooltips on mouseleave event.
element.on('mouseleave', function () {
$timeout(
function() {
scope.tooltipIsOpen = false;
}, 200
);
});
}
}
}
return {
restrict: 'A',
replace: true,
scope: {
'iconClass': '#',
'iconColorClass': '#',
'clipboardData': '#',
'tooltipPlacement': '#'
},
compile: compile,
templateUrl: 'elements/_span-clipboard.html'
};
}
]
);
$timeout makes sure it runs within an $apply cycle I guess.

Define button click listener within isolate scope in angularjs

I want to create a directive as a component, such that its not dependent on any controllers as such.
I have been trying to find out how to get a button click listener defined. But couldnt suceed yet.
angular.module('nestedDirectives', [])
.directive("parent", function () {
function linker(scope, element, attribute, controllers) {
console.log("linker called");
element.on("click", function clicked(event) {
console.log("clicked");
console.dir(this);
element.prepend("<h1>Hello</h1>");
});
}
return {
restrict: 'A',
template: '<div><h5>An Item</h5><button ng-click="clicked()">Click Me</button></div>',
link: linker,
scope: {}
}
})
In the template, no matter what i click the element.on("click") would get called. I want to call a clicked() method when button is clicked.
Here is the Plunker for the same.
The link function gets the scope (an isolated scope in your case) as the first argument, so you can do something like:
.directive("parent", function () {
function linker(scope, element, attribute, controllers) {
console.log("linker called");
//add the "clicked" function to your scope so you can reference with ng-click="clicked" in your template
scope.clicked = function() {
console.log("clicked");
console.dir(this);
element.prepend("<h1>Hello</h1>");
};
}
return {
restrict: 'A',
template: '<div><h5>An Item</h5><button ng-click="clicked()">Click Me</button></div>',
link: linker,
scope: {}
};
});
Here is your updated plunkr http://plnkr.co/edit/7mlcSB4phPO5EdEQqTj0

Auto focus on latest input element

I was developing a module where I need to create some text input manually (on enter or button clicking ) and auto focus on that input right after it's appended to the list. So far the function seems to work but when I open the console log, the $digest already in progress error appears. Kind of weird but if I remove some $eval or $apply the code won't work.
Here's my plnk demo for your reference: Demo
function keyEnter($document) {
return {
restrict: "A",
scope: false,
link: function(scope, ele, attrs) {
ele.bind("keydown keypress", function(event) {
if (event.which === 13) {
scope.$apply(function() {
scope.$eval(attrs.keyEnter);
});
event.preventDefault();
}
});
}
}
}
function customAutofocus() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(function() {
return scope.$eval(attrs.customAutofocus);
}, function(newValue) {
if (newValue === true) {
element[0].focus();
}
});
}
};
}
I followed the auto focus from this thread, it doesn't show any error even when I applied the same logic. The only difference is I'm using angular 1.3 while his is 1.2
What should I do to improve the code to avoid those $digest error ? Any help is really appreciate, thanks in advance
I adapted your plunk, so it works.
have a look at the new directive:
function customAutofocus($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
//rember this gets run only only
//once just after creating the element, so I just neet to focus once, when
// this digest cycle is done!
$timeout(function() {
// use a timout to foucus outside this digest cycle!
element[0].focus(); //use focus function instead of autofocus attribute to avoid cross browser problem. And autofocus should only be used to mark an element to be focused when page loads.
}, 0);
}
};
}
This makes use of how angular works.

AngularJS UniformJS Select Control not updating

I'm building an application using AngularJS and UniformJS. I'd like to have a reset button on the view that would reset my select's to their default value. If I use uniform.js, it isn't working.
You can examine it here:
http://plnkr.co/edit/QYZRzlRf1qqAYgi8VbO6?p=preview
If you click the reset button continuously, nothing happens.
If you remove the attribute, therefore no longer using uniform.js, everything behaves correctly.
Thanks
UPDATE:
Required the use of timeout.
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Found it. For the sake of completeness, I'm copying my comment here:
It looks like Uniform is really hacky. It covers up the actual select element, and displays span instead. Angular is working. The actual select element's value is changing, but the span that Uniform displays is not changing.
So you need to tell Uniform that your values have changed with jQuery.uniform.update. Uniform reads the value from the actual element to place in the span, and angular doesn't update the actual element until after the digest loop, so you need to wait a little bit before calling update:
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Alternatively, you can put this in your directive:
app.directive('applyUniform',function($timeout){
return {
restrict:'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
element.uniform({useID: false});
scope.$watch(function() {return ngModel.$modelValue}, function() {
$timeout(jQuery.uniform.update, 0);
} );
}
};
});
Just a slightly different take on #john-tseng's answer. I didn't want to apply a new attribute to all my check-boxes as we had quite a few in the application already. This also gives you the option to opt out of applying uniform to certain check-boxes by applying the no-uniform attribute.
/*
* Used to make sure that uniform.js works with angular by calling it's update method when the angular model value updates.
*/
app.directive('input', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attr, ngModel) {
if (attr.type === 'checkbox' && attr.ngModel && attr.noUniform === undefined) {
element.uniform({ useID: false });
scope.$watch(function () { return ngModel.$modelValue }, function () {
$timeout(jQuery.uniform.update, 0);
});
}
}
};
});
Please try blow code.
app.directive('applyUniform', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (!element.parents(".checker").length) {
element.show().uniform();
// update selected item check mark
setTimeout(function () { $.uniform.update(); }, 300);
}
}
};
});
<input apply-uniform type="checkbox" ng-checked="vm.Message.Followers.indexOf(item.usrID) > -1" ng-click="vm.toggleSelection(item.usrID)" />

How to hide element if transcluded contents are empty?

I created a very simple directive which displays a key/value pair. I would like to be able to automatically hide the element if the transcluded content is empty (either zero length or just whitespace).
I cannot figure out how to access the content that gets transcluded from within a directive.
app.directive('pair', function($compile) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div><span>{{label}}</span><span ng-transclude></span></div>"
}
});
For example, I would like the following element to be displayed.
<pair label="My Label">Hi there</pair>
But the next two elements should be hidden because they don't contain any text content.
<pair label="My Label"></pair>
<pair label="My Label"><i></i></pair>
I am new to Angular so there may be a great way handle this sort of thing out of the box. Any help is appreciated.
Here's an approach using ng-show on the template and within compile transcludeFn checking if transcluded html has text length.
If no text length ng-show is set to hide
app.directive('pair', function($timeout) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div ng-show='1'><span>{{label}} </span><span ng-transclude></span></div>",
compile: function(elem, attrs, transcludeFn) {
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcludded*/
var show=clone.text().length?'1':'0'
attrs.ngShow=show;
});
}
}
});
Plunker demo
Maybe a bit late but you can also consider using the CSS Pseudo class :empty.
So, this will work (IE9+)
.trancluded-item:empty {
display: none;
}
The element will still be registered in the dom but will be empty and invisible.
The previously provided answers were helpful but didn't solve my situation perfectly, so I came up with a different solution by creating a separate directive.
Create an attribute-based directive (i.e. restrict: 'A') that simply checks to see if there is any text on all the element's child nodes.
function hideEmpty() {
return {
restrict: 'A',
link: function (scope, element, attr) {
let hasText = false;
// Only checks 1 level deep; can be optimized
element.children().forEach((child) => {
hasText = hasText || !!child.text().trim().length;
});
if (!hasText) {
element.attr('style', 'display: none;');
}
}
};
}
angular
.module('directives.hideEmpty', [])
.directive('hideEmpty', hideEmpty);
If you only want to check the main element:
link: function (scope, element, attr) {
if (!element.text().trim().length) {
element.attr('style', 'display: none;');
}
}
To solve my problem, all I needed was to check if there were any child nodes:
link: function (scope, element, attr) {
if (!element.children().length) {
element.attr('style', 'display: none;');
}
}
YMMV
If you don't want to use ng-show every time, you can create a directive to do it automatically:
.directive('hideEmpty', ['$timeout', function($timeout) {
return {
restrict: 'A',
link: {
post: function (scope, elem, attrs) {
$timeout(function() {
if (!elem.html().trim().length) {
elem.hide();
}
});
}
}
};
}]);
Then you can apply it on any element. In your case it would be:
<span hide-empty>{{label}}</span>
I am not terribly familiar with transclude so not sure if it helps or not.
but one way to check for empty contents inside the directive code is to use iElement.text() or iElement.context object and then hide it.
I did it like this, using controllerAs.
/* inside directive */
controllerAs: "my",
controller: function ($scope, $element, $attrs, $transclude) {
//whatever controller does
},
compile: function(elem, attrs, transcludeFn) {
var self = this;
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcluded*/
var showTransclude = clone.text().trim().length ? true : false;
/* I set a property on my controller's prototype indicating whether or not to show the div that is ng-transclude in my template */
self.controller.prototype.showTransclude = showTransclude;
});
}
/* inside template */
<div ng-if="my.showTransclude" ng-transclude class="tilegroup-header-trans"></div>

Resources