In my $onInit() I pick up the propagation event:
$onInit() {
this.$rootScope.$on('CANCELLED', (event) => {
this.EventService.getEventsCurrentUser('own')
.then((result) => {
this.ownEvents = result
})
})
}
How can I stop the propagation at one time ?
You need to "unregister" $rootScope events manually by calling the return function. You can do it with the component lifecycle by using this.$onDestroy. $rootScope events getting binded again and again each time $rootScope.$on() is executed. Thats why your events getting called multiple times.
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope, $rootScope) {
var registerScope = null;
this.$onInit = function () {
//register rootScope event
registerScope = $rootScope.$on('CANCELLED', function(event) {
console.log("fired");
});
}
this.$onDestroy = function () {
//unregister rootScope event by calling the return function
registerScope();
}
});
Please also check this answers which will help you to understand the logic behind $rootScope and $scope events:
How to use component lifecycles
Difference between $rootScope.$on vs $scope.$on
Related
In my angular application (using ES6) I need to make an ajax call after some custom event is fired from other directive with the code below:
class GridController {
constructor($scope, $rootScope, MyService) {
this.$scope = $scope;
this.$rootScope = $rootScope;
this.MyService = MyService
this.MyService.getItems(arg1, arg2).then((resp) => {
this.viewModel= resp;
});
this.$rootScope.$on('myEvent', (e, args) => {
this.MyService.getItems(arg1, args[1]).then((resp) => {
this.viewModel= resp;
});
}
});
}
The problem is that the view is not updated after the successful ajax call.
Is there any ideas why this is happening?
It may be model update problem try this.
$scope.$apply();
Maybe the ajax-call happens outside of angulars digest cycle. You have tried calling $scope.$digest()? This fires the watchers of the current scope; they should pick up the model change.
I have a recursive method that, if a flag is set, will call itself every five seconds. I'm trying to write a test that spies on the method, calls it, waits six seconds and then expects the method to have been called twice. My test fails, as the spy reports the method only being called once (the initial call).
I'm using the Angular style guide, so am attaching these methods to a placeholder for this. I suspect there may be an issue with scoping of the controller returned from angular-mocks $controller(), but I'm not sure—most people are attaching methods to $scope.
Without attaching methods to $scope, how can I create a spy to verify that my method has been called twice?
app.js:
'use strict';
angular
.module('MyApp', [
//...
]);
angular
.module('MyApp')
.controller('MyController', MyController);
MyController.$inject = [
//...
];
function MyController() {
var vm = this;
vm.callMyself = callMyself;
vm.flag = false;
function callMyself () {
console.log('callMyself');
if (vm.flag) {
console.log('callMyself & flag');
setTimeout(vm.callMyself, 5000);
}
}
}
appSpec.js:
describe('MyController', function () {
var $scope;
beforeEach(function () {
module('MyApp');
});
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
controllerInstance = $controller('MyController', {$scope: $scope});
}));
it('should call itself within 6 seconds if flag is true', function (done) {
controllerInstance.flag = true;
spyOn(controllerInstance, 'callMyself');
controllerInstance.callMyself();
setTimeout(function () {
expect(controllerInstance.callMyself).toHaveBeenCalledTimes(2);
done();
}, 6000);
}, 7000);
});
Working Plunker
You need to use .and.callThrough() to further execute the function that would call itself:
By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation.
spyOn(controllerInstance, 'callMyself').and.callThrough();
Tested in the plunker - it works.
What I want to do:
I'm trying to create a function within a directive that can be called from the $rootScope.
The Problem:
It seems to only be working on the last item in the DOM which has this directive. I'm guessing that what's happening is rootScope.myFunction gets overwritten each time this directive runs.
The Question:
How can I create one function in the $rootScope which, when called, runs the internal function for each directive instead of just the last one?
The Relevant Code:
(function() {
angular.module('home')
.directive('closeBar', closeBar);
closeBar.$inject = ['$rootScope', '$window'];
function closeBar(rootScope, window) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
var myFunction = function() {
// do stuff
};
myFunction();
rootScope.myFunction = function() {
myFunction();
};
}
};
}
})();
Then in a different script, I want to call:
rootScope.myFunction();
Note, I'm not allowed to share actual code from the project I'm working on so this is a more general question not tied to a specific use case.
A simple solution would be to create a special function in your rootScope that accepts functions as an argument, and pushes it into an array, and you will be able to invoke that function later which will call all the registered functions.
angular.module('myApp').run(['$rootScope', function($root) {
var functionsToCall = [];
$root.registerDirectiveFunction = function(fn, context) {
context = context || window;
functionsToCall.push({fn: fn, context: context});
}
$root.myFunction = function(args) {
functionsToCall.forEach(function(fnObj) {
fnObj.fn.apply(fnObj.context,args);
});
}
}
And in your directive:
link: function($scope, $el, $attr) {
function myFunct() {
}
$scope.$root.registerDirectiveFunction(myFunct, $scope);
//call it if you want
$scope.$root.myFunction();
}
This should cover your scenario.
I'm trying to encapsulate the events in a service in order to implement a mechanics to subscribe / unsubscribe the listeners when a controller's scope is destroyed. This because I have been using the rootScope.$on in the following way:
if(!$rootScope.$$listeners['event']) {
$rootScope.$on('event', function(ev, data){
// do some...
});
}
or
$scope.$on('$destroy', function(ev, data){
// unsubscribe the listener
});
So I just need one listener of this event, I need to delete the existing listener when the controller is no longer alive, because the function I registered earlier is still being triggered.
So I need to implement a $destroy event listener on my controller, to destroy the listener when the scope is destroyed, but I don't want to do that code each time I create an event.
That's why I want to create a service in where I'm going to encapsulate the events.
angular.module('core').factory('event', [
function() {
var service = {};
service.events = {};
service.on = function(scope, eventId, callback) {
scope.$on('$destroy', function(ev, other){
//unsubscribe
});
service.events[eventId] = callback;
// scope = null; I guess ?
};
service.emit = function(eventId, data){
if (service.events[eventId])
service.events[eventId](data);
else
return new Error('The event is not subscribed');
};
return service;
}
]);
This could be done using $rootScope instead of my own methods but encapsulating the $on and $emit of $rootScope, but at the end I'll have the same issue here.
So these are my questions:
Is a good practice to pass the scope ref value to a service?
What is the meaning of $$destroyed? when this is true means that angularJS has no internal references to the instance?
Should I do a scope = null in my service to let GC delete the object or does angularJS handle an explicit delete?
Is there a better way to do what I want?
What you are trying to accomplish is basically an event bus.
You have also described very well what is wrong with the current implementation.
A different way to approach the problem is to decorate the $rootScope with your bus (or any other event bus for that matter). Here is how:
app.config(function ($provide) {
$provide.decorator('$rootScope', ['$delegate', '$$bus', function ($delegate, $$bus) {
Object.defineProperty($delegate.constructor.prototype, '$bus', {
get: function () {
var self = this;
return {
subscribe: function () {
var sub = $$bus.subscribe.apply($$bus, arguments);
self.$on('$destroy',
function () {
console.log("unsubscribe!");
sub.unsubscribe();
});
},
publish: $$bus.publish
};
},
enumerable: false
});
return $delegate;
}]);
});
Considering the following $$bus implementation (kept basic for simplicity):
app.factory('$$bus', function () {
var api = {};
var events = {};
api.subscribe = function (event) {
if (!events.hasOwnProperty(event.name)) {
events[event.name] = [event];
} else {
events[event.name].push(event);
}
return {
unsubscribe: function () {
api.unsubscribe(event);
}
}
};
api.publish = function (eventName, data) {
if (events.hasOwnProperty(eventName)) {
console.log(eventName);
angular.forEach(events[eventName], function (subscriber) {
subscriber.callback.call(this, data);
});
}
};
api.unsubscribe = function (event) {
if (events.hasOwnProperty(event.name)) {
events[event.name].splice(events[event.name].indexOf(event), 1);
if (events[event.name].length == 0) {
delete events[event.name];
}
}
};
return api;
});
Now all you have to do is subscribe or publish events. The unsubscribe will take place automatically (when the $scope is destroyed):
$scope.$bus.subscribe({
name: 'test', callback: function (data) {
console.log(data);
}
});
And later on publish an event:
$scope.$bus.publish('test', {name: "publishing event!"});
An important point to make is that the events themselves are subscribed to each individual $scope and not on the $rootScope. That is how you "know" which $scope to release.
I think it answers your question. With that in mind, you can obviously make this mechanism much sophisticated (such as controller event listener released when a view routed, unsubscribe automatically only to certain events, etc.).
Good luck!
** This solution is taken form Here which uses a different bus framework (other then that it is the same).
I am about to give up on this. I have tried every which way to access the directive scope in a test.
'use strict';
angular.module('cmsModuleApp')
.directive('fileUpload', function () {
return {
scope: {},
template: '<input type="file" >',
restrict: 'E',
controller: function fileUploadCtrl (scope) {
//also tried scope.uploadFile here...
this.uploadFile = function (files) {console.log("please work...")};
},
link: function postLink(scope, element, attrs, Ctrl) {
element.uploadFile = function (files) {console.log("pleaseeeee")};
}
};
});
test::
'use strict';
describe('Directive: fileUpload', function () {
beforeEach(module('cmsModuleApp'));
var element;
var scope;
var files;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
}));
it('should call a file upload method onchange event', inject(function ($compile) {
element = angular.element('<file-upload></file-upload>');
element = $compile(element)(scope);
//tried moving this around thinking maybe it had to render or update
scope.$digest();
//Im loggin the element's scope to explore the object a bit
console.log(element.scope());
spyOn(element.scope(), 'uploadFile')
element.triggerHandler('onchange');
expect(element.scope().uploadFile()).toHaveBeenCalled();
}));
});
What I am trying to test is that when this file input changes (is clicked and loaded up with files) it will execute the uploadFile() method on the directive's scope. Once I get this working I was going to implement an $http service.
However, the method does not exist or is undefined.. No matter what I seem to try.
Could you try to modify your test file like this?
I moved the variables declaration into the describe and the test initilization into the beforeEach. Then I created a spy on scope.uploadFile.
fileUpload_test :
'use strict';
describe('Directive: fileUpload', function () {
var element;
var scope;
var files;
beforeEach(module('cmsModuleApp'));
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
element = angular.element('<file-upload></file-upload>');
element = $compile(element)(scope);
scope.$digest();
}));
afterEach(function() {
scope.$destroy();
});
it('should call a file upload method onchange event', function() {
scope.uploadFile = jasmine.createSpy();
element.triggerHandler('change');
expect(scope.uploadFile).toHaveBeenCalled();
}));
});
I think the issue might be that you are using an isolate scope scope: {}. Here's an example of how I did a similar task:
describe('File Input Directive', function() {
var scope, element, isolateScope;
beforeEach(function() {
bard.appModule('appName');
bard.inject(this, '$compile', '$rootScope');
scope = $rootScope.$new();
var html = '<form><my-file-input /></form>';
var form = angular.element(html);
element = form.find('my-file-input');
var formElement = $compile(form)(scope);
scope.$digest();
isolateScope = element.isolateScope();
});
afterEach(function() {
scope.$destroy();
});
bard.verifyNoOutstandingHttpRequests();
describe('selectFile', function() {
it('triggers a click on the file input', function() {
var fileInput = $(element).find('.none')[0];
var mockClick = sinon.spy(fileInput, 'click');
isolateScope.selectFile();
scope.$digest();
expect(mockClick).calledOnce;
});
});
You can ignore all of the bard references - it's a helper library, which reduces some boilerplate. The important parts are creating the isolateScope in the beforeEach and referencing the directive's method (in this case, selectFile) on the isolateScope in the test itself. Also, notice the scope.$digest() after calling the method. Hope it helps!