I have below code:
vm.data = [{name: 'test-1'},{name: 'test-2'}];
function addRecords(data) {
vm.data.push(data);
}
function openPopup() {
$uibModal.open({
templateUrl: 'modal-popup/modal-popup.html',
controller: 'ModalPopupController',
controllerAs: 'vm',
resolve: {
id: _.constant('123')
}
}).result.then(addRecords);
}
Trying to mock this, Below are the declarations:
let allData = [{name: 'test-1'},{name: 'test-2'}];
let data = {name: 'test-3'};
beforeEach(inject(function (_$q_, _$rootScope_, _$componentController_, _$uibModal_) {
$q = _$q_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
controller = _$componentController_;
$uibModal = _$uibModal_;
spyOn($uibModal, 'open').and.returnValue({
result: function() {
return $q.when(data);
}
});
vm = controller('bvcListings', {
$q,
data: allData,
$uibModal
});
$scope.$apply();
}));
describe('openPopup', function () {
it('should add records on modal results', function () {
vm.openPopup();
expect($uibModal.open).toHaveBeenCalled();
});
});
Expectation is, it should add: {name: 'test-3'} as result to existing array.
Spy on modal open is working fine, but after results fetched, its not entering addRecords function. What am i doing wrong?
What changes need to be done here to get inside callback function after results retrieved.
.result.then callback method will get call only when you call modalInstance.close method, also don't forgot to pass data from close method something like modalInstance.close(data).
Before proceeding to test you need to do one change inside openPopup function. It should return $uibModal.open which basically returns newly created modal's instance. Thereafter you can easily have a control over modal to call dismiss/close method whenever needed.
function openPopup() {
vm.modalInstance = $uibModal.open({
templateUrl: 'modal-popup/modal-popup.html',
controller: 'ModalPopupController',
controllerAs: 'vm',
resolve: {
id: _.constant('123')
}
});
vm.modalInstance.result.then(addRecords);
}
Spec
$uibModal = _$uibModal_;
var data = {name: 'test-3'};
//creating fake modal which will help you to mock
var fakeModal = {
result: {
then: function(confirmCallback) {
//Store the callbacks
this.confirmCallBack = confirmCallback;
}
},
close: function( item ) {
//The user clicked OK on the modal dialog
this.result.confirmCallBack( item );
}
};
spyOn($uibModal, 'open').and.returnValue(fakeModal);
describe('It should data to vm.data when popup closed', function () {
it('should add records on modal results', function () {
vm.data = [{name: 'test-1'},{name: 'test-2'}];
let data = {name: 'test-3'};
vm.openPopup();
expect($uibModal.open).toHaveBeenCalled();
vm.modalInstance.close(data);
expect(vm.data.length).toBe(4);
expect(vm.data[3]).toBe(data);
});
});
Note: fakeModal has been referred from this post
Continuing with #Pankajs answer.
Here is a tweak which i made and got that worked.
function openPopup() {
vm.modalInstance = $uibModal.open({
templateUrl: 'modal-popup/modal-popup.html',
controller: 'ModalPopupController',
controllerAs: 'vm',
resolve: {
id: _.constant('123')
}
}).result.then(addRecords);
}
Spec
describe('modalpopup', function () {
it('should add records on modal results', function () {
vm.data = [{name: 'test-1'},{name: 'test-2'}];
let data = {name: 'test-3'};
vm.openPopup();
expect($uibModal.open).toHaveBeenCalled();
vm.modalInstance.close(data);
expect(vm.data.length).toBe(4);
expect(vm.data[3]).toBe(data);
});
});
Worked like charm for me. And i consier Pankajs answer as well which was almost 90% gave solution to my problem.
add $rootScope.$digest(); to resolve promises (like $q.when())
vm.openPopup();
expect($uibModal.open).toHaveBeenCalled();
$rootScope.$digest(); >> triggers your callback
Related
I'm trying to communicate two controllers.
var main = angular.module('starter', ["ionic", "ngCordova", "starter.services"]);
cart-ctrl.js
main.controller('CartCtrl',
["$scope", "global",
function($scope, global) {
$scope.$on("globalvar", function() {
//alert("from service cart: " + global.cart.items);
console.log("from service cart: " + global.cart.items);
$scope.carts = global.cart.items;
});
}]);
menu-ctrl.js
main.controller('AppCtrl',
["$scope", "$state", "global",
function($scope, $state, global) {
$scope.cart_click = function() {
global.updateCart();
$state.go('app.cart');
}
}]);
services.js
var service = angular.module("starter.services", []);
service.factory("global", ["$rootScope", "database",
function($rootScope, database) {
var service = {
cart: {
items: [],
count: 0
},
broadcastItem: function() {
$rootScope.$broadcast("globalvar");
},
updateCart: function() {
database.select_cart(function(p_cart) {
this.cart.items = p_cart;
alert("service cart: " + JSON.stringify(this.cart.items));
});
this.broadcastItem();
}
};
return service;
}]);
What I wanted to happen is when I click a the tab (which triggeres the cart_click()), the cart list will re-update. However no value is passed into CartCtrl. I wonder what's wrong in this code. service.cart.items has a value when I passed the value from the database.
I think we have 2 options.
You can $scope.cart_click => $rootScope.cart_click.
You can $emit, $broadcast and $on
And see more in https://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
I think you should call this.broadcastItem(); inside the callback of your database call. Also context of this inside the callback is not actually of the same service. Update your code as
updateCart: function() {
var self = this;
database.select_cart(function(p_cart) {
self.cart.items = p_cart;
self.broadcastItem();
alert("service cart: " + JSON.stringify(self.cart.items));
});
}
I am using ui-router to manage various states of my site. I have used resolve to pass data to header and home controller as displayed in following code. So now I need to update the value of resolved data from HomeController and this change should reflect across to HeaderController too.
var myapp = angular.module('myapp', ["ui.router"]);
myapp.service("DataService", [function() {
var data = { title: 'Some Title' };
this.get = function() {
return data;
};
}]);
myapp.controller("HeaderController", ["data", function(data) {
var vm = this;
vm.title = data.title;
}]);
myapp.controller("HomeController", ["data", function(data) {
var vm = this;
vm.title = data.title;
vm.updateTitle = function() {
// update the resolved data here so the header and home view
// updates with new data.title
data = { title: "Another title" };
// i know i can do vm.title = data.title; to update home view.
// But would be nice to globally update and reflect that change
// on all controllers sharing the same resolved data
};
}]);
myapp.config(function($stateProvider){
$stateProvider
.state({
name: "root",
abstract: true,
views: {
"header": {
templateUrl: "header.html",
controller: 'HeaderController as vm'
}
},
resolve: {
data: ['DataService', function(DataService) {
return DataService.get();
}]
}
})
.state({
name: "root.home",
url: "",
views: {
"content#": {
templateUrl: "home.html",
controller: "HomeController as vm"
}
}
})
});
PS:
Before looking into resolve, I was injecting service directly into the controller so please do not suggest on doing that.
EDIT: Plunkr updated and now works as expected.
Here is link to plunkr
Lesson Learnt:
Angular only watches the object that is assigned to the scope, and keeps separate reference of the objects. I mean:
data = { title: 'some title' };
vm.data = data;
vm.title = data.title;
data.title = 'another title';
{{vm.title}} // some title
/////////////////
data = { title: 'some title' };
vm.data = data;
data.title = 'another title';
{{vm.data.title}} // another title
You should take an advantage of the variable reference, where you should bind your HeaderController data to vm.data = data
Another incorrect thing is data = { title: "Another title" }; which would create an data object with new reference, and the reference of service object will lost. Instead of that you should do data.title = 'Another title';
header.html
{{vm.data.title}}
HeaderController
myapp.controller("HeaderController", ["data", function(data) {
var vm = this;
vm.data = data;
}]);
Update updateTitle method code to below.
vm.updateTitle = function() {
// update the resolved data here so the header updates with new data.title
vm.data.title = "Another title";
};
Demo here
I'd say that rather than playing with actual object reference, you should have setTitle function inside your factory, from updateTitle you will call that setter method which will update title. But in that case you need to again add the service reference on both controller. If its static data then there is no need to pass them by having resolve function. I'd loved to inject the service inside my controllers and then will play with data by its getter & setter.
Preferred Approach Plunkr
I am currently working on a small aplication using Angular.JS
In my view i have following button
<md-button class="md-primary" ng-click="editUser(user, $event)">Edit</md-button>
the editUser method looks something like this:
$scope.editUser = function (user, $event) {
$scope.userToEdit = user;
$mdDialog.show({
controller: DialogController,
targetEvent: $event,
templateUrl: '/js/modules/user/views/edit.tmpl.html',
parent: angular.element(document.body),
clickOutsideToClose: true,
scope: $scope
})
.
then(function (answer) {
if (answer == "save") {
for (right in $scope.allSystemRightsStatements) {
if ($scope.allSystemRightsStatements[right].selected) {
if( $scope.userToEdit.rights==null){
$scope.userToEdit.rights = [];
}
$scope.userToEdit.rights.push($scope.allSystemRightsStatements[right]);
}
}
$scope.updateUser($scope.userToEdit);
}
$scope.userToEdit = {};
}, function () {
$scope.userToEdit = {};
});
};
$scope.updateUser = function (user) {
//userService.updateUser makes a $http PUT request
var promise = userService.updateUser(user);
promise.then(function (result) {
$mdToast.show(
$mdToast.simple(result.message)
.position($scope.getToastPosition())
.hideDelay(3000)
);
}, function (reason) {
$mdToast.show(
$mdToast.simple(reason)
.position($scope.getToastPosition())
.hideDelay(3000)
);
}, function (update) {
});
};
Now the dialog is nicely shown and the answer function is also called, everything as expected.
However, when I click the button a second time the editUser funciton is not executed. As if the onClick event from the button had been removed at dialog close.
Any help on solving this problem is greatly appreciated,
Thanks
As said here
it is probably a good idea to explicitly mention that the scope will be destroyed upon hiding the dialog (so people shouldn't pass a controller's $scope directly).
(regarding the scope you are passing to mdDialog)
So, as the scope is destroyed, angular is not binding your button with any action
So I'm trying to get use to angular and having some troubles trying to call a directive (google maps https://github.com/davidepedone/angular-google-places-map) and performing reverse geocoding. I think this would be a more general directives questions though.
I am trying to call a function within the directive to update the google maps place information as well as map. The way I'm thinking in my head is that I would need to pass a variable through the controller, scope that variable to the directive and then the directive will run the function?
UPDATED:
<div class="row">
<places-map selectedid="selectid(place.id)"></places-map>
</div>
<button ng-click="selectid(place.id)">{{place.id}}</button> </div>
With this click I suppose to go to the controller,
$scope.selectid= function (pickplaceid){
$scope.selectedid(pickplaceid);
}
Then the selectplaceid should be in the scope variables of the directive.
scope: {
customCallback: '&?',
picked: '=?',
address: '=?',
fallback: '=?',
mapType: '#?',
readonly: '#?',
responsive: '#?',
draggable: '#?',
toggleMapDraggable: '=?',
placeNotFound: '=?',
updateMarkerLabel: '=?',
selectedid:'='
},
and can call my method as so:
link: function ($scope, element, attrs, controller) {
//everything else from angular-google-places
$scope.selectedid= function (selectedplace)
{
///Whatever I want to do to geocode with the placeid
}
I think I may just be doing this completely wrong having really no luck with the directive call at all. I'm trying to update my map based on the location that I click and pull out the information of that specific place from the placeId. Any help would be great.
I have almost same thing working, and I solved it with a Service that receives a placeId (in my code it's called addressId, but it's the placeId Google Maps expects). In my service, I use the placeId to retrieve address details:
app.service('AddressDetailsService', ['$q', function ($q) {
this.placeService = new google.maps.places.PlacesService(document.getElementById('map'));
this.getDetails = function (addressId, address) {
var deferred = $q.defer();
var request = {
placeId: addressId
};
this.placeService.getDetails(request, function (place, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
address.placeId = addressId;
address.street = getAddressComponent(place, 'route', 'long');
address.countryCode = getAddressComponent(place, 'country', 'short');
address.countryName = getAddressComponent(place, 'country', 'long');
address.cityCode = getAddressComponent(place, 'locality', 'short');
address.cityName = getAddressComponent(place, 'locality', 'long');
address.postalCode = getAddressComponent(place, 'postal_code', 'short');
address.streetNumber = getAddressComponent(place, 'street_number', 'short');
address.latitude = place.geometry.location.lat();
address.longitude = place.geometry.location.lng();
if (address.streetNumber) {
address.streetNumber = parseInt(address.streetNumber);
}
deferred.resolve(address);
}
});
return deferred.promise;
};
function getAddressComponent(address, component, type) {
var country = null;
angular.forEach(address.address_components, function (addressComponent) {
if (addressComponent.types[0] === component) {
country = (type === 'short') ? addressComponent.short_name : addressComponent.long_name;
}
});
return country;
}
}]);
Then you inject it and call the service from your directive. This is the one I use, you might need to adapt it, but you see the idea. Instead of a link function, I use a controller for the directive:
.directive('mdAddressDetails', function mdAddressDetails() {
var directive = {
restrict: 'EA',
scope: {
address: '='
},
bindToController: true,
templateUrl: 'modules/address/addressDetails.html',
controller: AddressDetailsController,
controllerAs: 'dir'
};
AddressDetailsController.$inject = ['AddressDetailsService', '$q'];
function AddressDetailsController(AddressDetailsService, $q) {
var dir = this;
dir.selectAddress = selectAddress;
function selectAddress(address) {
if ((address) && (address.place_id)) {
AddressDetailsService.getDetails(address.place_id, dir.address).then(
function (addressDetails) {
dir.address = addressDetails;
}
);
}
}
}
return directive;
});
And then you just call the directive with the wanted parameter:
<md-address-details address="myAddress"></md-address-details>
Here is my plnkr with my progress so far: http://plnkr.co/edit/iEHMUMlASZaqdMQUeF7J?p=preview
I'm having problems implementing the following functionality however.
When an item on the list is clicked, I need to disable the remaining items on the list. ie, another request should not take place, and these remaining items' colour should change to indicate the disabled state.
Once the request has taken place, then the entire list should go back to the original state.
Edit: I've made some progress. Although a bit messy it's getting me a bit closer. My problem is the following line:
$(this).parent().addClass('item-selected').children().unbind('click').removeClass('pending');
This prevents the click event running more than once at a time. However it's stopping the click event from running all together once its run for the first time. I would like to be able to re-run the process once it is complete an unlimited amount of times.
Directive:
app.directive('listItem', function (ListService, $timeout, $location) {
return {
restrict: 'ACE',
controller : 'ItemController',
template: '<p>{{item}} {{foo}}</p>',
link: function (scope, element, attrs) {
$(element).bind('click', function (e) {
$(this).parent().addClass('item-selected').children().unbind('click').removeClass('pending');
$(this).addClass('pending');
var elem = $(this);
$timeout(function () {
ListService
.selectItem(scope.item)
.then( function () {
console.log('success');
elem.removeClass('pending').addClass('success');
//$location.path('foo.html')
scope.foo = 'not bar';
}, function () {
console.log('error');
elem.removeClass('pending').addClass('error');
elem.parent().removeClass('item-selected');
});
;
}, 2000);
});
}
};
});
The entire app code including directive:
var app = angular.module('listtestApp', []);
app.service('ListService', function ($http) {
var data = [
'alpha',
'bravo',
'charlie',
'delta',
'foxtrot'
];
return {
getData : function () {
return data;
},
selectItem : function () {
return $http({ method: 'GET', url : '/data/list.json'});
}
}
});
app.controller('ListController', function ($scope, ListService) {
$scope.list = ListService.getData();
$scope.foo = 'Bar';
});
app.controller('ItemController', function ($scope, ListService) {
});
app.directive('listItem', function (ListService, $timeout, $location) {
return {
restrict: 'ACE',
controller : 'ItemController',
template: '<p>{{item}} {{foo}}</p>',
link: function (scope, element, attrs) {
$(element).bind('click', function (e) {
$(this).parent().addClass('item-selected').children().unbind('click').removeClass('pending');
$(this).addClass('pending');
var elem = $(this);
$timeout(function () {
ListService
.selectItem(scope.item)
.then( function () {
console.log('success');
elem.removeClass('pending').addClass('success');
//$location.path('foo.html')
scope.foo = 'not bar';
}, function () {
console.log('error');
elem.removeClass('pending').addClass('error');
});
;
}, 2000);
});
}
};
});
html markup below:
<body ng-app="listtestApp">
<div ng-controller="ListController">
<div ng-repeat="item in list" list-item>
</div>
</div>
</body>
You have several solutions at your disposal :
Check that any element has the pending or success or error class
use your function scope to store it in a variable
EDIT : if you want to re-enable selection after the request has been posted, you could use something like this (variant of version #1)