I need to call an Angular function on the hidden event of a bootstrap modal.
Here is my hidden event handler:
$('#modalAddAction').on('hidden.bs.modal',
function (e) {
console.log("in hidden.bs.modal");
var $rootScope = angular.element(document.querySelector("[ng-controller=actionsController]")).scope();
if ($rootScope) {
$rootScope.$apply(function () {
$rootScope.initializeAt1();
});
}
});
Here is the function defined in the controller I need to call:
$scope.initializeAt1 = function () {
$scope.data.addAction.actionTypeId; // initialized in $scope.getActionTypes
$scope.data.addAction.actionStatus = 1;
// initializers help set drop downs to appropriate selection.
$scope.data.addAction.actionType1.actionRecommendedByLerId = 0;
$scope.data.addAction.actionType1.actionsRecommendedByLer = [];
$scope.data.addAction.actionType1.actionProposedBySupervisorId = 0;
$scope.data.addAction.actionType1.actionTakenBySupervisorId = 0;
$scope.data.addAction.actionType1.actionChargeId = 0; // formerly initialized in $scope.getActionCharges
$scope.data.addAction.actionType1.actionCharges = [];
}();
So the first time the controller factory is run, the initializeAt1 function calls itself and does the initialization I need.
Then I try to call initializeAt1 again whenever the modal is hidden, whether from a save buttion, a cancel button, or just clicking on the screen.
The #modalAddAction - hidden event here gives me this error:
[$rootScope:inprog] $digest already in progress
So now I try to change he event handler to this (comment out the apply and just directly call the function from $rootScope):
$('#modalAddAction').on('hidden.bs.modal',
function (e) {
console.log("in hidden.bs.modal");
var $rootScope = angular.element(document.querySelector("[ng-controller=actionsController]")).scope();
//if ($rootScope) {
//$rootScope.$apply(function () {
$rootScope.initializeAt1();
//});
//}
});
And now I get this error.
$rootScope.initializeAt1 is not a function
So I have the rootScope, but why does it not see the function?
I figured it out.
It just didn't like the function being self called after the definition.
When I inspected the $scope off of the $rootScope initailizeAt1 was listed as undefined.
Had to take () off of the end.
And add the call:
$scope.initializeActions = function () {
$scope.initializeAt1();
}
$scope.initializeActions();
at the end.
Will then add init for At2, At3 and so on to initializeActions.
Related
I have 2 components which are both accessing a service. One component delivers an object and the other one is supposed to display it or just receive it. The problem is that after the initialization process is finished the variable in the display component doesn't change.
I have tried using $scope , $scope.$apply(), this.$onChanges aswell as $scope.$watch to keep track of the variable, but it always stays the same.
This controller from the display component provides a text, which is from an input field, in an object.
app.controller("Test2Controller", function ($log, TestService) {
this.click = function () {
let that = this;
TestService.changeText({"text": that.text});
}
});
That is the the service, which gets the objekt and saves it into this.currentText.
app.service("TestService", function ($log) {
this.currentText = {};
this.changeText = function (obj) {
this.currentText = obj;
$log.debug(this.currentText);
};
this.getCurrentText = function () {
return this.currentText;
};
});
This is the controller which is supposed to then display the object, but even fails to update the this.text variable.
app.controller("TestController", function (TestService, $timeout, $log) {
let that = this;
this.$onInit = function () {
this.text = TestService.getCurrentText();
//debugging
this.update();
};
//debugging
this.update = function() {
$timeout(function () {
$log.debug(that.text);
that.update();
}, 1000);
}
//debugging
this.$onChanges = function (obj) {
$log.debug(obj);
}
});
I spent quite some time searching for an answer, but most are related to directives or didn't work in my case, such as one solution to put the object into another object. I figured that I could use $broadcast and $on but I have heard to avoid using it. The angular version I am using is: 1.6.9
I see a problem with your approach. You're trying to share the single reference of an object. You want to share object reference once and want to reflect it wherever it has been used. But as per changeText method, you're setting up new reference to currentText service property which is wrong.
Rather I'd suggest you just use single reference of an object throughout and it will take care of sharing object between multiple controllers.
Service
app.service("TestService", function ($log) {
var currentText = {}; // private variable
// Passing text property explicitly, and changing that property only
this.changeText = function (text) {
currentText.text = text; // updating property, not changing reference of an object
$log.debug(currentText);
};
this.getCurrentText = function () {
return currentText;
};
});
Now from changeText method just pass on text that needs to be changed to, not an new object.
In my AngularJS application, I have a controller-A and a factory. I am using the following code in factory to call the function in controller-A. In the initial call, the function in controller A's function executes 1 time; on the next call the controller-A's function executes 2 times. Hence the number of times executed get increased for each call. Is it possible to avoid this, please advise me. I have added the factory code and controller-A code below:
Factory code:
updateUserData: function (value, action) {
$("#myModalInsertUser").modal('hide');
var id = value.Id;
var params = {};
params.id = depotId;
$rootScope.selectedId = params;
$rootScope.$emit("EVENT_1", {id});
});
Controller-A code:
var listener = $rootScope.$on("EVENT_1", function(event, params, reload) {
$scope.confirmUserInfo(params);
});
$scope.confirmUserInfo = function(params) {
$('#myModalConfirmUser').modal('show');
$('#closeConfirmUser').unbind('click').click(function () {
$('#myModalConfirmUser').modal('hide');
var params = $rootScope.selectedId;
$scope.getUsers(params);
$scope.$on('$destroy', listener);
});
}
Attach the event listener to $scope and it will be automatically destroyed when the scope is destroyed:
̶v̶a̶r̶ ̶l̶i̶s̶t̶e̶n̶e̶r̶ ̶=̶ ̶$̶r̶o̶o̶t̶S̶c̶o̶p̶e̶.̶$̶o̶n̶(̶"̶E̶V̶E̶N̶T̶_̶1̶"̶,̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶(̶e̶v̶e̶n̶t̶,̶ ̶p̶a̶r̶a̶m̶s̶,̶ ̶r̶e̶l̶o̶a̶d̶)̶ ̶{̶
var deregisterFn = $scope.$on("EVENT_1", function(event, params, reload) {
$scope.confirmUserInfo(params);
});
$scope.confirmUserInfo = function(params) {
$('#myModalConfirmUser').modal('show');
$('#closeConfirmUser').unbind('click').click(function () {
$('#myModalConfirmUser').modal('hide');
var params = $rootScope.selectedId;
$scope.getUsers(params);
̶$̶s̶c̶o̶p̶e̶.̶$̶o̶n̶(̶'̶$̶d̶e̶s̶t̶r̶o̶y̶'̶,̶ ̶l̶i̶s̶t̶e̶n̶e̶r̶)̶;̶
});
}
The recommended practice is to broadcast events from $rootScope and receive them on the $scope interested in the event.
is it possible to destroy the listener before the scope gets destroyed?
To remove the listener, simply invoke the de-register function:
deregisterFn();
(function () {
'use strict';
describe('TestButton', function () {
var controller, element, util;
beforeEach(function () {
module('app');
module('template');
module('TestUtil');
inject(function (TestUtilService) {
util = TestUtilService;
var testFunc = function foo(){
return 5;
};
// Passing the defined function to the custom directive and call it via
// controller.clickButton (it will be fired there)
// function=\' + testFunc + '" passing the function as string also not working
element = '<custom-button id="btn1" label="Click Me" function="testFunc"></custom-button>';
element = util.compileElement(element);
controller = util.getController(element, 'customButton');
});
});
it('should be resolved', function () {
expect(element.html()).toContain('input');
});
it('should be set', function () {
expect(element.find('input').val()).toBe('Click Me');
});
it('should be set', function () {
expect(element.find('input').attr('id')).toContain('button_btn1_');
});
it('should fire the passed function', function () {
//controller.clickButton() should log 5 or something, but its not working at all
//controller itself is working perfect (I have access to the whole directive-scope)
});
});
TestUtilService :
this.compileElement = function (element) {
var el = vm.$compile(element)(vm.$scope);
vm.$scope.$digest();
return el;
};
this.getController = function (element, directiveName) {
return element.controller(directiveName);
}
How can I pass an object to the directive, compile it and access it. I also have problems with passing arrays or something. How does that work in Karma/Jasmine??
Would be wonderful, if someone could give me some tips.
From what I've experienced, you can't do this directly through the directive alone; however, if you want to add the functionality of the function (or the object), you can shim it in through whichever isolate scope object you're using in the test.
However, you should not be asserting on its existence. A test that fires your custom function will pass if the function is excluded:
scope.testFunc = function(){console.log(1)};
element = '<custom-button id="btn1" label="Click Me"></custom-button>';
This is because the scope already has the function it cares about.
One thing to do in this scenario is to set up a condition in which the function is passed in and is presumed to be working; effectively, you'd be injecting the function in for testing purposes. May be worth mocking at this point to ensure that it fires in the way you expect it to, but assertions beyond that are not going to be valid.
You have to define your passing object at the created scope in Karma/Jasmine like so:
scope.testFunc = function(){console.log(1)};
element = '<custom-button id="btn1" label="Click Me" function="testFunc"></custom-button>';
I have a special developer button on my form, so I don't have to enter the form data every time I am testing the app. The fake button calls the function fakeSubmit() and pre-fills the form.
fakeSubmit simply sets the form value local.code to '000000' and calls the actual submit() function:
scope.submit = function () {
if (scope.requestTicketForm.$invalid) {
scope.local.showErrorAlert = true;
}
else {
// Send request
myApi.getTicket(scope.local.code)
.then(function (data) {
scope.local.showErrorAlert = false;
})
.catch(function (error) {
scope.local.showErrorAlert = true;
});
}
};
scope.fakeSubmit = function () {
$log.info('enter fake request click');
scope.local.code = '000000';
scope.submit();
};
The problem now is that the form $invalid property is not updated immediately, so the submit function sets showErrorAlert to true instead of sending the request. I tried adding a scope.$apply before calling submit():
scope.fakeSubmit = function () {
$log.info('enter fake request click');
scope.local.code = '000000';
scope.$apply();
scope.submit();
};
This works, but it throws the following error:
Error: [$rootScope:inprog] $apply already in progress
Any ideas how to solve this?
Another way of forcing angular to update the scope is to call $digest:
if(!$scope.$$phase) {
$scope.$digest();
}
So your code should be:
scope.fakeSubmit = function () {
$log.info('enter fake request click');
scope.local.code = '000000';
if(!scope.$$phase) {
scope.$digest();
}
scope.submit();
};
When I load a view, I'd like to run some initialization code in its associated controller.
To do so, I've used the ng-init directive on the main element of my view:
<div ng-init="init()">
blah
</div>
and in the controller:
$scope.init = function () {
if ($routeParams.Id) {
//get an existing object
});
} else {
//create a new object
}
$scope.isSaving = false;
}
First question: is this the right way to do it?
Next thing, I have a problem with the sequence of events taking place. In the view I have a 'save' button, which uses the ng-disabled directive as such:
<button ng-click="save()" ng-disabled="isClean()">Save</button>
the isClean() function is defined in the controller:
$scope.isClean = function () {
return $scope.hasChanges() && !$scope.isSaving;
}
As you can see, it uses the $scope.isSaving flag, which was initialized in the init() function.
PROBLEM: when the view is loaded, the isClean function is called before the init() function, hence the flag isSaving is undefined. What can I do to prevent that?
When your view loads, so does its associated controller. Instead of using ng-init, simply call your init() method in your controller:
$scope.init = function () {
if ($routeParams.Id) {
//get an existing object
} else {
//create a new object
}
$scope.isSaving = false;
}
...
$scope.init();
Since your controller runs before ng-init, this also solves your second issue.
Fiddle
As John David Five mentioned, you might not want to attach this to $scope in order to make this method private.
var init = function () {
// do something
}
...
init();
See jsFiddle
If you want to wait for certain data to be preset, either move that data request to a resolve or add a watcher to that collection or object and call your init method when your data meets your init criteria. I usually remove the watcher once my data requirements are met so the init function doesnt randomly re-run if the data your watching changes and meets your criteria to run your init method.
var init = function () {
// do something
}
...
var unwatch = scope.$watch('myCollecitonOrObject', function(newVal, oldVal){
if( newVal && newVal.length > 0) {
unwatch();
init();
}
});
Since AngularJS 1.5 we should use $onInit which is available on any AngularJS component. Taken from the component lifecycle documentation since v1.5 its the preferred way:
$onInit() - Called on each controller after all the controllers on an
element have been constructed and had their bindings initialized (and
before the pre & post linking functions for the directives on this
element). This is a good place to put initialization code for your
controller.
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope) {
//default state
$scope.name = '';
//all your init controller goodness in here
this.$onInit = function () {
$scope.name = 'Superhero';
}
});
Fiddle Demo
An advanced example of using component lifecycle:
The component lifecycle gives us the ability to handle component stuff in a good way. It allows us to create events for e.g. "init", "change" or "destroy" of an component. In that way we are able to manage stuff which is depending on the lifecycle of an component. This little example shows to register & unregister an $rootScope event listener $on. By knowing, that an event $on bound on $rootScope will not be unbound when the controller loses its reference in the view or getting destroyed we need to destroy a $rootScope.$on listener manually.
A good place to put that stuff is $onDestroy lifecycle function of an component:
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope, $rootScope) {
var registerScope = null;
this.$onInit = function () {
//register rootScope event
registerScope = $rootScope.$on('someEvent', function(event) {
console.log("fired");
});
}
this.$onDestroy = function () {
//unregister rootScope event by calling the return function
registerScope();
}
});
Fiddle demo
Or you can just initialize inline in the controller. If you use an init function internal to the controller, it doesn't need to be defined in the scope. In fact, it can be self executing:
function MyCtrl($scope) {
$scope.isSaving = false;
(function() { // init
if (true) { // $routeParams.Id) {
//get an existing object
} else {
//create a new object
}
})()
$scope.isClean = function () {
return $scope.hasChanges() && !$scope.isSaving;
}
$scope.hasChanges = function() { return false }
}
I use the following template in my projects:
angular.module("AppName.moduleName", [])
/**
* #ngdoc controller
* #name AppName.moduleName:ControllerNameController
* #description Describe what the controller is responsible for.
**/
.controller("ControllerNameController", function (dependencies) {
/* type */ $scope.modelName = null;
/* type */ $scope.modelName.modelProperty1 = null;
/* type */ $scope.modelName.modelPropertyX = null;
/* type */ var privateVariable1 = null;
/* type */ var privateVariableX = null;
(function init() {
// load data, init scope, etc.
})();
$scope.modelName.publicFunction1 = function () /* -> type */ {
// ...
};
$scope.modelName.publicFunctionX = function () /* -> type */ {
// ...
};
function privateFunction1() /* -> type */ {
// ...
}
function privateFunctionX() /* -> type */ {
// ...
}
});