I am using UI bootstrap modal dialog box with angular js. Modal is successfully loaded. But when I click YES/NO Button, issued occurred & modal did not close.
Error said, ' $uibModal.close is not a function'.
.directive('confirm', function(ConfirmService) {
return {
restrict: 'A',
scope: {
eventHandler: '&ngClick'
},
link: function(scope, element, attrs){
element.unbind("click");
element.bind("click", function(e) {
ConfirmService.open(attrs.confirm, scope.eventHandler);
});
}
}
})
This is my service
.service('ConfirmService', function($uibModal) {
var service = {};
service.open = function (text, onOk) {
var modalInstance = $uibModal.open({
templateUrl: 'modules/confirmation-box/confirmation-box.html',
controller: 'userListCtrl',
resolve: {
text: function () {
return text;
}
}
});
modalInstance.result.then(function (selectedItem) {
onOk();
}, function () {
});
};
return service;
})
This is my controller file. I am trying to yes/no button inside the controller
.controller('userListCtrl',
['$scope','$http','appConfig','$uibModalInstance', '$uibModal','$log','alertService',
function ($scope,$http, appConfig,$uibModalInstance, $uibModal,$log,alertService) {
$scope.ok = function () {
$uibModalInstance.close();
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
}]);
You're attempting to use two usage methods at once time. There are two (probably more) that you can use the $uibModal, but here are the two that I believe you're intermixing:
1) Service controls the modal and returns a promise, I believe this is what I think you're doing. You do not need to call close/dismiss manually in this instance. You can make the following changes:
service.open = function(text, onOK) {
var modalInstance = $uibModal.open({
templateUrl: 'modules/confirmation-box/confirmation-box.html',
controller: 'userListCtrl',
resolve: {
text: function () {
return text;
}
}
});
// Return so you can chain .then just in case. Generally we don't even
// do this, we just return the instance itself and allow the controller to
// decide how to handle results/rejections
return modalInstance.result;
}
In your template file you'd have something like:
<button type="button" ng-click="$close(selectedItem)"></button>
<button type="button" ng-click="$dismiss(readon)"></button>
2) If you want to use the close method directly, then you only need to change the service to:
...
return $uibModal.open({});
then in your controller:
var modal = service.open('confirm');
modal.result.then(...)
modal.close()
Edit - updated with change to op to remove the antipattern as per georgeawg suggestion.
Related
$scope.openModal = function (page, size) {
console.log(page); // this is working
$uibModal.open({
animation: true,
templateUrl: 'app/pages/servers/newRole.html',
size: size,
resolve: {
items: function () {
return $scope.items;
}
}
});
};
$scope.hello = function() {
console.log(page); // need value of page from above function
var btn = document.createElement("BUTTON");
var t = document.createTextNode("ABC");
btn.classList.add("btn-primary", 'btn-xs', 'btn');
btn.appendChild(t);
document.getElementById('Id1').appendChild(btn);
}
I have tried using a global variable and assigning the page to this but it is saying undefined.
I can access this variable by calling hello(page) inside openModal, but that won't work as it will call hello() when it's not needed.
I have two buttons and calling openModal function on click of 'btn1', and passing page parameter on this button and then there is another button inside that modal 'btn2', calling hello() on click of btn2.
Tl;Dr : use your scope, or use a service.
If those methods are in the same controller:
You should just be able to pass it through a variable:
Initialize it : $scope.data = {};
Push some data in it: $scope.data = anything; or $scope.data.field = anything.
Use it further in your function : $scope.data ... \\ Do anything
If they're not:
You can use an AngularJS service. When you finished your data treatment, you can save it into your service, then getting it back later. View it as a getter/setter for any data you would like. Example:
A dummy AngularJS service:
var service = angular.module('yourService', [])
.factory('$yourService', function () {
var yourdata = {};
return {
setData: function(data) {
yourdata = data;
},
getData: function() {
return data;
}
};
});
return service;
Then, be sure that you inject it into your controller. Into your first function, you can call your service as:
$yourService.setData(anyData);
And get back the data in your second function: $yourService.getData();
$scope.openModal = function (page, size) {
console.log(page); // this is working
$uibModal.open({
animation: true,
templateUrl: 'app/pages/servers/newRole.html',
controller: ModalInstanceCtrl, //resolved items can be use in this controller
size: size,
resolve: {
items: function () {
return page; // this will return value
}
}
});
};
angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($scope, $uibModalInstance, items) {
$scope.hello = function() {
console.log(items); // Here you can get value
}
});
I have a custom directive to confirm if a user that clicks an element really wants to perform an action:
.directive('ngReallyClick', ['$modal',
function ($modal) {
return {
restrict: 'A',
scope: {
ngReallyClick: "&",
},
link: function (scope, element, attrs) {
var isDeleteDisabled = scope.ngReallyDisabled;
element.bind('click', function () {
if (isDeleteDisabled != true) {
var message = attrs.ngReallyMessage || "Are you sure ?";
...
var modalInstance = $modal.open({
template: modalHtml,
controller: ModalInstanceCtrl
});
modalInstance.result.then(function () {
scope.ngReallyClick({ ngReallyItem: scope.ngReallyItem }); //raise an error : $digest already in progress
}, function () {
//Modal dismissed
return false;
});
};
});
}
}
}]);
It is used e.g:
<a ng-really-message="Are you sure you want to save and close?" ng-really-click="saveAndCloseGlobal(456)"
where saveAndCloseGlobal is called when the user confirms their choice. But, if I try and pass $event to this function, to get the original click event, it ends up undefined. If I use a plain ng-click=saveAndCloseGlobal($event) then I get the correct event object in saveAndCloseGlobal.
I wanted to use a directive to have some click-to-edit functionality in my front end.
This is the directive I am using for that: http://icelab.com.au/articles/levelling-up-with-angularjs-building-a-reusable-click-to-edit-directive/
'use strict';
angular.module('jayMapApp')
.directive('clickToEdit', function () {
return {
templateUrl: 'directives/clickToEdit/clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I added a second attribute to the directive to call a method after when the user changed the value and then update the database etc. The method (´$onSave´ here) is called fine, but it seems the parent scope is not yet updated when I call the method at the end of the directive.
Is there a way to call the method but have the parent scope updated for sure?
Thanks in advance,
Michael
I believe you are supposed to create the functions to attach inside the linking function:
Take a look at this code:
http://plnkr.co/edit/ZTx0xrOoQF3i93buJ279?p=preview
app.directive('clickToEdit', function () {
return {
templateUrl: 'clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
link: function(scope, element, attrs){
scope.save = function(){
console.log('save in link fired');
}
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
console.log('save in controller fired');
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I haven't declared the functions inside the controller before, but I don't see why it wouldn't work.
Though this question/answer explain it Link vs compile vs controller
From my understanding:
The controller is used to share data between directive instances, not to "link" functions which would be run as callbacks.
The method is being called but angular doesn't realise it needs to run the digest cycle to update the controller scope. Luckily you can still trigger the digest from inside your isolate scope just wrap the call to the method:
$scope.$apply($scope.method());
I'm having problems trying to write a jasmine unit test for an Angular-Bootstrap $modal. The exact error is
Expected spy open to have been called with [ { templateUrl : '/n/views/consent.html', controller : 'W2ConsentModal as w2modal', resolve : { employee : Function }, size : 'lg' } ] but actual calls were [ { templateUrl : '/n/views/consent.html', controller : 'W2ConsentModal as w2modal', resolve : { employee : Function }, size : 'lg' } ]
The expected and actual modal options object are the same. What is going on?
Controller
(function () {
'use strict';
angular
.module('app')
.controller('W2History', W2History);
W2History.$inject = ['$scope', '$modal', 'w2Service'];
function W2History($scope, $modal, w2Service) {
/* jshint validthis:true */
var vm = this;
vm.showModal = showModal;
function showModal(employee) {
var modalInstance = $modal.open({
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
});
modalInstance.result.then(function (didConsent) {
// code omitted
});
}
}
})();
Test
describe('W2History controller', function () {
var controller, scope, modal;
var fakeModal = {
result: {
then: function (confirmCallback, cancelCallback) {
//Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog
this.confirmCallBack = confirmCallback;
this.cancelCallback = cancelCallback;
}
},
close: function (item) {
//The user clicked OK on the modal dialog, call the stored confirm callback with the selected item
this.result.confirmCallBack(item);
},
dismiss: function (type) {
//The user clicked cancel on the modal dialog, call the stored cancel callback
this.result.cancelCallback(type);
}
};
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
};
beforeEach(function () {
module('app');
inject(function (_$controller_, _$rootScope_, _$modal_) {
scope = _$rootScope_.$new();
modal = _$modal_;
spyOn(modal, 'open').and.returnValue(fakeModal);
controller = _$controller_('W2History', {
$scope: scope,
$modal: modal,
w2Service: w2Srvc
});
});
});
it('Should correctly show the W2 consent modal', function () {
var employee = terminatedaccessMocks.getCurrentUserInfo();
controller.showModal(employee);
expect(modal.open).toHaveBeenCalledWith(modalOptions);
});
});
Try this:
describe('W2History controller', function () {
var controller, scope, modal;
var fakeModal = {
result: {
then: function (confirmCallback, cancelCallback) {
//Store the callbacks for later when the user clicks on the OK or Cancel button of the dialog
this.confirmCallBack = confirmCallback;
this.cancelCallback = cancelCallback;
}
},
close: function (item) {
//The user clicked OK on the modal dialog, call the stored confirm callback with the selected item
this.result.confirmCallBack(item);
},
dismiss: function (type) {
//The user clicked cancel on the modal dialog, call the stored cancel callback
this.result.cancelCallback(type);
}
};
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: jasmine.any(Function)
},
size: 'lg'
};
var actualOptions;
beforeEach(function () {
module('plunker');
inject(function (_$controller_, _$rootScope_, _$modal_) {
scope = _$rootScope_.$new();
modal = _$modal_;
spyOn(modal, 'open').and.callFake(function(options){
actualOptions = options;
return fakeModal;
});
controller = _$controller_('W2History', {
$scope: scope,
$modal: modal
});
});
});
it('Should correctly show the W2 consent modal', function () {
var employee = { name : "test"};
controller.showModal(employee);
expect(modal.open).toHaveBeenCalledWith(modalOptions);
expect(actualOptions.resolve.employee()).toEqual(employee);
});
});
PLUNK
Explanation:
We should not expect the actual resolve.employee to be the same with the fake resolve.employee because resolve.employee is a function which returns an employee (in this case the employee is captured in closure). The function could be the same but at runtime the returned objects could be different.
The reason your test is failing is the way javascript compares functions. Take a look at this fiddle. Anyway, I don't care about this because we should not expect function implementations. What we do care about in this case is the resolve.employee returns the same object as we pass in:
expect(actualOptions.resolve.employee()).toEqual(employee);
So the solution here is:
We expect everything except for the resolve.employee:
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: jasmine.any(Function) //don't care about the function as we check it separately.
},
size: 'lg'
};
expect(modal.open).toHaveBeenCalledWith(modalOptions);
Check the resolve.employee separately by capturing it first:
var actualOptions;
spyOn(modal, 'open').and.callFake(function(options){
actualOptions = options; //capture the actual options
return fakeModal;
});
expect(actualOptions.resolve.employee()).toEqual(employee); //Check the returned employee is actually the one we pass in.
This is a pass by reference vs pass by value issue. The resolve.employee anonymous function used in $modal.open:
var modalInstance = $modal.open({
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
});
is not the same (by reference) as the resolve.employee anonymous function in your test:
var modalOptions = {
templateUrl: '/n/views/consent.html',
controller: 'W2ConsentModal as w2modal',
resolve: {
employee: function () {
return employee;
}
},
size: 'lg'
};
Your test should be:
resolve: {
employee: jasmine.any(Function)
}
If it's essential that the resolve function be tested, you should expose it somewhere where you can get a reference to the same function in your tests.
I am not sure if this will help you now, but when you spy on something you can get the argument that is passed to the $uibModal.open spy, you can then call that function to test that it returns what is in the resolve method.
it('expect resolve to be have metadataid that will return 9999', () => {
spyOn($uibModal, 'open');
//add test code here that will call the $uibModal.open
var spy = <jasmine.Spy>$uibModal.open;
var args = spy.calls.argsFor(0);
expect(args[0].resolve.metadataId()).toEqual(9999);
});
***** my code is using typescript, but this works for me.**
I have come across the same scenario. I have come across the problem with the below given solution
//Function to open export modal
scope.openExportModal();
expect( uibModal.open ).toHaveBeenCalledWith(options);
expect( uibModal.open.calls.mostRecent().args[0].resolve.modalData() ).toEqual(modalData);
Hope this may help if you want a quick fix.
What I am trying to do here is:
Type in the new language name and click "Add" button, the new language will be added into the existing object.
For example: the existing object: {"default": "English"}, When I type in "German", a new object is added like this: {"default": "English", "German": "German"}
Here is my PLUNKER.
Could someone help me on that? Thanks so much, I will appreciate!
I would prefer to use events. Just subscribe one piece on some event like:
$rootScope.$on('myEvent', function(event, info){
// do something
});
And another one will fire it:
scope.$broadcast('myEvent', info);
The system glitched when I was trying to save your plunkr or I don't have a permission so here the code:
var app = angular.module('plunker', ['ui.bootstrap']);
app.factory('Data', function(){
var data =
{
Language: ''
};
return {
setLanguage: function(language) {
data.Language = language;
}
}
})
var ModalDemoCtrl = function ($scope, $modal, $log) {
$scope.languages = {"sch": "Simple Chinese"};
$scope.$on('newLanguageAdded', function(e, lang){
$scope.languages[lang] = lang;
});
$scope.open = function () {
var modalInstance = $modal.open({
templateUrl: 'myModalContent.html',
controller: ModalInstanceCtrl,
resolve: {
languages: function () {
return $scope.languages;
},
newLanguage: function () {
return $scope.newLanguage;
}
}
});
};
};
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
var ModalInstanceCtrl = function ($scope, $modal, $modalInstance, languages, newLanguage) {
$scope.languages = languages;
$scope.ok = function () {
$modalInstance.close($scope.languages);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.openDialog = function () {
var modalInstance = $modal.open({
templateUrl: 'addNewLanguageDialog.html',
controller: AddNewLanguageCtrl,
});
}
var AddNewLanguageCtrl = function ($scope, $rootScope, $modalInstance, Data){
$scope.newValue = {text: ''};
$scope.$watch('newLanguage', function(newValue) {
if(newValue) Data.setLanguage(newValue);
});
$scope.add = function () {
alert($scope.newValue.text);
$rootScope.$broadcast('newLanguageAdded', $scope.newValue.text);
$modalInstance.close($scope.languages);
}
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
}
}
};
You can just copy this piece into plunkr instead yours.
Also change the layout:
<div class="modal-body">
<input ng-model="newValue.text">
</div>
Let me know if something doesn't work
You need to use a service, by definition singletons, and inject it in both models, adding a watch to the array in the service and updating accordingly in the scope of every model, from the values in the service.
An angular-ui way to achieve what you need would be to use these two basic methodologies found in the angular-ui documentation. See associated plunker for the answer below.
First is to use the close(result) inside the Instance Controller of the modal which updates the result promise property of the Instance Controller
Second is to use the result promise property to get the result(s) passed on to the close() method
Inside The AddNewLanguageCtrl is something like this
$scope.data = {newLanguage: ""};
$scope.add = function() {
$scope.close(data.newLanguage);
};
Inside the your addNewLanguageDialog.html script template
instead of using
<input ng-model="newLanguage">
use this
<input ng-model="data.newLanguage">
This is because whenever a modal is created, a new scope is created under the $rootScope(default) if a scope is not passed on to the options when the $modal.open() is invoked as stated in the angular-ui documentation. If you use newLanguage as the model then it won't receive any updates inside the AddNewLanguageCtrl. You can read this to get a better understanding of what I'm talking about regarding scopes
Inside the first modal ModalInstanceCtrl is something like this
$scope.newLanguages = [];
$scope.openDialog = function () {
var modalInstance = $modal.open({
templateUrl: 'addNewLanguageDialog.html',
controller: AddNewLanguageCtrl,
});
modalInstance.result.then(function(newLanguage) {
if(newLanguage)
$scope.newLanguages.push(newLanguage);
});
};
And then in your ModalDemoCtrl
$scope.languages = [];
$scope.open = function () {
var modalInstance = $modal.open({
templateUrl: 'myModalContent.html',
controller: ModalInstanceCtrl
});
modalInstance.result.then(function(languages) {
$scope.languages = $scope.languages.concat(languages);
});
};