How to stop $broadcast events in AngularJS? - angularjs

Is there a built in way to stop $broadcast events from going down the scope chain?
The event object passed by a $broadcast event does not have a stopPropagation method (as the docs on $rootScope mention.) However this merged pull request suggest that $broadcast events can have stopPropagation called on them.

Snippets from angularJS 1.1.2 source code:
$emit: function(name, args) {
// ....
event = {
name: name,
targetScope: scope,
stopPropagation: function() {
stopPropagation = true;
},
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
// ....
}
$broadcast: function(name, args) {
// ...
event = {
name: name,
targetScope: target,
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
// ...
}
As you can see event object in $broadcast not have "stopPropagation".
Instead of stopPropagation you can use preventDefault in order to mark event as "not need to handle this event". This not stop event propagation but this will tell the children scopes: "not need to handle this event"
Example: http://jsfiddle.net/C8EqT/1/

Since broadcast does not have the stopPropagation method,you need to use the defaultPrevented property and this will make sense in recursive directives.
Have a look at this plunker here:Plunkr
$scope.$on('test', function(event) {
if (!event.defaultPrevented) {
event.defaultPrevented = true;
console.log('Handle event here for the root node only.');
}
});

I implemented an event thief for this purpose:
.factory("stealEvent", [function () {
/**
* If event is already "default prevented", noop.
* If event isn't "default prevented", executes callback.
* If callback returns a truthy value or undefined,
* stops event propagation if possible, and flags event as "default prevented".
*/
return function (callback) {
return function (event) {
if (!event.defaultPrevented) {
var stopEvent = callback.apply(null, arguments);
if (typeof stopEvent === "undefined" || stopEvent) {
event.stopPropagation && event.stopPropagation();
event.preventDefault();
}
}
};
};
}]);
To use:
$scope.$on("AnyEvent", stealEvent(function (event, anyOtherParameter) {
if ($scope.keepEvent) {
// do some stuff with anyOtherParameter
return true; // steal event
} else {
return false; // let event available for other listeners
}
}));
$scope.$on("AnyOtherEvent", stealEvent(function (event, anyOtherParameter) {
// do some stuff with anyOtherParameter, event stolen by default
}));

Related

Angular JS upload file occurs twice in IE

I have a directive that handles uploading file and shows it in a list. For this I have a custom button for opening up the explorer. And after user selects a file from the explorer system shows the file name twice in the list. After debugging I realized it's calling the "onClick" method twice, once when the user clicks it (duh) and some mysterious event invokes it again. I think it's the scope.$apply part but can't be sure. Here's my code snippet:
<div data-ng-click="addFile($event)">
<span class="icon-small icon-add"></span>
</div>
Angular JS:
scope.addFile = function (event) {
if (event.originalEvent == null || !(event.originalEvent instanceof MouseEvent)) {
return;
}
if (!hiddenInputElementNode) {
//inject the hidden HtmlInputFile element and bind to the click event
hiddenInputElementNode = angular.element(
"<input accept='application/pdf,audio/*' type='file' class='hidden' multiple />");
hiddenInputElementNode.insertAfter(event.target);
}
//bind to the inputElementNode change event
hiddenInputElementNode.bind('change', function () {
angular.forEach(hiddenInputElementNode[0].files, function (dataFile) {
scope.$apply(
scope.selectedFiles.push({
name: dataFile.name,
data: dataFile
}));
});
this.value = null;
hiddenInputElementNode.unbind('change');
});
$timeout(function () {
if (!!hiddenInputElementNode) {
hiddenInputElementNode.click();
}
}, 0, false);
};
Even weirder this.value = null doesn't nullify the value!
Try by changing your javascript code for this one:
scope.addFile = function (event) {
if (event.target.tagName.toUpperCase() === "DIV") {
if (!hiddenInputElementNode) {
//inject the hidden HtmlInputFile element and bind to the click event
hiddenInputElementNode = angular.element(
"<input accept='application/pdf,audio/*' type='file' class='hidden' multiple />");
hiddenInputElementNode.insertAfter(event.target);
}
//bind to the inputElementNode change event
hiddenInputElementNode.bind('change', function () {
angular.forEach(hiddenInputElementNode[0].files, function (dataFile) {
scope.$apply(
scope.selectedFiles.push({
name: dataFile.name,
data: dataFile
}));
});
this.value = null;
hiddenInputElementNode.unbind('change');
});
$timeout(function () {
if (!!hiddenInputElementNode) {
hiddenInputElementNode.click();
}
}, 0, false);
}
};
I believe that you ng-click event is been fired twice because of the span inside the div (i had a similar problem with IE too).

Widget toggle functionality with $compile

I need to implement toggle functionality for the widget. When the user clicks on the minimization button then widget should shrink and expand when click on maximize button respectively.
I'm trying to achieve this functionality with below piece of code.
Functionality working as expected but it is registering the event multiple times(I'm emitting the event and catching in the filterTemplate directive).
How can we stop registering the event multiple times ?
Or
Is there anyway to like compiling once and on toggle button bind the template/directive to DOM and to make it work rest of the functionality .
So could you please help me to fix this.
function bindFilterTemplate(minimize) {
if ($scope.item && !minimize) {
if ($scope.item.filterTemplate) { // filter template is custom
// directive like this
// "<widget></widget>"
$timeout(function () {
var filterElement = angular.element($scope.item.filterTemplate);
var filterBody = element.find('.cls-filter-body');
filterElement.appendTo(filterBody);
$compile(filterElement)($scope); // Compiling with
// current scope on every time when user click on
// the minimization button.
});
}
} else {
$timeout(function () {
element.find('.cls-filter-body').empty();
});
}
}
bindFilterTemplate();
// Directive
app.directive('widget', function () {
return {
restrict: 'E',
controller: 'widgetController',
link: function ($scope, elem) {
// Some code
}
};
});
// Controller
app.controller('widgetController', function ($scope) {
// This event emitting from parent directive
// On every compile, the event is registering with scope.
// So it is triggering multiple times.
$scope.$on('evt.filer', function ($evt) {
// Server call
});
});
I fixed this issue by creating new scope with $scope.$new().
When user minimizes the widget destroying the scope.
Please let me know if you have any other solution to fix this.
function bindFilterTemplate(minimize) {
// Creating the new scope.
$scope.newChildScope = $scope.$new();
if ($scope.item && !minimize) {
if ($scope.item.filterTemplate) {
$timeout(function () {
var filterElement = angular.element($scope.item.filterTemplate);
var filterBody = element.find('.cls-filter-body');
filterElement.appendTo(filterBody);
$compile(filterElement)($scope.newChildScope);
});
}
} else {
$timeout(function () {
if ($scope.newChildScope) {
// Destroying the new scope
$scope.newChildScope.$destroy();
}
element.find('.cls-filter-body').empty();
});
}
}

debounce is not working with keyup event in Angular

I have a text box to enter some text to search records. I am using data-ng-model-options="{ debounce: 1000 }" with keyup event, data-ng-model-options" working fine, but I want to fire keyup event after the debounce time duration.
Currently keyup event fires instantly before debounce duration. May be I doing something wrong.
Here is my HTML
<input type="text" id="focusOnMe" placeholder="Search..."/ data-ng-keyup="loadSearchResult($event)" data-ng-model="searchText" ng-model-options="{ debounce: 1000 }">
And this is my Keyup event action
$scope.loadSearchResult = function(event) {
$rootScope.hideSearchResult = true;
$rootScope.showLoading = true;
var searchText = $scope.searchText.trim();
if (searchText.length > 0) {
$http({
method: 'POST',
url: '/secure/search',
data: {
searchText: searchText,
peopleFlag: checkboxValueForPeopleSearch,
colonyFlag: checkboxValueForColonySearch
}
}).success(function(data) {
console.log(data);
if (data !== undefined || data !== null) {
$timeout(function() {
$rootScope.hideSearchResult = false;
$rootScope.showLoading = false;
$scope.allSearchResult = {
"bookmarks": data.bookmarks,
"people": data.people,
"colonies": data.colonies
};
}, 300);
} else {
$rootScope.showLoading = false;
commonNotification($rootScope, false, true, true, 'something went wrong!');
$timeout(function() {
$rootScope.newStatus = false;
}, 2000);
}
}).error(function(err) {
});
} else {
$rootScope.hideSearchResult = true;
$rootScope.showLoading = false;
}
};
Sorry the code is dependent to more files, so that I don't have plunker example
Any suggestion will be helpful for me.
Thank You
Debounce doesn't affect the keyup event. It only delays the assigning of the model ($scope) variable. So your keyup event fires immediately and loadSearchResult runs before you want it to.
To solve it, you can add a watch tied to $scope.searchText.
$scope.$watch('searchText', function (newValue) {
loadSearchResult(newValue);
});
Super-simple and cleanly-coded because we're using the Single Source of Truth ($scope).
Note: I omitted the event object because you're not using it.
Once you specify debounce in ng-model-options, it will change the way model gets updated. But it won't change how key event works. For your case, use _.debounce would help:
var delay = 500;
$scope.loadSearchResult = _.debounce($scope.loadSearchResult, delay);

Karma Unit: Testing keypress with escape button

I have such code inside directive :
$document.bind('keydown', function ($event) {
if ($event && $scope.visible && $event.which === escapeKey) {
$scope.toggle();
$scope.$apply();
}
});
I want to test if user click escape toggle will run. At moment I have such test:
it('should toggle window visibility to false when keypress escape', function () {
var doc,
$event;
$httpBackend.when(method, url)
.respond(template);
$event = {
event: 'keydown'
};
directive = createDirective();
$httpBackend.flush();
$isolateScope = directive.isolateScope();
$isolateScope.toggle();
$document.triggerHandler('keydown');
});
But how can I pass that certain key was pressed thought triggerHandler. Don't want to use any jQuery . Is there another way of testing this?
element.triggerHandler({type: 'keydown', which: escapeKey});

How can I send an object with $broadcast?

I have the following:
$scope.$watch('tableForm.$pristine', function (newValue) {
$rootScope.$broadcast("tableDataUpdated",
{ state: $scope.tableForm.$pristine });
});
I also tried:
$scope.$watch('tableForm.$pristine', function (newValue) {
var tableForm = { pristine: $scope.tableForm.$pristine };
$rootScope.$broadcast("tableDataUpdated", tableForm);
});
When the tableForm $pristine state changes then the value of $scope.tableForm.$pristine is set to False and this message is broadcast.
However when I try to receive the message the value of "state" is not defined:
$rootScope.$on("tableDataUpdated", function (args) {
alert(args.state);
});
I also tried:
$rootScope.$on("tableDataUpdated", function (args) {
alert(args.tableForm);
});
Still I seem not to be able to send the object and have it received
That's because listener function has two arguments being passed into it, event, and args. See the angular docs.
Try:
$rootScope.$on("tableDataUpdated", function (event, args) {
alert(args.state);
});

Resources