AngularJS one click on element and click+hold more 5 seconds - angularjs

I have the div element and I need catch event of one click and click and hold.
If happened one click of this div, i should call function 1 in scope, if click and hold (more of 5 seconds) i should call function 2 in scope.

Create a directive on-click-and-hold and use it.
Directive
directive('onClickAndHold', function ($parse, $timeout) {
return {
link: function (scope, element, attrs) {
var clickAndHoldFn = $parse(attrs.onClickAndHold);
var doNotTriggerClick;
var timeoutHandler;
element.on('mousedown', function () {
$timeout.cancel(timeoutHandler);
timeoutHandler = $timeout(function () {
clickAndHoldFn(scope, {$event: event})
}, 5000)
});
element.on('mouseup', function (event) {
$timeout.cancel(timeoutHandler);
});
if (attrs.onClick) {
var clickFn = $parse(attrs.onClick);
element.on('click', function (event) {
if (doNotTriggerClick) {
doNotTriggerClick = false;
return;
}
clickFn(scope, {$event: event});
scope.$apply();
});
}
}
}
})
Markup
<div on-click-and-hold="f2()" on-click="f1()"></div>
Controller
controller('AppCtrl', function ($scope) {
$scope.f1 = function () {
console.warn('inside f1');
}
$scope.f2 = function () {
console.warn('inside f2');
}
})
If you want to handle only the click event use ng-click instead of on-click-and-hold.
Working Plnkr

Related

Can I simplify clicking the enter key with AngularJS?

I already have this code that I came up with:
In my outer controller:
$scope.key = function ($event) {
$scope.$broadcast('key', $event.keyCode)
}
In my inner controller (I have more than one like this)
$scope.$on('key', function (e, key) {
if (key == 13) {
if (ts.test.current) {
var btn = null;
if (ts.test.userTestId) {
btn = document.getElementById('viewQuestions');
} else {
btn = document.getElementById('acquireTest');
}
$timeout(function () {
btn.focus();
btn.click();
window.setTimeout(function () {
btn.blur();
}, 500);
})
}
}
});
Is there another way that I could simplify this using some features of AngularJS that I have not included here?
Please check this gist, https://gist.github.com/EpokK/5884263
You can simply create a directive ng-enter and pass your action as paramater
app.directive('ngEnter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
};
});
HTML
<input ng-enter="myControllerFunction()" />
You may change the name ng-enter to something different, because ng-** is a reserved by Angular core team.
Also I see that your controller is dealing with DOM, and you should not. Move those logic to other directive or to HTML, and keep your controller lean.
if (ts.test.userTestId) {
btn = document.getElementById('viewQuestions'); //NOT in controller
} else {
btn = document.getElementById('acquireTest'); //NOT in controller
}
$timeout(function () {
btn.focus(); //NOT in controller
btn.click(); //NOT in controller
window.setTimeout(function () { // $timeout in $timeout, questionable
btn.blur(); //NOT in controller
}, 500);
})
What i've done in the past is a directive which just listens for enter key inputs and then executes a function that is provided to it similar to an ng-click. This makes the logic stay in the controller, and will allow for reuse across multiple elements.
//directive
angular.module('yourModule').directive('enterHandler', [function () {
return{
restrict:'A',
link: function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
var key = event.which ? event.which : event.keyCode;
if (key === 13) {
scope.$apply(function () {
scope.$eval(attrs.enterHandler);
});
event.preventDefault();
}
});
}
}
}]);
then your controller becomes
$scope.eventHandler = function(){
if (ts.test.current) {
var btn = ts.test.userTestId
? document.getElementById('viewQuestions')
: document.getElementById('acquireTest');
$timeout(function () {
btn.focus();
btn.click();
window.setTimeout(function () {
btn.blur();
}, 500);
})
}
}
and your markup can then be
<div enter-handler="eventHandler()" ></div>

AngularJs: Run function when any element in section is blurred

I've created the following directive:
.directive('onSectionBlur', function ($parse) {
return {
restrict: 'A',
controller: function ($scope, $element, $attrs) {
$element.focusout(function (event) {
if (!jQuery.contains($element[0], event.relatedTarget)) {
$scope.$apply($parse($attrs.onSectionBlur)($scope));
}
});
}
};
})
My goal here is if a user tabs out of a section of a form (or clicks elsewhere), I want to display a read-only version of that data: http://jsfiddle.net/uZBXw/3/
So this works from what I can tell, but I feel like I was just mashing buttons on this line:
$scope.$apply($parse($attrs.onSectionBlur)($scope));
Is this the correct way to run code and wire it into the angular lifecycle?
I think you should use an isolated scope with an attribute marked with &. This will give you access to a function that will run on the parent scope and is the exact use case of what you're trying to do.
app.directive('onSectionBlur', function () {
return {
restrict: 'A',
scope: {
'notify': '&onSectionBlur' // reuse the directive name for easier handling
},
link: function (scope, element) {
element.on('focusout', function (evt) {
if (!angular.element.contains(element[0], evt.relatedTarget)) {
scope.$apply(scope.notify); // let $apply call the notify-callback
}
});
}
};
});
demo: http://jsbin.com/diwetaje/1/
from the Developer Guide:
Best Practice: use &attr in the scope option when you want your directive to expose an API for binding to behaviors.
I was having issues with clicking on various items in the section (i.e. checkbox labels), so if anyone else runs across this issue I've added a potential enhancement to Yoshi's version:
.directive('onSectionBlur', function ($document) {
return {
restrict: 'A',
scope: {
'notify': '&onSectionBlur'
},
link: function (scope, element) {
var hasFocus = false;
element.on('focusin', function (evt) {
hasFocus = true;
});
$document.on('click focusin', function (evt) {
if (hasFocus && !angular.element.contains(element[0], evt.target)) {
hasFocus = false;
scope.$apply(scope.notify);
}
});
}
};
});
EDIT: Here's the butchered up version I ended up with, that takes into account buttons that weren't clickable (if they were outside the section and below it) as well as not firing the event if the user has a modal window open:
link: function (scope, element) {
var hasFocus = false;
var lostFocus = function () {
hasFocus = false;
scope.$apply(scope.notify);
};
element.on('focusin', function (evt) {
hasFocus = true;
});
element.on('keydown', function (evt) {
if (hasFocus && evt.keyCode == 9) {
//Using timeout to give the browser time to process what it should have been doing (i.e. focusing next item)
if (evt.shiftKey && element.find(':focusable:first').is(evt.target)) {
$timeout(lostFocus);
} else if (element.find(':focusable:last').is(evt.target)) {
$timeout(lostFocus);
}
}
});
var docHandler = function (evt) {
//If the click came from inside of a modal window, ignore it
if (angular.element(evt.target).closest('.modal').length == 0) {
if (hasFocus && !angular.element.contains(element[0], evt.target)) {
lostFocus();
}
}
};
$document.on('click', docHandler);
scope.$on('$destroy', function () {
$document.off('click', docHandler);
});
}

AngularJS - bind to directive resize

How can i be notified when a directive is resized?
i have tried
element[0].onresize = function() {
console.log(element[0].offsetWidth + " " + element[0].offsetHeight);
}
but its not calling the function
(function() {
'use strict';
// Define the directive on the module.
// Inject the dependencies.
// Point to the directive definition function.
angular.module('app').directive('nvLayout', ['$window', '$compile', layoutDirective]);
function layoutDirective($window, $compile) {
// Usage:
//
// Creates:
//
var directive = {
link: link,
restrict: 'EA',
scope: {
layoutEntries: "=",
selected: "&onSelected"
},
template: "<div></div>",
controller: controller
};
return directive;
function link(scope, element, attrs) {
var elementCol = [];
var onSelectedHandler = scope.selected();
element.on("resize", function () {
console.log("resized.");
});
$(window).on("resize",scope.sizeNotifier);
scope.$on("$destroy", function () {
$(window).off("resize", $scope.sizeNotifier);
});
scope.sizeNotifier = function() {
alert("windows is being resized...");
};
scope.onselected = function(id) {
onSelectedHandler(id);
};
scope.$watch(function () {
return scope.layoutEntries.length;
},
function (value) {
//layout was changed
activateLayout(scope.layoutEntries);
});
function activateLayout(layoutEntries) {
for (var i = 0; i < layoutEntries.length; i++) {
if (elementCol[layoutEntries[i].id]) {
continue;
}
var div = "<nv-single-layout-entry id=slot" + layoutEntries[i].id + " on-selected='onselected' style=\"position:absolute;";
div = div + "top:" + layoutEntries[i].position.top + "%;";
div = div + "left:" + layoutEntries[i].position.left + "%;";
div = div + "height:" + layoutEntries[i].size.height + "%;";
div = div + "width:" + layoutEntries[i].size.width + "%;";
div = div + "\"></nv-single-layout-entry>";
var el = $compile(div)(scope);
element.append(el);
elementCol[layoutEntries[i].id] = 1;
}
};
}
function controller($scope, $element) {
}
}
})();
Use scope.$watch with a custom watch function:
scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
console.log('directive got resized:', value.split('x'));
}
)
You would typically want to watch the element's offsetWidth and offsetHeight properties. With more recent versions of AngularJS, you can use $scope.$watchGroup in your link function:
app.directive('myDirective', [function() {
function link($scope, element) {
var container = element[0];
$scope.$watchGroup([
function() { return container.offsetWidth; },
function() { return container.offsetHeight; }
], function(values) {
// Handle resize event ...
});
}
// Return directive definition ...
}]);
However, you may find that updates are quite slow when watching the element properties directly in this manner.
To make your directive more responsive, you could moderate the refresh rate by using $interval. Here's an example of a reusable service for watching element sizes at a configurable millisecond rate:
app.factory('sizeWatcher', ['$interval', function($interval) {
return function (element, rate) {
var self = this;
(self.update = function() { self.dimensions = [element.offsetWidth, element.offsetHeight]; })();
self.monitor = $interval(self.update, rate);
self.group = [function() { return self.dimensions[0]; }, function() { return self.dimensions[1]; }];
self.cancel = function() { $interval.cancel(self.monitor); };
};
}]);
A directive using such a service would look something like this:
app.directive('myDirective', ['sizeWatcher', function(sizeWatcher) {
function link($scope, element) {
var container = element[0],
watcher = new sizeWatcher(container, 200);
$scope.$watchGroup(watcher.group, function(values) {
// Handle resize event ...
});
$scope.$on('$destroy', watcher.cancel);
}
// Return directive definition ...
}]);
Note the call to watcher.cancel() in the $scope.$destroy event handler; this ensures that the $interval instance is destroyed when no longer required.
A JSFiddle example can be found here.
Here a sample code of what you need to do:
APP.directive('nvLayout', function ($window) {
return {
template: "<div></div>",
restrict: 'EA',
link: function postLink(scope, element, attrs) {
scope.onResizeFunction = function() {
scope.windowHeight = $window.innerHeight;
scope.windowWidth = $window.innerWidth;
console.log(scope.windowHeight+"-"+scope.windowWidth)
};
// Call to the function when the page is first loaded
scope.onResizeFunction();
angular.element($window).bind('resize', function() {
scope.onResizeFunction();
scope.$apply();
});
}
};
});
The only way you would be able to detect size/position changes on an element using $watch is if you constantly updated your scope using something like $interval or $timeout. While possible, it can become an expensive operation, and really slow your app down.
One way you could detect a change on an element is by calling
requestAnimationFrame.
var previousPosition = element[0].getBoundingClientRect();
onFrame();
function onFrame() {
var currentPosition = element[0].getBoundingClientRect();
if (!angular.equals(previousPosition, currentPosition)) {
resiszeNotifier();
}
previousPosition = currentPosition;
requestAnimationFrame(onFrame);
}
function resiszeNotifier() {
// Notify...
}
Here's a Plunk demonstrating this. As long as you're moving the box around, it will stay red.
http://plnkr.co/edit/qiMJaeipE9DgFsYd0sfr?p=preview
A slight variation on Eliel's answer worked for me. In the directive.js:
$scope.onResizeFunction = function() {
};
// Call to the function when the page is first loaded
$scope.onResizeFunction();
angular.element($(window)).bind('resize', function() {
$scope.onResizeFunction();
$scope.$apply();
});
I call
$(window).resize();
from within my app.js. The directive's d3 chart now resizes to fill the container.
Here is my take on this directive (using Webpack as bundler):
module.exports = (ngModule) ->
ngModule.directive 'onResize', ['Callback', (Callback) ->
restrict: 'A'
scope:
onResize: '#'
onResizeDebounce: '#'
link: (scope, element) ->
container = element[0]
eventName = scope.onResize || 'onResize'
delay = scope.onResizeDebounce || 1000
scope.$watchGroup [
-> container.offsetWidth ,
-> container.offsetHeight
], _.debounce (values) ->
Callback.event(eventName, values)
, delay
]

how to defer a method execution in a directive in angularjs

I'm writing a directive which recives a function as a parameter, the directive executes and run whatever is contained into the function.
the use case is:
the directive contains a link
when the user click the link the method passed as a parameter (from the controller) is executed
when the user hit the click button a spinner will show
the spinner will hide when the execution of the method is finished
my question is how can I defer the execution and bind it to a promise, to hide the spinner after the method execution.
for illustration purposes I have used $timeout that counts from 3 to 1, please take a look to the code I've done so far:
app.directive('toogleTextLink', function($compile,$q) {
return {
restrict: 'AE',
scope: { callback: "&targetMethod" },
template: '<div><a style="cursor: pointer" ><b>{{text}}</b></a>show value= {{show}} <br/><div ng-class="{previewLoader: show}"></div></div>',
link: function (scope, element, attr) {
scope.value = attr.value;
scope.show = false;
scope.$watch('value', function () {
if (scope.value) {
scope.text = "yes";
} else {
scope.text = "no";
}
});
element.bind('click', function () {
scope.show = true;
scope.value = !scope.value;
scope.$digest();
if (scope.callback) {
var deferred = $q.defer(scope.callback());
deferred.promise.then(function () {
scope.show = false;
console.log("then called");
});
}
});
}
};
});
app.controller('myCtrl', function($scope,$timeout,$q) {
$scope.IsFacebookConnected = false;
$scope.countDown = 3;
$scope.authSocial = function(value, socialNetwork) {
switch (socialNetwork) {
case "facebook":
$scope.IsFacebookConnected = !value;
}
runCounter = function() {
$scope.countDown -= 1;
if ( $scope.countDown > 0)
$timeout(runCounter, 1000);
console.log("timer");
};
runCounter();
};
});
this is the plunker as well.
Andy Joslin wrote a "promise tracker" which does exactly what you need. It even has some sugar for $http integration. Basically you add "promises" to it, and it tells you the execution state. You can then bind that execution state to ng-show on something like a loading spinner animation. https://github.com/ajoslin/angular-promise-tracker and here's a demo: http://plnkr.co/edit/3uAe0NdXLz1lCYlhpaMp?p=preview
Your code will look something like:
$scope.tracker = promiseTracker("socialtracker");
$scope.tracker.addPromise(somePromise);
And in your view:
<div ng-show="tracker.active()">Loading...</div>

AngularJS dropdown directive hide when clicking outside

I'm trying to create a multiselect dropdown list with checkbox and filter option. I'm trying to get the list hidden with I click outside but could not figure it out how. Appreciate your help.
http://plnkr.co/edit/tw0hLz68O8ueWj7uZ78c
Watch out, your solution (the Plunker provided in the question) doesn't close the popups of other boxes when opening a second popup (on a page with multiple selects).
By clicking on a box to open a new popup the click event will always be stopped. The event will never reach any other opened popup (to close them).
I solved this by removing the event.stopPropagation(); line and matching all child elements of the popup.
The popup will only be closed, if the events element doesn't match any child elements of the popup.
I changed the directive code to the following:
select.html (directive code)
link: function(scope, element, attr){
scope.isPopupVisible = false;
scope.toggleSelect = function(){
scope.isPopupVisible = !scope.isPopupVisible;
}
$(document).bind('click', function(event){
var isClickedElementChildOfPopup = element
.find(event.target)
.length > 0;
if (isClickedElementChildOfPopup)
return;
scope.$apply(function(){
scope.isPopupVisible = false;
});
});
}
I forked your plunker and applied the changes:
Plunker: Hide popup div on click outside
Screenshot:
This is an old post but in case this helps anyone here is a working example of click outside that doesn't rely on anything but angular.
module('clickOutside', []).directive('clickOutside', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function (scope, el, attr) {
$document.on('click', function (e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function () {
scope.$eval(scope.clickOutside);
});
}
});
}
}
});
OK I had to call $apply() as the event is happening outside angular world (as per doc).
element.bind('click', function(event) {
event.stopPropagation();
});
$document.bind('click', function(){
scope.isVisible = false;
scope.$apply();
});
I realized it by listening for a global click event like so:
.directive('globalEvents', ['News', function(News) {
// Used for global events
return function(scope, element) {
// Listens for a mouse click
// Need to close drop down menus
element.bind('click', function(e) {
News.setClick(e.target);
});
}
}])
The event itself is then broadcasted via a News service
angular.factory('News', ['$rootScope', function($rootScope) {
var news = {};
news.setClick = function( target ) {
this.clickTarget = target;
$rootScope.$broadcast('click');
};
}]);
You can then listen for the broadcast anywhere you need to. Here is an example directive:
.directive('dropdown', ['News', function(News) {
// Drop down menu für the logo button
return {
restrict: 'E',
scope: {},
link: function(scope, element) {
var opened = true;
// Toggles the visibility of the drop down menu
scope.toggle = function() {
element.removeClass(opened ? 'closed' : 'opened');
element.addClass(opened ? 'opened' : 'closed');
};
// Listens for the global click event broad-casted by the News service
scope.$on('click', function() {
if (element.find(News.clickTarget.tagName)[0] !== News.clickTarget) {
scope.toggle(false);
}
});
// Init
scope.toggle();
}
}
}])
I hope it helps!
I was not totally satisfied with the answers provided so I made my own. Improvements:
More defensive updating of the scope. Will check to see if a apply/digest is already in progress
div will also close when the user presses the escape key
window events are unbound when the div is closed (prevents leaks)
window events are unbound when the scope is destroyed (prevents leaks)
function link(scope, $element, attributes, $window) {
var el = $element[0],
$$window = angular.element($window);
function onClick(event) {
console.log('window clicked');
// might need to polyfill node.contains
if (el.contains(event.target)) {
console.log('click inside element');
return;
}
scope.isActive = !scope.isActive;
if (!scope.$$phase) {
scope.$apply();
}
}
function onKeyUp(event) {
if (event.keyCode !== 27) {
return;
}
console.log('escape pressed');
scope.isActive = false;
if (!scope.$$phase) {
scope.$apply();
}
}
function bindCloseHandler() {
console.log('binding window click event');
$$window.on('click', onClick);
$$window.on('keyup', onKeyUp);
}
function unbindCloseHandler() {
console.log('unbinding window click event');
$$window.off('click', onClick);
$$window.off('keyup', onKeyUp);
}
scope.$watch('isActive', function(newValue, oldValue) {
if (newValue) {
bindCloseHandler();
} else {
unbindCloseHandler();
}
});
// prevent leaks - destroy handlers when scope is destroyed
scope.$on('$destroy', function() {
unbindCloseHandler();
});
}
I get $window directly into the link function. However, you do not need to do this exactly to get $window.
function directive($window) {
return {
restrict: 'AE',
link: function(scope, $element, attributes) {
link.call(null, scope, $element, attributes, $window);
}
};
}
There is a cool directive called angular-click-outside. You can use it in your project. It is super simple to use:
https://github.com/IamAdamJowett/angular-click-outside
The answer Danny F posted is awesome and nearly complete, but Thịnh's comment is correct, so here is my modified directive to remove the listeners on the $destroy event of the directive:
const ClickModule = angular
.module('clickOutside', [])
.directive('clickOutside', ['$document', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function (scope, el, attr) {
const handler = function (e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function () {
console.log("hiiii");
// whatever expression you assign to the click-outside attribute gets executed here
// good for closing dropdowns etc
scope.$eval(scope.clickOutside);
});
}
}
$document.on('click', handler);
scope.$on('$destroy', function() {
$document.off('click', handler);
});
}
}
}]);
If you put a log in the handler method, you will still see it fire when an element has been removed from the DOM. Adding my small change is enough to remove it. Not trying to steal anyone's thunder, but this is a fix to an elegant solution.
Use angular-click-outside
Installation:
bower install angular-click-outside --save
npm install #iamadamjowett/angular-click-outside
yarn add #iamadamjowett/angular-click-outside
Usage:
angular.module('myApp', ['angular-click-outside'])
//in your html
<div class="menu" click-outside="closeThis">
...
</div>
//And then in your controller
$scope.closeThis = function () {
console.log('closing');
}
I found some issues with the implementation in https://github.com/IamAdamJowett/angular-click-outside
If for example the element clicked on is removed from the DOM, the directive above will trigger the logic.
That didn't work for me, since I had some logic in a modal that, after click, removed the element with a ng-if.
I rewrote his implementation. Not battle tested, but seems to be working better (at least in my scenario)
angular
.module('sbs.directives')
.directive('clickOutside', ['$document', '$parse', '$timeout', clickOutside]);
const MAX_RECURSIONS = 400;
function clickOutside($document, $parse, $timeout) {
return {
restrict: 'A',
link: function ($scope, elem, attr) {
// postpone linking to next digest to allow for unique id generation
$timeout(() => {
function runLogicIfClickedElementIsOutside(e) {
// check if our element already hidden and abort if so
if (angular.element(elem).hasClass('ng-hide')) {
return;
}
// if there is no click target, no point going on
if (!e || !e.target) {
return;
}
let clickedElementIsOutsideDirectiveRoot = false;
let hasParent = true;
let recursions = 0;
let compareNode = elem[0].parentNode;
while (
!clickedElementIsOutsideDirectiveRoot &&
hasParent &&
recursions < MAX_RECURSIONS
) {
if (e.target === compareNode) {
clickedElementIsOutsideDirectiveRoot = true;
}
compareNode = compareNode.parentNode;
hasParent = Boolean(compareNode);
recursions++; // just in case to avoid eternal loop
}
if (clickedElementIsOutsideDirectiveRoot) {
$timeout(function () {
const fn = $parse(attr['clickOutside']);
fn($scope, { event: e });
});
}
}
// if the devices has a touchscreen, listen for this event
if (_hasTouch()) {
$document.on('touchstart', function () {
setTimeout(runLogicIfClickedElementIsOutside);
});
}
// still listen for the click event even if there is touch to cater for touchscreen laptops
$document.on('click', runLogicIfClickedElementIsOutside);
// when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around
$scope.$on('$destroy', function () {
if (_hasTouch()) {
$document.off('touchstart', runLogicIfClickedElementIsOutside);
}
$document.off('click', runLogicIfClickedElementIsOutside);
});
});
},
};
}
function _hasTouch() {
// works on most browsers, IE10/11 and Surface
return 'ontouchstart' in window || navigator.maxTouchPoints;
}

Resources