Calling methods from directive - javascript

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>

Related

How to mock results from uibmodal?

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

Error accessing AngularJS factory methods/arrays

I'm dividing my functions/objects into service and factory methods, and injecting them into my controller patentTab. I had a code for a tab panel which I originally placed within the controller patentTab that worked.
Now I have placed this code in a factory method and for some reason the content isn't loading. Console log shows no errors, and when I click the relative tab the correct URL is loaded, but the content doesn't change. Is there an issue with my array in the factory? If not, what is the reason?
Orginal code
app.controller('patentTab', function($scope, $http){
$scope.tabs = [{
title: 'Patent Information',
url: 'patent-info.htm'
}, {
title: 'Cost Analysis',
url: 'cost-analysis.htm'
}, {
title: 'Renewal History',
url: 'renewal-history.htm'
}];
$http.get('../json/patent-info.json').then(function(response){
$scope.patentData = response.data.patentInfo;
})
$scope.currentTab = 'patent-info.htm';
$scope.onClickTab = function (tab) {
$scope.currentTab = tab.url; //the tabs array is passed as a parameter from the view. The function returns the url property value from the array of objects.
}
$scope.isActiveTab = function(tabUrl) {
return tabUrl == $scope.currentTab;
}
});
New code (with issue)
app.controller('patentCtrl', ['$scope', '$http', 'patentTabFactory', function($scope, $http, patentTabFactory) {
$http.get('http://localhost:8080/Sprint002b/restpatent/').then(function(response) {
$scope.patents = response.data;
});
$scope.loadPatentItem = function(url) {
$scope.patentItem = url;
}
$scope.tabs = patentTabFactory.tabs;
$scope.currentTab = patentTabFactory.currentTab;
$scope.onClickTab = patentTabFactory.onClickTab;
$scope.isActiveTab = patentTabFactory.isActiveTab;
}]);
app.factory('patentTabFactory', function() {
var factory = {};
factory.tabs = [{
title: 'Patent Information',
url: 'patent-info.htm'
}, {
title: 'Cost Analysis',
url: 'cost-analysis.htm'
}, {
title: 'Renewal History',
url: 'renewal-history.htm'
}];
factory.currentTab = 'patent-info.htm';
factory.onClickTab = function (tab) {
factory.currentTab = tab.url; //the tabs array is passed as a parameter from the view. The function returns the url property value from the array of objects.
console.log(tab.url);
}
factory.isActiveTab = function(tabUrl) {
return tabUrl == factory.currentTab; //for styling purposes
}
return factory;
});
You not calling factory.onClickTab() method from your controller.
It should be like :
$scope.onClickTab = function(currentTab) {
patentTabFactory.onClickTab(currentTab);
$scope.currentTab = patentTabFactory.currentTab;
};
and, for isActiveTab, Like :
$scope.isActiveTab = patentTabFactory.isActiveTab(currentTab);
Here is a plunker where I am using a factory. The only changes I have done are:
1. Place the factory file before the app script file.
2. Use a separate declaration for factories and then inject it in the app.
var factories = angular.module('plunker.factory', []);
factories.factory('patentTabFactory', function() {
// Factory bits
};
I have injected the factories in the app.
var app = angular.module('plunker', ['plunker.factory']);
Here is a working plunker for that. PlunkR

Communicating between two controllers in angularjs 1

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));
});
}

Update ui-router resolved data across different states

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

AngularJS: how to bind a constant object to a directive

I've created a directive with a binding using "scope". In some cases, I want to bind a constant object. For instance, with HTML:
<div ng-controller="Ctrl">
<greeting person="{firstName: 'Bob', lastName: 'Jones'}"></greeting>
</div>
and JavaScript:
var app = angular.module('myApp', []);
app.controller("Ctrl", function($scope) {
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: {
person: "="
},
template:
'<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
Although this works, it also causes a JavaScript error:
Error: 10 $digest() iterations reached. Aborting!
(Fiddle demonstrating the problem)
What's the correct way to bind a constant object without causing the error?
Here's the solution I came up with, based on #sh0ber's answer:
Implement a custom link function. If the attribute is valid JSON, then it's a constant value, so we only evaluate it once. Otherwise, watch and update the value as normal (in other words, try to behave as a = binding). scope needs to be set to true to make sure that the assigned value only affects this instance of the directive.
(Example on jsFiddle)
HTML:
<div ng-controller="Ctrl">
<greeting person='{"firstName": "Bob", "lastName": "Jones"}'></greeting>
<greeting person="jim"></greeting>
</div>
JavaScript:
var app = angular.module('myApp', []);
app.controller("Ctrl", function($scope) {
$scope.jim = {firstName: 'Jim', lastName: "Bloggs"};
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: true,
link: function(scope, elements, attrs) {
try {
scope.person = JSON.parse(attrs.person);
} catch (e) {
scope.$watch(function() {
return scope.$parent.$eval(attrs.person);
}, function(newValue, oldValue) {
scope.person = newValue;
});
}
},
template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
You are getting that error because Angular is evaluating the expression every time. '=' is for variable names.
Here are two alternative ways to achieve the same think without the error.
First Solution:
app.controller("Ctrl", function($scope) {
$scope.person = {firstName: 'Bob', lastName: 'Jones'};
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: {
person: "="
},
template:
'<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
<greeting person="person"></greeting>
Second Solution:
app.directive("greeting2", function () {
return {
restrict: "E",
replace: true,
scope: {
firstName: "#",
lastName: "#"
},
template:
'<p>Hello {{firstName}} {{lastName}}</p>'
};
});
<greeting2 first-name="Bob" last-Name="Jones"></greeting2>
http://jsfiddle.net/7bNAd/82/
Another option:
app.directive("greeting", function () {
return {
restrict: "E",
link: function(scope,element,attrs){
scope.person = scope.$eval(attrs.person);
},
template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
This is because if you use the = type of scope field link, the attribute value is being observed for changes, but tested for reference equality (with !==) rather than tested deeply for equality. Specifying object literal in-line will cause angular to create the new object whenever the atribute is accessed for getting its value — thus when angular does dirty-checking, comparing the old value to the current one always signals the change.
One way to overcome that would be to modify angular's source as described here:
https://github.com/mgonto/angular.js/commit/09d19353a2ba0de8edcf625aa7a21464be830f02.
Otherwise, you could create your object in the controller and reference it by name in the element's attribute:
HTML
<div ng-controller="Ctrl">
<greeting person="personObj"></greeting>
</div>
JS
app.controller("Ctrl", function($scope)
{
$scope.personObj = { firstName : 'Bob', lastName : 'Jones' };
});
Yet another way is to create the object in the parent element's ng-init directive and later reference it by name (but this one is less readable):
<div ng-controller="Ctrl" ng-init="personObj = { firstName : 'Bob', lastName : 'Jones' }">
<greeting person="personObj"></greeting>
</div>
I don't particularly like using eval(), but if you really want to get this to work with the HTML you provided:
app.directive("greeting", function() {
return {
restrict: "E",
compile: function(element, attrs) {
eval("var person = " + attrs.person);
var htmlText = '<p>Hello ' + person.firstName + ' ' + person.lastName + '</p>';
element.replaceWith(htmlText);
}
};
});
I had the same problem, I solved it by parsing the json in the compile step:
angular.module('foo', []).
directive('myDirective', function () {
return {
scope: {
myData: '#'
},
controller: function ($scope, $timeout) {
$timeout(function () {
console.log($scope.myData);
});
},
template: "{{myData | json}} a is {{myData.a}} b is {{myData.b}}",
compile: function (element, attrs) {
attrs['myData'] = angular.fromJson(attrs['myData']);
}
};
});
The one drawback is that the $scope isn't initially populated when the controller first runs.
Here's a JSFiddle with this code.

Categories