How to get hold of a value from modal popup angular js - angularjs

I have a input box in modal popup , i want that value in my main controller which invoked the modal.

Easy way:
//Modal controller
$rootScope.$broadcast('modal:nameOfModal:onSave', { data: data })
//Any other controller
$scope.$on('modal:nameOfModal:onSave', function(event, data) { ... } )
Right way:
Modal creator
Factory with methods representing each modal type you need
Each method method of Factory (like Factory.confirmationWindow()) returning a promise that should be handled separately in controllers that rise appropriate modal
Call something like in your controller
Controller.propotype.openModal = function() {
ModalsFactory.confirmationModal().then(function(result) {
//using result inside of your controller
//if clicked ok
}).catch(function() {
//behavior to be called when modal canceled
});
}
Pros:
Totally separates logic
Allow to define different behavior of same modals in different controllers
Cons:
ModalsFactory will be big file

Related

Returning data from angular $uibModal backdrop click

I'm trying to pass back a value from a uibModal. I can define the return if the user clicks the modal's close button
$scope.close = function () {
$modalInstance.close($scope.editMade);
};
But this doesn't work if the user click the backdrop area.
How can I define a return value for that particular event?
When you click on the backdrop outside, it does a dismiss internally.
Try using this inside modal:
$modalInstance.dismiss($scope.editMade);
And use this to handle data:
instance.result.then(function(){
//Get triggers when modal is closed
}, function(){
//gets triggers when modal is dismissed. You can basically handle data here
});
Check this working plunker. It uses the dismiss as I mentioned
http://embed.plnkr.co/YdWmqYPFBNbv4vhQZc4t/
Passing custom data to your parent controller when modal is dismissed:
Code in Modal Controller:
$scope.$on("modal.closing",function(){
$rootScope.$broadcast("modalClosing",$scope.editMade);
});
Then in your parent controller:
$scope.$on("modalClosing",function(event,value){
console.log(value); //value should be $scope.editMade
});
You can learn more by reading the documentation here:
https://github.com/angular-ui/bootstrap/tree/master/src/modal/docs
In the modal controller, you can do something like this:
$scope.$on('modal.closing', function(event, reason, closed) {
if (reason == "backdrop click" || reason == "escape key press")
{
event.preventDefault();
$uibModalInstance.close({"data": somedata});
}
});
This way you'll always get the success callback on modalInstance
modalInstance.result.then(function(response) {
// here response will be whatever is passed. in this sample {"data": somedata}
});

Passing data to sub-controller for insert or update

I have a main controller for my profile edit page, from where users can add their education, in a modal window. The modal has it's own controller. The user can add as many education items they want and they can modify the existing ones.
Now for my issue. I want the modal controller to take care of both adding a new item and updating an existing one. The difference is that on update, the controller should receive a fully populated education object, which it should update. I don't know how to pass this object from the main controller to the modal controller. I've seen some ways of passing data between controllers via services, but that seems too much of a hassle, and it doesn't make sense to me to use a service in this particular case.
My main controller creates modals like so:
vm.openEducation = function(){
$modal.open({
templateUrl: "some.html",
controller: "SomeController",
controllerAs: "vm",
size: 'lg'
}).result.then(function (education) {
vm.educations.push(education);
});
}
And the child controller (which currently only supports insert, hence the empty init of vm.education):
function SomeController($scope){
var vm = this;
vm.education = {}; // or get from parent controller
vm.save = function () {
$scope.$close(vm.education);
};
vm.close = function () {
$scope.$dismiss();
}
return vm;
}
You can use $rootScope.$broadcast to send data from the modal and then $scope.$on to receive that data anywhere else.
For example:
// in your modal
$rootScope.$broadcast('education:updated', yourDataObj)
// in your controller
$scope.$on('education:updated', function(e, data) {
console.log(data)
})
Note that the $broadcast will be picked up by anything that's listening for it, so if you have multiple controller instances you will get multiple console.log messages

Marker eventListener fails to give me the object I request

I am doing an app where when I click on a marker on a google.map triggers some action, specifically I want to get an object out of an array of objects to display the details of such object.
I use the controller defs like so
(function() {
'use strict';
angular
.module('gulpAngular')
.controller('GeolocationController', GeolocationController);
/** #ngInject */
function GeolocationController(HouseList) {
....
}
})();
The HouseList is a service defined elsewhere and having a method called getHouses()
Inside my controller, I do besides other things this:
var vm = this;
vm.houses = HouseList.getHouses();
Then I define my marker on the map like
allMarkers = new google.maps.Marker({....});
and add an Listener to it like below. To make things simple, I assign vm.house = vm.houses[0]
allMarkers.addListener('click', function() {
vm.house = vm.houses[0]
}
Now I suppose I should be able to use the vm.house object to display in my html block the attributes of this house object in the fashion of
<h4>{{vm.house.bedrooms}}</h4>
HOWEVER, NOTHING GETS DISPLAYED. I should see my vm.house object be updated and this reflected on the DOM, correct? What do I miss?
Funny: When i add a simple button on the html and use a ng-click function on it without anything other than say console.log(vm.house), not only it does display the correct object, but also the refresh of the html is happening. I am lost
thanks
Peter
addListener is not an Angular function and will not trigger the digest loop. You need to do it manually.
For example:
allMarkers.addListener('click', function() {
$scope.$apply(function () {
vm.house = vm.houses[0]
});
});
Note that you need to inject $scope for this.
ng-click triggers the digest loop, which is why using it will update the HTML.

How to remove Cordova specific events outside Angular controller?

Imagine I have a controller which handles, for example, view changes:
function Controller($scope){
var viewModel = this;
viewModel.goBack= function(){
viewModel.visible = visibleLinks.pop(); //get last visible link
viewModel.swipeDirection = 'left';// for view change animation
}
}
But I want to handle it not only for example with HTML buttons inside <body>, but also with Back button on device. So I have to add Event Listener for deviceready event, and also explicit call $scope.$apply() in order to fact, that it is called outside of AngularJS context, like this:
document.addEventListener("deviceready", function(){
document.addEventListener("backbutton", function(){
viewModel.goBack();
$scope.$apply();
}, false);
}, false);
}
But I also want to follow (relatively :) ) new controllerAssyntax, cause this is recommended now e.g. by Todd Motto: Opinionated AngularJS styleguide for teams and it allows to remove $scope from controllers when things like $emit or $on are not used. But I can't do it, case I have to call $apply() cause my context is not Angular context when user clicks on device back button. I thought about creating a Service which can be wrapper facade for cordova and inject $scope to this service but as I read here: Injecting $scope into an angular service function() it is not possible. I saw this: Angular JS & Phonegap back button event and accepted solution also contains $apply() which makes $scope unremovable. Anybody knows a solution to remove Cordova specific events outside Angular controller, in order to remove $scope from controllers when not explicity needed? Thank you in advance.
I don't see a reason why to remove the $scope from the controller. It is fine to follow the best practice and to remove it if not needed, but as you said you still need it for $emit, $on, $watch.. and you can add it $apply() in the list for sure.
What I can suggest here as an alternative solution is to implement a helper function that will handle that. We can place it in a service and use $rootScope service which is injectable.
app.factory('utilService', function ($rootScope) {
return {
justApply: function () {
$rootScope.$apply();
},
createNgAware: function (fnCallback) {
return function () {
fnCallback.apply(this, arguments);
$rootScope.$apply();
};
}
};
});
// use it
app.controller('SampleCtrl', function(utilService) {
var backBtnHandler1 = function () {
viewModel.goBack();
utilService.justApply(); // instead of $scope.$apply();
}
// or
var backBtnHandler2 = utilService.createNgAware(function(){
viewModel.goBack();
});
document.addEventListener("backbutton", backBtnHandler2, false);
});
In my case I was simply forwarding Cordova events with the help of Angular $broadcast firing it on the $rootScope. Basically any application controller would then receive this custom event. Listeners are attached on the configuration phase - in the run block, before any controller gets initialized. Here is an example:
angular
.module('app', [])
.run(function ($rootScope, $document) {
$document.on('backbutton', function (e) {
// block original system back button behavior for the entire application
e.preventDefault();
e.stopPropagation();
// forward the event
$rootScope.$broadcast('SYSTEM_BACKBUTTON', e);
});
})
.controller('AppCtrl', function ($scope) {
$scope.$on('SYSTEM_BACKBUTTON', function () {
// do stuff
viewModel.goBack();
});
});
Obviously in the $scope.$on handler you do not have to call $scope.$apply().
Pros of this solution are:
you'll be able to modify an event or do something else for the entire application before the event will be broadcasted to all the controllers;
when you use $document.on() every time controller is instantiated, the event handler stays in the memory unless you manually unsibscribe from this event; using $scope.$on cares about it automatically;
if the way a system dispatches Cordova event changes, you'll have to change it in one place
Cons:
you'll have to be careful when inheriting controllers which already have an event handler attached on initialization phase, and if you want your own handler in a child.
Where to place the listeners and the forwarder is up to you and it highly depends on your application structure. If your app allows you could even keep all the logic for the backbutton event in the run block and get rid of it in controllers. Another way to organize it is to specify a single global callback attached to $rootScope for example, which can be overriden inside controllers, if they have different behavior for the back button, not to mess with events.
I am not sure about deviceready event though, it fires once in the very beginning. In my case I was first waiting for the deviceready event to fire and then was manually bootstrapping AngularJS application to provide a sequential load of the app and prevent any conflicts:
document.addEventListener('deviceready', function onDeviceReady() {
angular.element(document).ready(function () {
angular.bootstrap(document.body, ['app']);
});
}, false);
From my point of view the logic of the app and how you bootstrap it should be separated from each other. That's why I've moved listener for backbutton to a run block.

Angular UI Bootstrap Module Not Closing on Back Button

I'm using a module from the UI Boostrap extensions (http://angular-ui.github.io/bootstrap). The module actually serves as a loading dialog and is automatically closed when a set of web service data is returned to my Angular code. As the data on this page is loaded automatically the dialog comes up immediately.
All this works great when I hit the page in question for the the first time or simply refresh it. The problem occurs when I go to a deeper page and then try and navigate back to the original page (with the dialog) via the browser's back button. The dialog never goes away despite all the fact that all the data is returned and the module's dismiss() call has been made.
I've traced this down to the promise to open the dialog appears to be happening after the dismiss call but, again, only when the page is loaded via the back button. The dismiss call never closes anything because it hasn't been added yet (I've confirmed this in the debugger).
The question I have is how could I handle this? Is there a solid way to catch the completion of the page loading via Angular and double check that the dialog closed? Is there a better way via UI Bootstrap's api?
I know this is rather unusual case but any thoughts on it would be great.
Thanks!
#HankScorpio's solution is good, but I think there may be a simplified option now.
There is no need to store the current modal anymore, if you register either a $locationChangeStart or $routeChangeStart listener with $uibModalStack injected and call $uibModalStack.dismissAll(). $locationChangeStart has the benefit of working for both ngRoute and uiRoute.
i.e. If only for the one page, then in your controller you'd have:
angular.module('app')
.controller('ctrl', ['$scope', '$uibModalStack', ctrl]);
function ctrl($scope, $uibModalStack) {
$scope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
If you want to do this for all pages then define this in a factory that is always loaded or just an app.run code segment:
angular.module('app')
.run(['$rootScope', '$uibModalStack', setupUibModal]);
setupUibModal($rootScope, $uibModalStack) {
$rootScope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
Here is the simple solution when using ui-router for state change
Closing modal popup on the back button click in angularjs
App.run(['$rootScope', '$modalStack', function ($rootScope, $modalStack) {
$rootScope.$on('$stateChangeStart', function (event) {
var top = $modalStack.getTop();
if (top) {
$modalStack.dismiss(top.key);
}
})
}]);
hope this will save lot of time for people who are breaking heads
I've run into this same problem. Here's how I fixed it.
1) Create a service to abstract the opening and closing of a modal and track which one is open (necessary for step 2). Instead of calling $modal.open() directly, call ModalService.open().
Here you go, you can have the one I wrote:
(function () {
'use strict';
var theModule = angular.module('services.modalService', ['ui.bootstrap']);
theModule.factory('ModalService', function ($modal) {
var service = {};
var currentModal;
var clearModal = function () {
currentModal = undefined;
};
service.getCurrentModal = function () {
return currentModal;
};
service.open = function (options) {
currentModal = $modal.open(options);
currentModal.result['finally'](clearModal);
return currentModal;
};
return service;
});
}());
2) In a controller, add an event listener to $routeChangeStart, this event will fire whenever someone hits the back button.
$scope.$on('$routeChangeStart', function(){
var currentModal = ModalService.getCurrentModal();
if(angular.isDefined(currentModal)){
currentModal.dismiss('cancel');
}
});
3) Your modals should now close when a user hits back.
4) Enjoy.
IMPROVEMENT:
I found the answer from HankScorpio to be the best out there. I wanted to include this snippet for those using ui-router and their recommendation for stateful modals.
1) I wanted the result.finally(...) to jump to a parent state;
2) I wanted to control the closing of the modal from $stateProvider config, NOT through rigging a controller and adding a listener to $routeChangeStart
Here is an example of a state that opens (and closes) it's modal:
.state('product.detail', {
url: '/detail/{productId}',
onEnter: /*open-modal logic*/,
onExit: ['ModalService', function (ModalService) { ModalService.close()} ]
})
I made ModalService aware of $state so that the result of closing a modal could jump to a parent view:
a. Add an isStateful flag to modalService.open(...):
service.open = function (options, isStateful) {
currentModal = $uibModal.open(options);
currentModal.result.finally(function () {
clearModal(isStateful);
});
return currentModal;
};
so that clearModal will return to previous state:
var clearModal = function (isStateful) {
currentModal = undefined;
if (isStateful)
$state.go('^');
};
Finally, add the closeModal() function called above (not a "stateful" close, simply a dismissal):
service.close = function() {
if (currentModal) {
currentModal.dismiss().then(function () {
clearModal();
})
}
}
The benefits of this are that back button functionality is controlled at state config level, not through a listener.

Resources