Changes event does not trigger in angular directive - angularjs

I'm writing an angular directive. Here is the code:
'use strict';
app.directive('mcategory', function (MainCategory, $rootScope) {
return {
restrict: 'E',
replace: true,
template: '<select data-ng-model="selectedMainCategory" data-ng-options="mainCategory.mainCategoryId as mainCategory.mainCategoryName ' +
'for mainCategory in mainCategories" data-ng-change="mainCategoryChanged()"></select>',
link: function(scope, element, attr, controller) {
var elm = angular.element(element);
elm.attr('id', attr['id']);
elm.attr('name', attr['name']);
elm.attr('class', attr['class']);
MainCategory.get().then(function (mainCategories) {
scope.mainCategories = mainCategories;
});
scope.selectedMainCategory = false;
scope.mainCategoryChanged = function () {
if (!scope.selectedMainCategory) {
return;
}
$rootScope.$broadcast("mainCategoryChanged", { mainCategory: scope.selectedMainCategory });
};
}
};
})
MainCategory is a service and it works ok.
The problem is that selectedMainCategory is always undefined and mainCategoryChanged() event is not triggered when user selects another category in the select element. I think I have made a silly mistake but it is more than 2 hours that I can't solve the problem. Any help is appreciated in advance.
UPDATE
Here is the service:
'use strict';
app.factory('MainCategory', function ($http, $q) {
return {
get: function() {
var deferred = $q.defer();
$http.get('/MainCategory/GetMainCategories').success(deferred.resolve).error(deferred.reject );
return deferred.promise;
}
};
})
When I '/MainCategory/GetMainCategories' url in the browser I get all the categories in the correct json format.

Related

How can I use a function from a directive inside a controller function?

I'm a newbie in AngularJS and I'm trying to use the goToUserLocation() function from the userLocation directive in the getMyPosition() function in the MapController. The reason I want to do this is that once the user clicks a button I want to take his location, zoom in to 40 and place it at the center of the map.
Can someone please help me with this?
Thank you.
Here are the directive, the controller and a service that the directive uses:
(function ()
{
'use strict';
angular
.module('app.map')
.directive('userLocation', userLocation);
function userLocation(geolocation)
{
return {
restrict: "A",
link: function(scope, element, attrs){
var ctrl = scope['vm'];
var goToUserLocation = UserLocationFactory();
goToUserLocation(ctrl, geolocation, ctrl.defaultLocation );
}
};
}
//Factory to build goToUserLocation function with callbacks configuration
function UserLocationFactory(cantGetLocation, notSupportedBrowser)
{
return goToUserLocation;
//function to make the map go to user location
function goToUserLocation(vm, geolocation, defaultLocation)
{
resolveErrorCallbacks();
geolocation.goToUserLocation(success, cantGetLocation, notSupportedBrowser);
function success(position)
{
var location = {
lat: position.lat,
lng: position.lng,
zoom: 15
};
configureLatlng(location);
}
function configureLatlng(location)
{
vm.map = {
center: location
};
}
function resolveErrorCallbacks(){
if( !cantGetLocation || !( typeof cantGetLocation === "function" ) )
{
cantGetLocation = function()
{
configureLatlng(defaultLocation);
};
}
if( !notSupportedBrowser || !( typeof notSupportedBrowser === "function" ) )
{
notSupportedBrowser = function()
{
configureLatlng(defaultLocation);
};
}
}
}
}
})();
Here is the controller:
(function ()
{
'use strict';
angular
.module('app.map')
.controller('MapController', MapController);
/** #ngInject */
function MapController($mdDialog, $mdSidenav, $animate, $timeout, $scope, $document, MapData,
leafletData, leafletMapEvents, api, prototypes, $window, appEnvService, geolocation) {
var vm = this;
vm.getMyPosition = getMyPosition;
function getMyPosition(){
console.log("Here is your location.");
}
Here is a service that the directive us:
(function() {
'use strict';
angular
.module('app.map')
.service('geolocation', geolocation);
function geolocation() {
/*
success - called when user location is successfully found
cantGetLocation - called when the geolocation browser api find an error in retrieving user location
notSupportedBrowser - callend when browser dont have support
*/
this.goToUserLocation = function(success, cantGetLocation, notSupportedBrowser)
{
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(currentPosition, function() {
cantGetLocation();
});
}
else
{
// Browser doesn't support Geolocation
notSupportedBrowser();
}
function currentPosition(position)
{
var pos =
{
lat: position.coords.latitude,
lng: position.coords.longitude
};
success(pos);
}
};
}
})();
To call a directive function from a controller you can bind a object from the controller onto the directive using the scope '='.
<body ng-controller="MainCtrl">
<div my-directive control="directiveControl"></div>
<button ng-click="directiveControl.foo()">Call Directive Func</button>
<p>{{directiveControl.fooCount}}</p>
</body>
then
app.controller('MainCtrl', function($scope) {
$scope.directiveControl = {};
});
app.directive('myDirective', function factory() {
return {
restrict: 'A',
scope: {
control: '='
},
link: function(scope, element, attrs) {
scope.directiveControl = scope.control ? scope.control : {};
scope.directiveControl.fooCount=0;
scope.directiveControl.foo = function() {
scope.directiveControl.fooCount++;
}
}
};
});
See this example:
https://plnkr.co/edit/21Fj8dvfN0WZ3s9OUApp?p=preview
and this question:
How to call a method defined in an AngularJS directive?

AngularJS Factory object not being updated on controller and view

I'm with a problem with binding an object of a Factory and a Controller and it's view.
I am trying to get the fileUri of a picture selected by the user. So far so good. The problem is that I am saving the value that file to overlays.dataUrl. But I am referencing it on the view and it isn't updated. (I checked and the value is actually saved to the overlays.dataUrl variable.
Here goes the source code of settings.service.js:
(function () {
"use strict";
angular
.module("cameraApp.core")
.factory("settingsService", settingsService);
settingsService.$inject = ["$rootScope", "$cordovaFileTransfer", "$cordovaCamera"];
function settingsService($rootScope, $cordovaFileTransfer, $cordovaCamera) {
var overlays = {
dataUrl: "",
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.FILE_URI
}
};
var errorMessages = [];
var service = {
overlays: overlays,
selectOverlayFile: selectOverlayFile,
errorMessages: errorMessages
};
return service;
function selectOverlayFile() {
$cordovaCamera.getPicture(overlays.options).then(successOverlay, errorOverlay);
}
//Callback functions
function successOverlay(imageUrl) {
//If user has successfully selected a file
var extension = "jpg";
var filename = getCurrentDateFileName();
$cordovaFileTransfer.download(imageUrl, cordova.file.dataDirectory + filename + '.' + extension, {}, true)
.then(function (fileEntry) {
overlays.dataUrl = fileEntry.nativeURL;
}, function (e) {
errorMessages.push(e);
});
}
function errorOverlay(message) {
//If user couldn't select a file
errorMessages.push(message);
//$rootScope.$apply();
}
}
})();
Now the controller:
(function () {
angular
.module("cameraApp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = ["settingsService"];
function SettingsController(settingsService) {
var vm = this;
vm.settings = settingsService;
activate();
//////////////////
function activate(){
// Nothing here yet
}
}
})();
Finnally on the view:
<h1>{{vm.settings.overlays.dataUrl}}</h1>
<button id="overlay" class="button"
ng-click="vm.settings.selectOverlayFile()">
Browse...
</button>
Whenever I change the value in the factory, it doesn't change in the view.
Thanks in advance!
Unfortunately Factories in angularjs are not meant to be used as two way bindings. Factories and Services are only singletons. They are only there to be used when called.
Ex Factory:
app.factory('itemFactory', ['$http', '$rootScope', function($http, $rootScope) {
var service = {};
service.item = null;
service.getItem = function(id) {
$http.get(baseUrl + "getitem/" + id)
.then(function successCallback(resp) {
service.item = resp.data.Data;
$rootScope.$broadcast("itemready");
}, function errorCallback(resp) {
console.log(resp)
});
};
return service;
}]);
I use the $broadcast so if I call getItem my controller knows to go get the fresh data.
Ex Directive:
angular.module("itemApp").directive("item", ['itemFactory', '$routeParams', '$location', '$rootScope', '$timeout', function (itemFactory, $routeParams, $location, $rootScope, $timeout) {
return {
restrict: 'E',
templateUrl: "components/item.html",
link: function (scope, elem, attr) {
scope.item = itemFactory.item;
scope.changeMade = function(){
itemFactory.getItem(1);
}
scope.$on("itemready", function () {
scope.item = itemFactory.item;
})
}
}
}]);
So as you can see in my code above anytime I need a fresh item I use $broadcast and $on to update my service and directive. I hope this makes sense, feel free to ask any questions.
As pointed by Ohjay44, the factory is not updated on the view. The way to do it is using a directive (also as Ohjay44 said). To use $broadcast, $emit and $on and keep the encapsulation I did what is recommended by John Papa's Angular Style Guide: created a factory (in my case a named it comms).
Here goes the newly created directive (overlay.directive.js):
(function () {
angular
.module('cameraApp.settings')
.directive('ptrptSettingsOverlaysInfo', settingsOverlaysInfo);
settingsOverlaysInfo.$inject = ["settingsService", "comms"];
function settingsOverlaysInfo(settingsService, comms) {
var directive = {
restrict: "EA",
templateUrl: "js/app/settings/overlays.directive.html",
link: linkFunc,
controller: "SettingsController",
controllerAs: "vm",
bindToController: true // because the scope is isolated
};
return directive;
function linkFunc(scope, element, attrs, vm) {
vm.overlays = settingsService.overlays;
comms.on("overlaysUpdate", function (event, overlays) {
vm.overlays = overlays;
});
}
}
})();
I created overlay.directive.html with:
<div class="item item-thumbnail-left">
<img ng-src="{{vm.overlays.dataUrl}}">
<h2>{{vm.overlays.dataUrl}}</h2>
</div>
And finally I put an $emit on the settingsService where the overlay is updated:
(function () {
"use strict";
angular
.module("cameraApp.core")
.factory("settingsService", settingsService);
settingsService.$inject = ["comms", "$cordovaFileTransfer", "$cordovaCamera"];
function settingsService(comms, $cordovaFileTransfer, $cordovaCamera) {
var overlays = {
dataUrl: "",
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.FILE_URI
}
};
var errorMessages = [];
var service = {
overlays: overlays,
selectOverlayFile: selectOverlayFile,
errorMessages: errorMessages
};
return service;
function selectOverlayFile() {
$cordovaCamera.getPicture(overlays.options).then(successOverlay, errorOverlay);
}
//Callback functions
function successOverlay(imageUrl) {
//If user has successfully selected a file
var extension = "jpg";
var filename = getCurrentDateFileName();
$cordovaFileTransfer.download(imageUrl, cordova.file.dataDirectory + filename + '.' + extension, {}, true)
.then(function (fileEntry) {
overlays.dataUrl = fileEntry.nativeURL;
// New code!!!!
comms.emit("overlaysUpdated", overlays);
}, function (e) {
errorMessages.push(e);
});
}
function errorOverlay(message) {
//If user couldn't select a file
errorMessages.push(message);
//$rootScope.$apply();
}
}
})();
I used an $emit instead of a broadcast to prevent the bubbling as explained here: What's the correct way to communicate between controllers in AngularJS?
Hope this helps someone else too.
Cheers!

Why my value is not updated in the directive's view

I have a value named $scope.title in my controller. This value is initialized with $scope.title = 'global.loading';. I have a factory named Product.
My view is calling a directive via <menu-top ng-title="title"></menu-top>, the view of this directive is <span>{{title|translate}}</span>.
When I want to get a product I do : Product.get(id). Their is two possibility.
First one (working) -> My product is cached in localstorage and my title in the directive is uptated.
Second one (not working) -> My product is not cached, I call my WebService, put the response in cache and return the response. In this case, the title is updated (console.log) in the controller, but not in my directive ...
angular.module('angularApp')
.directive('menuTop', function () {
return {
templateUrl: 'views/directives/menutop.html',
restrict: 'E',
scope:{
ngTitle: '=?'
},
link: function postLink(scope) {
scope.title = scope.ngTitle;
}
};
});
angular.module('angularApp')
.controller('ProductCtrl', function ($scope, $routeParams, Product) {
$scope.productId = parseInt($routeParams.product);
$scope.title = 'global.loading';
$scope.loading = true;
$scope.error = false;
$scope.product = null;
Product
.get($scope.productId)
.then(function(product){
$scope.loading = false;
$scope.title = product.name;
$scope.product = product;
}, function(){
$scope.error = true;
$scope.loading = false;
})
;
});
angular.module('angularApp')
.factory('Product', function ($http, responseHandler, ApiLink, LocalStorage, $q) {
var _get = function(id) {
return $q(function(resolve, reject) {
var key = 'catalog/product/' + id;
var ret = LocalStorage.getObject(key);
if (ret) {
return resolve(ret);
}
responseHandler
.handle($http({
method: 'GET',
url: ApiLink.get('catalog', 'product', {id: id})
}))
.then(function(response) {
if (response.product && response.product.name) {
LocalStorage.putObject(key, response.product, 60 * 5);
return resolve(response.product);
}
reject(null);
}, function() {
reject(null);
});
});
};
return {
'get': _get
};
});
Thank you for your help !
As Sergio Tulentsev suggested, you can use '#' as binding method.
Using # will interpolate the value. It means that you can use it as a readonly this way : ng-title="{{mytitle}}"
angular.module('angularApp')
.directive('menuTop', function () {
return {
templateUrl: 'views/directives/menutop.html',
restrict: 'E',
scope:{
ngTitle: '#'
},
link: function postLink(scope) {
scope.title = scope.ngTitle;
}
};
});
Also keep in mind that you shouldn't use "ng" for your custom directives. ng is used for angular natives components. You can (should) keep this naming convention with your application name. Like for an application "MyStats" your could name your components ms-directivename
If you need more informations about the directives bindings you can refer to this documentation

How can I return error context information from an AngularJS async validator?

I'm using the new AngularJS async validators feature introduced in 1.3. I have a directive that looks like this:
angular.module('app')
.directive('usernameValidator', function(API_ENDPOINT, $http, $q, _) {
return {
require: 'ngModel',
link: function($scope, element, attrs, ngModel) {
ngModel.$asyncValidators.username = function(username) {
return $http.get(API_ENDPOINT.user, {userName: username})
.then(function(response) {
var username = response.data;
if (_.isEmpty(username)) {
return true;
} else {
return $q.reject(username.error);
}
}, function() {
return $q.reject();
});
};
}
};
});
I'd like to somehow get the value of username.error into the model controller scope so I can display it to the user. Displaying a static message is easy, however I want to display some of the error context information returned by the server as well.
Is there a clean way to do this or am I stuck with setting properties on the model controller?
Edit: To clarify, I am not looking for a one-off solution that just works. I intend to use this directive as a reusable, cleanly encapsulated component. This means directly writing to the surrounding scope or anything like that is probably not acceptable.
the validation directive is just like any other directive, you have access to $scope, so why not set the value as it: $scope.errors.username = username.error;
angular.module('app')
.directive('usernameValidator', function(API_ENDPOINT, $http, $q, _) {
return {
require: 'ngModel',
link: function($scope, element, attrs, ngModel) {
$scope.errors = $scope.errors | {}; //initialize it
ngModel.$asyncValidators.username = function(username) {
return $http.get(API_ENDPOINT.user, {userName: username})
.then(function(response) {
var username = response.data;
if (_.isEmpty(username)) {
return true;
} else {
$scope.errors.username = username.error; //set it here
return $q.reject(username.error);
}
}, function() {
return $q.reject();
});
};
}
};
});
I just initialized it separately $scope.errors = $scope.errors | {}; //initialize it so that you can reuse $scope.errors object in multiple directives if you wish it
Why not have a global alerts array of alerts that you can push an error onto.
<alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)"><span ng-class="alert.icon"></span> {{alert.msg}}</alert>
Doing this, the alert can be a success or warning or whatever. And can be called from the global scope. Which I think is good so an async task in some random place you called can place an alert into the stack. Just an idea....
You can pass an empty variable, and set it on reject:
angular.module('app')
.directive('usernameValidator', function(API_ENDPOINT, $http, $q, _) {
return {
require: 'ngModel',
scope: {
errorMessage: '=usernameValidator'
},
link: function($scope, element, attrs, ngModel) {
ngModel.$asyncValidators.username = function(username) {
return $http.get(API_ENDPOINT.user, {userName: username})
.then(function(response) {
var username = response.data;
if (_.isEmpty(username)) {
return true;
} else {
//set it here
$scope.errorMessage = username.error;
return $q.reject(username.error);
}
}, function() {
return $q.reject();
});
};
}
};
});
And in your template:
<input
ng-model="model.email"
username-validation="userValidationError"
name="email"
type="text">
<p class="error" ng-if="form.email.$error">{{userValidationError}}</p>

Unit test angular directive that uses ngModel

I am trying to unit test a directive that uses ngModel and having difficulties. It seems that the link function of my directive is never being called...
Here is my directive code:
coreModule.directive('coreUnit', ['$timeout', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
template: "{{output}}",
link: function (scope, elem, attrs, ngModelCtrl) {
ngModelCtrl.$render = function () {
render(ngModelCtrl.$modelValue);
};
console.log("called");
function render(unit) {
if (unit) {
var output = '(' +
unit.numerator +
(unit.denominator == '' ? '' : '/') +
unit.denominator +
(unit.rate == 'NONE' || unit.rate == '' ? '' : '/' + unit.rate) +
')';
scope.output = output == '()' ? '' : output;
}
}
}
}
}]);
Here is my test spec:
describe('core', function () {
describe('coreUnitDirective', function () {
beforeEach(module('core'));
var scope,
elem;
var tpl = '<core-unit ng-model="myUnit"></core-unit>';
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.myUnit = {};
elem = $compile(tpl)(scope);
scope.$digest();
}));
it('the unit should be empty', function () {
expect(elem.html()).toBe('');
});
it('should show (boe)', function () {
scope.myUnit = {
numerator: 'boe',
denominator: "",
rate: ""
};
scope.$digest();
expect(elem.html()).toContain('(boe)');
});
});
});
The console log output "called" is never occurring and obviously the elem in my test spec is never updating.
What am I doing wrong??
Turns out that I wasn't including the directive in my karma.config file :S. Adding it in resolved all of my issues.
You can try out two things.
First, instead of using just a string tpl, try angular.element().
var tpl = angular.element('<core-unit ng-model="myUnit"></core-unit>');
Second, place the tpl in the beforeEach block. So the result should look like this:
beforeEach(inject(function ($rootScope, $compile) {
var tpl = angular.element('<core-unit ng-model="myUnit"></core-unit>');
scope = $rootScope.$new();
scope.myUnit = {};
elem = $compile(tpl)(scope);
scope.$digest();
}));

Resources