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.
Related
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();
}
}
})();
I have run into this issue in my app several times. I have a fun ngResource resources that I attach to $scope as $scope.jogasok = Jogas.query();, and I create a new resource in the same scope by calling something like
$scope.addJogas = function (jogas) {
$scope.isDisabled = true;
var j = new Jogas(jogas);
j.$save(function (value) {
$scope.isDisabled = false;
}, httpErrorHandler);
};
where Jogas is an ngResource
.factory('Jogas', function ($resource) {
var Jogas = $resource('/jogasok/:id/:action', {
'id': '#_id',
'action': '#action'
}, {
'ujBerlet': {'method': 'POST', 'params': {'action': 'ujberlet'}}
});
return Jogas;
})
The POST call is successful, thus reloading the page shows the new item in $scope.jogasok, but the new item is not shown if I don't reload.
Have can I get $scope.jogasok reloaded/extended automatically when $save has finished?
I've figured out my own dumbness concerning this question.
$scope.jogasok is an Array, thus its value will not change without changing it.
The most basic solution is to reload $scope.jogasok in the success handler:
$scope.addJogas = function (jogas) {
$scope.isDisabled = true;
var j = new Jogas(jogas);
j.$save(function (value) {
$scope.jogasok = Jogas.query();
$scope.isDisabled = false;
}, httpErrorHandler);
};
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>
I'm trying to create a directive that will allow me to bind to hash of other scope properties.
HTML
<div lookup lookup-model="data.countryId"></div>
<div lookup lookup-model="data.stateId" lookup-params="{countryId: data.countryId}"></div>
What I would like to be able to do is every time a value in lookup-params is updated to refresh the lookup with the model of data.stateId. I'm trying to keep this generic since there is likely a variety of different lookup-params I'll want to have.
Is there a way to do this in Angular?
Update
I certainly didn't provide enough detail. Clicked submit too soon. Here is my solution based off feedback from #Olivvv. The suggestion led me to the scope.$eval function.
The goal here was to create a directive that would allow us to use a select with a $http get to seed the options within the select. Some of the $http requests will need a parameter since their is a dependency on another value. For example, a set of states can only be provided when a country value is provided.
The following is the code I pulled together. I'm sure it can be improved, but it is doing the trick at the moment. Please note, I'm using Lodash for some utility functions. You'll also see a scope object "lookupModelObject". This was purely to meet a design need for styling selects. It probably can be ignored if you are only interested in the lookupParams.
HTML Snippet
<div select-lookup lookup-value="block.data.countryId" lookup-type="countries" lookup-placeholder="Select a Country"></div>
<div select-lookup lookup-value="block.data.stateId" lookup-type="states" lookup-params="{countryId: block.data.countryId}" lookup-placeholder="Select a State"></div>
Directive
The important part to point out is how I'm evaluating the attrs.lookupParams. If the attribute exists I use scope.$eval to evaluate the attribute. You'll later see how I added each parameter to scope and added a watcher in case one of the params changed. This would allow me to reset the "state" to null if a different country was selected.
angular.module("foo").directive('selectLookup', ['$http', '$q', function($http, $q) {
return {
restrict: 'A',
replace: true,
templateUrl: 'common/lookups/partials/selectLookupPartial.html',
scope: {
id: "#",
lookupValue: "=",
lookupParams: "="
},
link: function(scope, element, attrs) {
// Initialize the options.
scope.options = [];
var optionsLoaded = false;
var resetLookupModel = function() {
scope.lookupModelObject = {
id: null,
text: attrs.lookupPlaceholder
};
};
// Evaluate and obtain the lookup parameters for this lookup.
var lookupParams = {};
if (attrs.lookupParams) {
lookupParams = scope.$eval(attrs.lookupParams);
}
var updateLookupModelObject = function(value) {
// This function is only relevant if the options have been loaded.
if (optionsLoaded) {
if (value === undefined || value === null) {
resetLookupModel();
}
else {
var item = _.findWhere(scope.options, {id: value});
if (item) {
scope.lookupModelObject = item;
}
else {
resetLookupModel();
}
}
}
};
var fetchValues = _.throttle(function() {
var deferred = $q.defer(),
fetchUrl = "/api/lookup/" + attrs.lookupType,
keys = _.keys(lookupParams);
_.each(keys, function(key, index) {
if (index === 0) {
fetchUrl += "?";
}
else {
fetchUrl += "&";
}
fetchUrl += key + "=" + lookupParams[key];
});
// Empty the options.
scope.options.splice(0, scope.options.length);
$http.get(fetchUrl).then(function(response) {
scope.options = response.data;
optionsLoaded = true;
updateLookupModelObject(scope.lookupValue);
deferred.resolve(scope.options);
});
return deferred.promise;
}, 150);
// Setup the watchers
// If there are lookup params add them to scope so we can watch them.
var keys = _.keys(lookupParams);
_.each(keys, function(key) {
scope[key] = lookupParams[key];
// Setup watchers for each param.
scope.$watch(key, function() {
fetchValues();
});
});
scope.$watch('lookupParams', function(newValue, oldValue) {
if (!_.isEqual(newValue, oldValue)) {
lookupParams = newValue;
fetchValues().then(resetLookupModel);
}
});
scope.$watch('lookupValue', updateLookupModelObject);
scope.$watch('lookupModelObject', function(newValue, oldValue) {
if (!_.isEqual(newValue, oldValue)) {
scope.lookupValue = newValue.id;
}
});
fetchValues();
}
};
}]);
Template
We had a design constraint that forced us to introduce a "select-placeholder". Outside of that the select is the "typical" way to setup a select in Angular.
<div class="container select-container">
<div class="select-placeholder">
<span class="select-placeholder-text">{{ lookupModelObject.text }}</span>
<select class="select-input" data-ng-model="lookupModelObject" id="{{ id }}" data-ng-options="option as option.text for option in options"></select>
</div>
</div>
I solved it with the following function in a "utils" service:
function utils ($parse) {
this.$params = function(attrs, attrName, allowedParams){
var out = {},
parsed = $parse(attrs[attrName])();
if (typeof parsed === 'object'){
angular.forEach(parsed, function(val, key){
if (allowedParams.indexOf(key) !== -1){
this[key] = val;
} else {
//do some logging. i.e ('parameter not recognized :', key, ' list of the allowed params:', allowedParams)
}
}, out);
}else{
out[allowedParams[0]] = attrs[attrName];
}
return out;
};
}
use it that way in your template:
lookup-params="{countryId: '{{data.countryId}}'}"
and in your directive :
var lookupParams = utils.$params(attrs, 'lookup-params', ['countryId','anotherParams', 'etcetera']);
The first of the allowed params can be passed directly as string instead of an object:
lookup-params="{'{{data.countryId}}'}"
will work
I am using ng-include in order to include a persistent menu, that exists in all of the views of my SPA.
The problem is that I want to display different options and content in this menu per each user type(admin, guest, user etc.), and this requires the service function authService.loadCurrentUser to be resolved first.
For the purpose of managing this content easily and comfortably, I have created a simple directive, that takes an attribute with the required access level, and at the compile phase
of the element, if the permissions of the given user are not sufficient, removes the element and it's children.
So after failing miserably at trying to make the ng-include go through the routeProvider function, I've tried to use ng-init, but nothing seems to work, the user role remain undefined at the time that I am logging it out.
I am thinking about trying a new approach, and making the entire menu a directive that includes the template that is suitable for each user type, but first I would like to try and solve this matter.
Directive:
'use strict';
/* Directives */
angular.module('myApp.directives', []).
directive('restrict', function(authService){
return{
restrict: 'A',
prioriry: 100000,
scope: {
// : '#'
},
link: function(){
// alert('ergo sum!');
},
compile: function(element, attr, linker){
var user = authService.getUser();
if(user.role != attr.access){
console.log(attr.access);
console.log(user.role);//Always returns undefined!
element.children().remove();
element.remove();
}
}
}
});
Service:
'use strict';
/* Services */
angular.module('myApp.services', []).
factory('authService', function ($http, $q) {
var authServ = {};
var that = this;
that.currentUser = {};
authServ.authUser = function () {
return $http.head('/users/me', {
withCredentials: true
});
},
authServ.getUser = function () {
return that.currentUser;
},
authServ.setCompany = function (companyId) {
that.currentUser.company = companyId;
},
authServ.loadCurrentUser = function () {
var defer = $q.defer();
$http.get('/users/me', {
withCredentials: true
}).
success(function (data, status, headers, config) {
console.log(data);
that.currentUser.company = {};
that.currentUser.company.id = that.currentUser.company.id ? that.currentUser.company.id : data.main_company;
that.currentUser.companies = [];
for (var i in data.roles) {
that.currentUser.companies[data.roles[i]['company']] = data.roles[i]['company_name'];
if (data.roles[i]['company'] == that.currentUser.company.id){
that.currentUser.role = data.roles[i]['role_type'];
that.currentUser.company.name = data.roles[i]['company_name'];
// console.log(that.currentUser.role);
}
}
// defer.resolve(data);
defer.resolve();
}).
error(function (data, status, headers, config) {
that.currentUser.role = 'guest';
that.currentUser.company = 1;
defer.reject("reject");
});
return defer.promise;
}
return authServ;
});
Menu controller:
angular.module('myApp.controllers', []).
controller('menuCtrl', function($scope, $route, $location, authService){
//TODO: Check if this assignment should be local to each $scope func in order to be compliant with 2-way data binding
$scope.user = authService.getUser();
console.log($scope.user);
// $scope.companies = $scope.user.companies;
$scope.companyOpts = function(){
// var user = authService.getUser();
if(typeof $scope.user.company == 'undefined')
return;
var companies = [];
companies[$scope.user.company.id] = $scope.user.company.name;
for(var i in $scope.user.companies){
if(i != $scope.user.company.id){
companies[i] = $scope.user.companies[i];
}
}
// console.log(companies);
// if(nonCurrentComapnies.length > 0){
console.log(companies);
return companies;
// }
}
$scope.$watch('user.company.name', function(company){
for(var i in $scope.user.companies)
if(company == $scope.user.companies[i].id)
authService.setCompany(i);
});
$scope.$watch(function(){return authService.getUser().company; }, function(company){
//Refresh the page on company change here, first time, and each time the user changes the select
// $scope.companyOpts();
// $scope.currentComapany = company;
})
;})
Main SPA HTML page:
<div ng-init="authservice.loadCurrentUser" ng-include src="'partials/menu.html'"></div>
menu element that should be visible only to the admin:
<ul class="left" restrict access="admin">
<li>You are the admin!</li>
</ul>
Thanks in advance for any assistance!
I personally would do the "reverse" way. Which mean: I will add the menu in when the user role is "admin", or "user", etc...
This way, you can do something like this in the "restrict" directive:
...
var roleListener = $scope.$watch('user.role', function (newVal, oldVal) {
if (newVal == $scope.access) {
// add the menu items
// supposed that loadcurrentuser be called only once
// we should clear the watch
roleListener();
} else {
// personally, I would remove the item here too
// so the menu would be added or removed when user.role update
}
});
...
One more thing, for just display menu base on the user role, you can use ngSwitch, something like this:
<ul class="left" ng-switch="user.role">
<li ng-switch-when="admin">You are the admin!</li>
<li ng-switch-when="user">You are the user!</li>
<li ng-switch-default><img src="some-thing-running.gif"/>Your menu is loading, please wait...</li>
</ul>
And let the magical AngularJS binding render up the menus for you!
The call to authServ.getUser should also return a promise by calling internally
authServ.loadCurrentUser
which should be modified a bit to check if the user context exists to avoid making another API call and always returning resolve with the user context:
defer.resolve(that.currentUser);
Loading the user context should also be done early on as this enables the authorization of the app. The app.run function can be used for this purpose.
hope it helps others.