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

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

Related

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!

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>

Hide form while loading data

In my AngularJS app I want to hide some page elements (sometimes all of them) until all async data is loaded.
Any suggestions on how to solve that when I have several data requests in the same controller?
I also would like to combine it with http://victorbjelkholm.github.io/ngProgress and find a solution with a service that listens to the status of dataLoaded (or whatever solution is best) that I can use in all different controllers of the app.
controller.js
$scope.dataLoaded = false;
dataFactory.getProduct(accessToken, storeId).then(function (response) {
$scope.formData = response.data;
$scope.dataLoaded = true;
}, function (error) {
console.log('Error: dataFactory.getProduct');
});
dataFactory.getBrand().then(function (response) {
$scope.brandData = response.data;
$scope.dataLoaded = true;
}, function (error) {
console.log('Error: dataFactory.getBrand');
});
service.js
app.factory("dataFactory", function ($http) {
var factory = {};
factory.getProducts = function (accessToken, storeId) {
return $http.get('data/getProducts.aspx?accessToken=' + accessToken + '&storeId=' + storeId)
};
factory.getProduct = function (accessToken, storeId, productId) {
return $http.get('data/getProduct.aspx?accessToken=' + accessToken + '&storeId=' + storeId + '&productId=' + productId)
};
factory.getBrand = function (accessToken, storeId) {
return $http.get('data/getBrand.aspx?accessToken=' + accessToken + '&storeId=' + storeId)
};
return factory;
});
what you can do is to resolve that function using your route provider with a combination of ng-cloak class that works like a charm, and also you can hide and show a "loader image" while you are switching routes.
let's say you have this service in your app:
app.factory("greetingService", function($q, $timeout){
return {
getGreeting: function(){
var deferred = $q.defer();
$timeout(function(){
deferred.resolve("Allo!");
},2000);
return deferred.promise;
}
}
});
setting up the "resolve" function
.when("/anyroute", {
templateUrl: "anyview.html",
controller: "anyController",
resolve: {
greeting: function(greetingService){
return greetingService.getGreeting();
}
}
})
in your controller:
app.controller("anyController", function ($scope, greeting) {
$scope.greeting = greeting;
});
you can inject your "resolved" dependency right before your controller, that way you'll have the initial data when the controller loads.
for showing and hiding a loader you can do a directive like
angular.module('app').directive('loaderBar', function ($rootScope) {
return {
restrict: 'EA',
replace: true,
template: '<div class="showloader"><img src="loader.gif" alt="loading..." /></div>',
link: function (scope, element) {
var namedListener = $rootScope.$$listeners['$stateChangeStart'];
if (namedListener === null || namedListener === undefined) {
$rootScope.$on('$stateChangeStart', function () {
element.addClass('animatedLoader');
});
$rootScope.$on('$stateChangeSuccess', function () {
element.removeClass('animatedLoader');
});
}
}
};
});
and don't forget to use ng-cloak class to hide the contents while loading at the top of each view, that will work like a charm!

injector error when trying to inject $scope into a service

I'm trying to learn angular by extending a sample app to use firebase. I want to modify the service object being used to call firebase instead of my webserver. I tried to inject $scope into my service object and am now getting an error. What am I doing wrong?
var app = angular.module("myApp", ['ngRoute', 'firebase']);
app.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl: "templates/home.html",
controller: "HomeController"
})
.when('/settings', {
templateUrl: 'templates/settings.html',
controller: 'SettingsController'
})
.otherwise({ redirectTo: '/' });
});
//services must return objects
app.service('mailService', ['$scope', '$http', '$firebase', function($scope, $http, $firebase) {
var mailRef = new Firebase("https://ng-book-email-client.firebaseio.com/mail");
//var getMail = function() {
// return $http({
// method: 'GET',
// url: '/api/mail'
// });
//};
$scope.email = $firebase(mailRef);
var sendEmail = function(mail) {
//var d = $q.defer();
//$http({
// method: 'POST',
// data: 'mail',
// url: '/api/send'
//}).success(function(data, status, headers) {
// d.resolve(data);
//}).error(function(data, status, headers) {
// d.reject(data);
//});
//return d.promise;
return $scope.email.$add(mail);
};
return {
//getMail: getMail,
sendEmail: sendEmail
};
}]);
app.controller('HomeController', function($scope) {
$scope.selectedMail;
$scope.setSelectedMail = function(mail) {
$scope.selectedMail = mail;
};
$scope.isSelected = function(mail) {
if($scope.selectedMail) {
return $scope.selectedMail === mail;
}
};
});
// directive that builds the email listing
app.directive('emailListing', function() {
var url = "http://www.gravatar.com/avatar/";
return {
restrict: 'EA', // E- element A- attribute C- class M- comment
replace: false, // whether angular should replace the element or append
scope: { // may be true/false or hash. if a hash we create an 'isolate' scope
email: '=', // accept an object as parameter
action: '&', // accept a function as a parameter
isSelected: '&',
shouldUseGravatar: '#', // accept a string as a parameter
gravatarSize: '#'
},
transclude: false,
templateUrl: '/templates/emailListing.html',
controller: ['$scope', '$element', '$attrs', '$transclude',
function($scope, $element, $attrs, $transclude) {
$scope.handleClick = function() {
$scope.action({selectedMail: $scope.email});
};
}
],
// if you had a compile section here, link: wont run
link: function(scope, iElement, iAttrs, controller) {
var size = iAttrs.gravatarSize || 80;
scope.$watch('gravatarImage', function() {
var hash = md5(scope.email.from[0]);
scope.gravatarImage = url + hash + '?s=' + size;
});
iElement.bind('click', function() {
iElement.parent().children().removeClass('selected');
iElement.addClass('selected');
});
}
};
});
app.controller('MailListingController', ['$scope', 'mailService', function($scope, mailService) {
$scope.email = [];
$scope.nYearsAgo = 10;
//mailService.getMail()
//.success(function(data, status, headers) {
// $scope.email = data.all;
//})
//.error(function(data, status, headers) {
//});
$scope.searchPastNYears = function(email) {
var emailSentAtDate = new Date(email.sent_at),
nYearsAgoDate = new Date();
nYearsAgoDate.setFullYear(nYearsAgoDate.getFullYear() - $scope.nYearsAgo);
return emailSentAtDate > nYearsAgoDate;
};
}]);
app.controller('ContentController', ['$scope', 'mailService', '$rootScope', function($scope, mailService, $rootScope) {
$scope.showingReply = false;
$scope.reply = {};
$scope.toggleReplyForm = function() {
$scope.reply = {}; //reset variable
$scope.showingReply = !$scope.showingReply;
console.log($scope.selectedMail.from);
$scope.reply.to = $scope.selectedMail.from.join(", ");
$scope.reply.body = "\n\n -----------------\n\n" + $scope.selectedMail.body;
};
$scope.sendReply = function() {
$scope.showingReply = false;
$rootScope.loading = true;
mailService.sendEmail($scope.reply)
.then(function(status) {
$rootScope.loading = false;
}, function(err) {
$rootScope.loading = false;
});
}
$scope.$watch('selectedMail', function(evt) {
$scope.showingReply = false;
$scope.reply = {};
});
}]);
app.controller('SettingsController', function($scope) {
$scope.settings = {
name: 'harry',
email: "me#me.com"
};
$scope.updateSettings = function() {
console.log("updateSettings clicked")
};
});
error
Error: [$injector:unpr] http://errors.angularjs.org/1.2.14/$injector/unpr?p0=<div ng-view="" class="ng-scope">copeProvider%20%3C-%20%24scope%20%3C-%20mailService
at Error (native)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:6:450
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:32:125
at Object.c [as get] (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:30:200)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:32:193
at c (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:30:200)
at d (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:30:417)
at Object.instantiate (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:31:80)
at Object.<anonymous> (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:31:343)
at Object.d [as invoke] (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js:30:452) angular.js:9509
The answer is that the $scope shouldn't be injected into services or factories. They're supposed to be reusable and sharable. The right way to achieve the effect is to create a $firebase object inside the service and return a function that can take any variable and scope and $bind them to the firebase object.
Inspiration comes from http://plnkr.co/edit/Uf2fB0?p=info
var app = angular.module("myApp", ['ngRoute', 'firebase']);
//services must return objects
app.service('mailService', ['$http', '$firebase', function($http, $firebase) {
var emailRef = new Firebase("https://ng-book-email-client.firebaseio.com/all");
var emails = $firebase(emailRef);
// uses firebase to bind $scope.email to the firebase mail object
var setEmails = function(scope, localScopeVarName) {
return emails.$bind(scope, localScopeVarName);
};
var sendEmail = function(mail) {
return $scope.email.$add(mail);
};
return {
setEmails: setEmails,
sendEmail: sendEmail
};
}]);
app.controller('MailListingController', ['$scope', 'mailService', function($scope, mailService) {
mailService.setEmails($scope, 'emails');
$scope.nYearsAgo = 10;
$scope.searchPastNYears = function(email) {
var emailSentAtDate = new Date(email.sent_at),
nYearsAgoDate = new Date();
nYearsAgoDate.setFullYear(nYearsAgoDate.getFullYear() - $scope.nYearsAgo);
return emailSentAtDate > nYearsAgoDate;
};
}]);
I think you should not inject $scope to a service, Service can be a common one for implementation /support for several controllers and service shouldn't know about a $scope (controller). But controllers users services to make their work done by passing parameters to a service method.
mailService.sendEmail($scope.reply)
Not sure why you need $scope inside a service here,
$scope.email = $firebase(mailRef);
are you expecting something like this,
this.email = $firebase(mailRef);

Changes event does not trigger in angular directive

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.

Resources