Angular string binding not working with ControllerAs vm - angularjs

I've been trying to do a two-way bind to a string variable on the Controller. When the controller changes the string, it isn't updated right away. I have already run the debugger on it and I know that the variable vm.overlay.file is changed. But it isn't updated on the View... it only updates the next time the user clicks the button that fires the selectOverlayFile() and then it presents the previous value of vm.overlay.file
Here goes the code:
(function () {
angular
.module("myapp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = [];
function SettingsController() {
var vm = this;
vm.overlay = {
file: undefined,
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.DATA_URL
}
};
vm.errorMessages = [];
vm.selectOverlayFile = selectOverlayFile;
vm.appMode = "photo";
vm.appModes = ["gif-HD", "gif-video", "photo"];
activate();
function activate() {
}
function selectOverlayFile() {
navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options);
}
function successOverlay(imageUrl) {
//If user has successfully selected a file
vm.overlay.file = "data:image/jpeg;base64," + imageUrl;
}
function errorOverlay(message) {
//If user couldn't select a file
vm.errorMessages.push(message);
}
}
})();
Thanks!

After a couple of hours searching for the issue and testing various solutions. I finally found it. The issue was that when the navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options) calls the callback function, it is out of AngularJS scope. So we need to notify Angular to update binding from within these callbacks using $scope.$apply():
(function () {
angular
.module("myapp.settings")
.controller("SettingsController", SettingsController);
SettingsController.$inject = ["$scope"];
function SettingsController($scope) {
var vm = this;
vm.overlay = {
file: undefined,
options: {
sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
destinationType: Camera.DestinationType.DATA_URL
}
};
vm.errorMessages = [];
vm.selectOverlayFile = selectOverlayFile;
vm.appMode = "photo";
vm.appModes = ["gif-HD", "gif-video", "photo"];
activate();
///////////////////
function activate() {
}
function selectOverlayFile() {
navigator.camera.getPicture(successOverlay, errorOverlay, vm.overlay.options);
}
function successOverlay(imageUrl) {
//If user has successfully selected a file
vm.overlay.file = "data:image/jpeg;base64," + imageUrl;
$scope.$apply();
}
function errorOverlay(message) {
//If user couldn't select a file
vm.errorMessages.push(message);
$scope.$apply();
}
}
})();

Related

angularjs unit testing unable to inject a service

I have a really simple service:
'use strict';
angular.module('sapphire.orders').service('deliveryDatesService', service);
function service() {
return {
clearAddressReason: clearAddressReason,
getMinDate: getMinDate
};
//////////////////////////////////////////////////
function clearAddressReason(model, dateReasons, reasons) {
if (model.manualAddress) {
model.overrideDates = true;
reasons.date = dateReasons.filter(function (item) {
return item.value === 'D4';
})[0];
} else {
reasons.address = null;
if (!reasons.date || reasons.date.value === 'D4') {
reasons.date = null;
model.overrideDates = false;
}
}
};
function getMinDate(model) {
var now = new Date();
// If we are not overriding dates, set to today
if (!model.overrideDates) return now;
// If dates are overriden, then the min date is today + daysToDispatch
return new Date(now.setDate(now.getDate() + model.daysToDispatch));
};
};
It has no dependencies, so I want to test the methods.
So I have tried to create a spec like this:
'use strict';
describe('Service: deliveryDatesService', function () {
beforeEach(module('sapphire.orders'));
var service,
reasons,
dateReasons;
beforeEach(inject(function (deliveryDatesService) {
console.log(deliveryDatesService);
service = deliveryDatesService;
reasons = {};
dateReasons = [{ value: 'D4' }];
}));
it('can create an instance of the service', function () {
expect(service).toBeDefined();
});
it('if manual delivery address is true, then override dates should be true', function () {
var model = { manualDeliveryDate: true };
service.clearAddressReason(model, dateReasons, reasons);
expect(model.overrideDates).toBe(true);
});
it('if manual delivery address is false, then override dates should be false', function () {
var model = { manualDeliveryDate: false };
service.clearAddressReason(model, dateReasons, reasons);
expect(model.overrideDates).toBe(false);
});
it('minimum date cannot be less than today', function () {
var model = { };
var minDate = service.getMinDate(model);
var now = new Date();
expect(minDate).toBeGreaterThan(now);
});
});
But my service is always undefined. Can someone tell me what I am doing wrong please?
Update
So, it turns out this is to do with one or more services interfering somehow.
In my karma.conf.js I had declared all my bower applications and then this:
'src/app/app.module.js',
'src/app/**/*module.js',
'src/app/**/*constants.js',
'src/app/**/*service.js',
'src/app/**/*routes.js',
'src/app/**/*.js',
'test/spec/**/*.js'
I created a test service in the root of my scripts directory and then created a spec file to see if it was created. It moaned at me about a reference error in a file that was not related at all. It moaned about this bit of code:
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
stating that Modernizr is not defined.
I changed the code to this:
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
and it started working. But my other test was not.
So I changed my references in karma.conf.js to this:
'src/app/app.module.js',
'src/app/orders/orders.module.js',
'src/app/orders/shared/*.js',
'test/spec/**/*.js'
and it started working.
That leads me to believe there is something wrong with my application somewhere. Maybe another reference like Modernizr. I still have an outstanding question though. How can services that are not dependant on another service interfere?
I think it's worth noting that each service, controller, directive is in it's own file and they all follow this structure:
(function () {
'use strict';
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
})();
I am wondering that because I wrap them in anonymous functions that execute themselves, is that what is causing this problem?
* Solution *
So in the end I found out exactly what was causing this issue. It was indeed to do with the file in karma.conf.js. I had told it to load all files and somewhere in there was something it didn't like.
After a bit of playing I finally found what it was and thought I would share it just in case someone else gets here.
The issue was routes. I am using ui.router and it appears that having them in your tests fail.
I changed my files section to this:
'src/app/app.module.js',
'src/app/**/*module.js',
'src/app/**/*constants.js',
'src/app/**/*service.js',
'src/app/**/*controller.js',
//'src/app/**/*routes.js',
'test/spec/**/*.js'
As you can see I have a routes file(s) commented out. If I bring them back in, everything fails.
I think your inner deliveryDatesService variable is hiding the external one.
To avoid that, you can try puting underscores around the inner service variable as per the spec on the website below:
https://docs.angularjs.org/api/ngMock/function/angular.mock.inject
Look for
'Resolving References (Underscore Wrapping)'
on that page.
Then your code would look like this:
beforeEach(inject(function (_deliveryDatesService_) {
console.log(_deliveryDatesService_);
service = _deliveryDatesService_;
reasons = {};
dateReasons = [{ value: 'D4' }];
}));
Also you need to make sure that all required files and directories are declared in karma.conf.js so that the test framework can use them.

Delay loading data in Angular JS

I have code like this
(function (app) {
app.controller('productListController', productListController)
productListController.$inject = ['$scope', 'apiService', 'notificationService', '$ngBootbox', '$filter'];
function productListController($scope, apiService, notificationService, $ngBootbox, $filter) {
$scope.products = [];
$scope.page = 0;
$scope.pagesCount = 0;
$scope.getProducts = getProducts;
$scope.keyword = '';
$scope.search = search;
$scope.deleteProduct = deleteProduct;
$scope.selectAll = selectAll;
$scope.deleteMultiple = deleteMultiple;
function deleteMultiple() {
var listId = [];
$.each($scope.selected, function (i, item) {
listId.push(item.ID);
});
var config = {
params: {
checkedProducts: JSON.stringify(listId)
}
}
apiService.del('/api/product/deletemulti', config, function (result) {
notificationService.displaySuccess('Deleted successfully ' + result.data + 'record(s).');
search();
}, function (error) {
notificationService.displayError('Can not delete product.');
});
}
$scope.isAll = false;
function selectAll() {
if ($scope.isAll === false) {
angular.forEach($scope.products, function (item) {
item.checked = true;
});
$scope.isAll = true;
} else {
angular.forEach($scope.products, function (item) {
item.checked = false;
});
$scope.isAll = false;
}
}
$scope.$watch("products", function (n, o) {
var checked = $filter("filter")(n, { checked: true });
if (checked.length) {
$scope.selected = checked;
$('#btnDelete').removeAttr('disabled');
} else {
$('#btnDelete').attr('disabled', 'disabled');
}
}, true);
function deleteProduct(id) {
$ngBootbox.confirm('Are you sure to detele?').then(function () {
var config = {
params: {
id: id
}
}
apiService.del('/api/product/delete', config, function () {
notificationService.displaySuccess('The product hase been deleted successfully!');
search();
}, function () {
notificationService.displayError('Can not delete product');
})
});
}
function search() {
getProducts();
}
function getProducts(page) {
page = page || 0;
var config = {
params: {
keyword: $scope.keyword,
page: page,
pageSize: 20
}
}
apiService.get('/api/product/getall', config, function (result) {
if (result.data.TotalCount == 0) {
notificationService.displayWarning('Can not find any record.');
}
$scope.products = result.data.Items;
$scope.page = result.data.Page;
$scope.pagesCount = result.data.TotalPages;
$scope.totalCount = result.data.TotalCount;
}, function () {
console.log('Load product failed.');
});
}
$scope.getProducts();
}
})(angular.module('THTCMS.products'));
So my problem is when i loading data the application take me some time to load data.
I need load data as soon as
Is the any solution for this?
Since you are loading data via api call, there will be a delay. To handle this delay, you should display a loading screen. Once the data is loaded, the loading screen gets hidden and your main screen is visible. You can achieve this using $http interceptors.
See : Showing Spinner GIF during $http request in angular
The api-call is almost certainly causing the delay. Data may be received slowly via the api-call so you could display any sort of loading text/image to notify the use that the data is being loaded.
If u want the data ready at the time when controller inits, u can add a resolve param and pass the api call as a $promise in the route configuration for this route.

AngularJS v1.3 breaks translations filter

In Angular v1.2 I was using the following code for serving up localised strings in the application:
var i18n = angular.module('i18n', []);
i18n.service('i18n', function ($http, $timeout) {
/**
A dictionary of translations keyed on culture
*/
this.translations = {},
/**
The current culture
*/
this.currentCulture = null,
/**
Sets the current culture, loading the associated translations file if not already loaded
*/
this.setCurrentCulture = function (culture) {
var self = this;
if (self.translations[culture]) {
$timeout(function () {
self.currentCulture = culture;
});
} else {
$http({ method: 'GET', url: 'i18n/' + culture + '/translations.json?' + Date.now() })
.success(function (data) {
// $timeout is used here to defer the $scope update to the next $digest cycle
$timeout(function () {
self.translations[culture] = data;
self.currentCulture = culture;
});
});
}
};
this.getTranslation = function (key) {
if (this.currentCulture) {
return this.translations[this.currentCulture][key] || key;
} else {
return key;
}
},
// Initialize the default culture
this.setCurrentCulture(config.defaultCulture);
});
i18n.filter('i18n', function (i18n) {
return function (key) {
return i18n.getTranslation(key);
};
});
In the template it is then used as follows:
<p>{{ 'HelloWorld' | i18n }}</p>
For some reason that I can't fathom, upgrading to v1.3 of AngularJS has broken this functionality. Either the $timeout isn't triggering a digest cycle, or the filter isn't updating. I can see that the $timeout code is running, but the filter code never gets hit.
Any ideas why this might be broken in v1.3?
Thanks!
In angular 1.3 the filtering was changed so that they are no longer "stateful". You can see more info in this question: What is stateful filtering in AngularJS?
The end result is that filter will no longer re-evaluate unless the input changes. To fix this you can add the line:
i18n.filter('i18n', function (i18n) {
var filter = function (key) {
return i18n.getTranslation(key);
};
filter.$stateful = true; ///add this line
return filter;
});
Or else implement your filter some other way.

Should I return collection from factory to controller via function?

I'm struggling with choosing the correct way of accessing collection located inside service from controller. I see two options, both have ups and downs:
Returning function from service that returns collection:
Service:
app.factory('healthService', function () {
var healths = [{},{},{},{}];
function updateHealths() {
healths = [...];
}
return {
getHealths : function() {
return healths;
},
update : function () {
updateHealths();
}};
});
Controller:
$scope.healths = healthService.getHealths;
$scope.update = healthService.update;
View:
ng-repeat = "health in healths()"
ng-click = "update()" '
I'm not sure about efficiency here- how often will healths() be evaluated?
Giving the possibility to access collection directly from controller:
Service:
app.factory('healthService', function () {
return {
healths : [{},{},{},{}],
update :function() {
this.healths = [...];
}
});
Controller:
$scope.healthService = healthService;
View:
ng-repeat = "health in healthService.healths" '
ng-click = "healthService.update()"
Which one is better, faster? Any other tips?
Why not try wrapping your collection in an object (which acts as a service state) to allow binding to occur on the object, rather than by exposing functions. For example:
app.factory('healthService', function() {
var state = {
healths: [...]
};
return {
getState: function() { return state; },
updateHealths: function() { state.healths = [...]; }
};
});
Then inside your controller:
$scope.healthState = healthService.getState();
Then to reference your healths from your html, use:
<div ng-repeat="health in healthState.healths"></div>

AngularJS local storage - initialize app retrieving local-stored data

I'm pretty new to angular and I'm trying to avoid losing items added on a simple cart application when the user refreshes the page.
I'm using angularLocalStorage (https://github.com/agrublev/angularLocalStorage) but don't know how to retrieve it back the content.
My lines:
var myApp = angular.module('ionicApp', ['ionic','angularLocalStorage']);
myApp.factory('prodottiData', function($http) {
return {
getFooOldSchool: function(callback) {
$http.get('http://192.168.1.128/hongkongapp/?json=get_recent_posts&post_type=product&custom_fields=all').success(callback);
}
}
});
myApp.factory('DataService', function() {
var myCart = new shoppingCart("AngularStore");
return {
cart : myCart
};
});
myApp.controller('MyController', function MyController ($scope, storage, $ionicSideMenuDelegate, prodottiData, DataService, $sce) {
$scope.toggleLeft = function() {
$ionicSideMenuDelegate.$getByHandle('mainMenu').toggleLeft();
};
$scope.toggleMySecondMenuLeft = function() {
$ionicSideMenuDelegate.$getByHandle('mySecondMenu').toggleLeft();
};
//adding menu data to the scope object
prodottiData.getFooOldSchool(function(data) {
$scope.menu = data;
});
//adding the cart to the scope object
$scope.cart = DataService.cart;
$scope.to_trusted = function(html_code) {
return $sce.trustAsHtml(html_code);
}
images = $scope.menu;
$scope.showloader = function(){
$scope.shownImage = this.post.thumbnail_images.full.url;
$scope.itemDesc = this.post.content;
$scope.itemPrice = this.post.custom_fields._price[0];
$scope.productName = this.post.title;
$scope.skuProdotto = this.post.id;
}
});
Now, if I check local storage on the console I can see something is really stored, but I miss the way to re-populate the cart at startup.
Any help would be great!
why not just using browser local storage ?
you can add it to your services.js as a new service and just used that.
var storeService = myAppServices.factory('storeService', function() {
var service =
{
setClientData:function(client_details)
{
window.localStorage.setItem( "client_data", JSON.stringify(client_details) );
client_data = client_details;
},
getClientData:function()
{
if (client_data == null)
{
client_data = JSON.parse(window.localStorage.getItem("client_data"));
}
return client_data;
}
}
var client_data = null;
return service;
});
From the documentation, to retrieve, it's storage.get('key')
So, to check after refresh:
if (storage.get('someKey')){
$scope.retrieved_value = storage.get('someKey');
}else{
// whatever
}
You can use localStorage instead windows.localStorage.
if(typeof(Storage)!=="undefined")
{
// Code for localStorage/sessionStorage.
var hello = "Hello World!!";
localStorage.setItem("hello",hello);
// get string
console.log(localStorage.getItem("hello")); // will return 'Hello World!!'
var me = {name:'abel',age:26,gender:'male'};
localStorage.setItem("user", JSON.stringify(me));
//fetch object
console.log(localStorage.getItem("user")); // will return {"name":"myname","age":99,"gender":"myGender"}
var objetos = JSON.parse(localStorage.getItem("user"));
console.log(objetos.name);
}
else
{
// Sorry! No Web Storage support..
}

Resources