I am currently facing a problem with my project's design.
I am using angularjs framework and my task is to provide a translations for a webpage, but the translations need to be provided form the xml file o the BE side.
So since I#ve found out that angulars i18n is configurable on the FE side i had to use another strategy.
I've decided to make a service which fetches the data during a resolve period before everything else is loaded:
app.factory('dictionaryService', ['$http', '$rootScope', function ($http, $rootScope) {
return {
getDictionary: function (defaultLanguage) {
var chosenLanguage = null;
if (angular.isUndefined($rootScope.defaultLanguage) || $rootScope.defaultLanguage == null) {
chosenLanguage = defaultLanguage;
$rootScope.defaultLanguage = chosenLanguage;
} else {
chosenLanguage = $rootScope.defaultLanguage;
}
var translation = new Array();
translation[chosenLanguage] = new Array();
return $http.get('Translation/GetCurrentDictionary/', {
params: {
language: chosenLanguage
}
});
},
GetLanguagesSetup: function () {
return $http.get('Translation/GetLanguagesSetup/');
}
}
}]);
and then resolve it as follows:
$routeProvider.when("/diagnose", {
controller: "diagnoseCtrl",
templateUrl: "/app/views/diagnose.html",
resolve: {
startupData: function (dictionaryService, $q) {
var def = $q.defer();
var translation = new Array();
var startupData = new Array();
var defaultLanguage = "EN";
var dict = dictionaryService.getDictionary(defaultLanguage).then(function (JSONData) {
var keys = Object.keys(JSONData.data.data);
var chosenLanguage = JSONData.data.lang;
translation[chosenLanguage] = {};
for (i = 0; i < keys.length; i++) {
translation[keys[i]] = JSONData.data.data[keys[i]];
}
startupData['translations'] = translation;
def.resolve(startupData);
}).catch(function (e) {
console.log("Translation fetching exception, " + e);
return $q.reject(e);
});
return def.promise;
}
}
});
So as you can see I am storing my fetched translations in a startupData. Then in a controller which is using it I am assigning this data to the $rootScope. It seems already here as a not the best solution, but I could not come up with a different one
Then I have created a translation service which gets the direct translation text:
app.factory('translationService', ['$rootScope', '$http', function ($rootScope, $http) {
var translations = null;
return {
getText: function (key) {
if ($rootScope.cachedTranslations == undefined) {
return key;
}
var result = $rootScope.cachedTranslations[key];
if (result == null) {
return key;
} else {
return result;
}
}
}
}]);
The biggest problem with this solution is, that I am not using promises, but I do not want to make an http query to BE for each translation.
The other problem is with the html template provided by the designers:
<body ng-controller="mainController">
<loading-screen ng-show="!isDataLoaded"></loading-screen>
<div id="header" class="headerView" ng-controller="headerController" ng-show="isDataLoaded">
some header stuff
...
<button ng-bind="option1" ng-click="redirectTo('#subpage1')"></button>
<button ng-bind="option2" ng-click="redirectTo('#subpage2')"></button>
<button ng-bind="option3" ng-click="redirectTo('#subpage3')"></button>
<button ng-bind="language" ng-if="availableLanguages.length > 1" ng-repeat="language in availableLanguages" ng-click="setLanguage(language)"></button>
</div>
</div>
<
<div id="content" ng-view ng-show="isDataLoaded">
</div>
<footer id="footer" class="footer" ng-show="isDataLoaded">
<status-bar></status-bar>
</footer>
Resolve applies only for ng-views's controller, but header stuff needs to be translated as well, so I need to make a headerCtrl somehow wait before it tries to apply translations.
So I have made another unpopular decision to inform all controllers about the finished startup via a broadcast message and to wait until it is all done while showing the loading screen.
It looks fine and is pretty responsive (1sec per startup is acceptable at this point).
The problem is, that I see many design mistakes with this attempt and I just can not come up with the better design.
So my main question is:
How can I make it better? 1st service returns a whole array which is used by the 2nd service so I do not know how to combine it with promises?
I am afraid that with the development of the application I will find myself in a global variables and global events hell
Thanks in advance for your help!
It seems like you are looking for the angular-translate-loader-static-files extension for angular-translate. See the documentation here.
This together with proper configuration of $translateProvider will allow you to fetch json files with translations from the backend or even swap translations on demand - for example user changes language setting, controller reconfigures $translateProvider. Your job is done - everything will be fetched and updated automatically without a page reload.
Related
I have a Chrome extension uses an ng-show expression that checks a variable of Chrome storage, and displays a big blue button if the value is at zero. When opening the extension, however, the button could show up on the first click, or you may have to close and reopen the extension several times before it shows up (simply showing a blank body). This has been incredibly frustrating and is obviously a UX problem that I would like to fix before I publish the extension to the public.
Here is the div code from within the main view of the extension:
<div id="no_vseeks" ng-show="vseeks.length === 0">
<div class="big-button-container"><h1>CREATE</h1></div>
</div>
The expression is referring to an array called 'vseeks' in the Chrome local storage.
And here is what the extension is supposed to output:
But this is what the extension will show (seemingly) at random.
Please inform me if I need to include more code or images.
EDIT: Here's the main controller where the vSeeks array is being retrieved from Chrome storage. The console logs show that after the chrome.storage.get function is called, the array is present, but yet I still get a blank view.
app.controller('mainController', function($scope, $window) {
$scope.toggleAcc = function($event) {
var currentAcc = $event.currentTarget;
currentAcc.classList.toggle("active");
var panel = currentAcc.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
}
$scope.sendID = function(id) {
currentVID = id;
$window.location.href = "#!delete";
}
var noVseeks;
var home_button;
var newV_button;
console.log('controller reached', $scope.vseeks);
chrome.storage.sync.get('userData', function(items){
$scope.vseeks = items.userData.vSeeks;
console.log('inside chrome storage get', $scope.vseeks);
home_button = document.getElementById('home_button');
newV_button = document.getElementById('newV_button');
console.log('variables: ', home_button, newV_button);
if ($scope.vseeks.length < 1) {
home_button.style.visibility = "hidden";
newV_button.style.visibility = "hidden";
}
else {
newV_button.style.visibility = "visible";
if (home_button.style.visibility == "visible") {
home_button.style.visibility = "hidden";
}
}
});
});
I am unfamiliar with Chrome.storage.sync but it must be returning a non-angularjs promise (i.e. not $q). In that scenario, the function you are running upon resolve is executing outside of the angular digest cycle -- angular doesn't know it should be doing anything. The way to force angular to run its cycle is to use $scope.$apply. This will synchronize the model to the view and vice versa.
I am using ForerunnerDB - a NoSQL JSON Document DB for creating a data store on the client (web). This particular library offers a persistence storage option. However, there seems to be something wrong with it. I have included my code below:
App:
(function(){
angular.module("TestApp", ['ui.router', 'LocalStorageModule', 'forerunnerdb']);
})();
Controller:
(function(){
angular.module("TestApp")
.controller("FetchGroupsController", function(clientStore, $scope, $http){
clientStore.getTable("groups").load(function (err, tableStats, metaStats) {
if (err) {
// Load operation was unsuccessful
console.error("error in pulling collection " + name);
console.error(err)
}
else{
//there is persisted data corresponding to the requested table
if(tableStats.foundData && tableStats.rowCount > 0){
controller.groups = clientStore.fetchRecords("groups"); //returns an array of objects
console.log("User has " + tableStats.rowCount + " groups");
console.log(controller.groups);
}
else{//no table for group data persisted */
$http({ method: "POST", url: "api/groups/index"})
.success(function(data, status){
//TODO: handle the activation code
if(status == 200){
delete data["debug"]; //deleting an unnecessary object from response data
clientStore.addList("groups", data);
controller.groups = clientStore.fetchRecords("groups");
console.log(controller.groups);
clientStore.getTable("groups").save();
}
else if(status == 403){ //forbidden
controller.messages = data;
}
else if(status == 401){ //error
controller.errors = data;
}
})
;
}
}
});
});
})();
Service:
angular.module("TestApp")
.factory('clientStore', function($fdb, $http, localStorageService){
var clientDB = null;
var factory = {};
clientDB = $fdb.db("testDB");
factory.getDB = function(){
return clientDB;
};
factory.createTable = function(name, pk){
if(pk != false){
return clientDB.collection(name, {primaryKey : pk});
}
return clientDB.collection(name);
};
factory.resetTable = function(name){
clientDB.collection(name)._data = [];
clientDB.collection(name).save();
};
factory.getTable = function(name){
return clientDB.collection(name);
};
factory.add = function(name, record){ //insert a single object
clientDB.collection(name).insert(record/*, function(result){
console.warn(result);
}*/);
};
factory.addList = function(name, records){ //insert a list of objects
for (record in records){
clientDB.collection(name).insert(records[record]);
}
};
factory.fetchRecords = function(name){
return clientDB.collection(name)._data;
};
return factory;
});
View:
<div ng-repeat="group in fetchGrpsCtrl.groups" class="row">
<div class="col-md-12">
<div class="groups" id="grp-#{{group.id}}" ng-click="fetchGrpsCtrl.setGroup(group)">
<div class="details">
<h5> #{{group.title}}</h5>
</div>
</div>
</div>
</div>
The problem is that the variable controller.group is null in the view, but in the controller, I am getting the expected value when I log it (line 15) out to the console. I thought maybe it was a problem of scope and that the changes made to the variable inside the callback were not being picked up in the view. But that is the not the case because I commented out the line
clientStore.getTable("groups").save();
and ran it again and the view was updated perfectly as expected. I cannot figure out why the save() function is erasing the controller.groups variable in the view but is logged out perfectly in the controller.
You are using ForerunnerDB incorrectly. It includes an AngularJS plugin that makes using it with AngularJS very easy, but instead you've written a wrapper of your own that hooks onto the _data property of a collection (something that you shouldn't do because that property is managed internally by ForerunnerDB and can be destroyed and re-created meaning you will have an incorrect reference to an object that might no longer be used by a collection).
Instead you should use ForerunnerDB in AngularJS as described in the documentation: https://github.com/Irrelon/ForerunnerDB#angularjs-and-ionic-support
ForerunnerDB includes a helper method called "ng()" that allows you to correctly link data from a collection to your view's scope.
Also, when using third party javascript libraries, properties with an underscore are generally considered "private" and should not be accessed directly. You should use an accessor method (such as .find() in ForerunnerDB's case) to access data instead. Usually a property that starts with an underscore is a good indication that it is private (not all libraries and programmers follow this convention but many do).
I use angular-translate with angular-datatables and implemented a language switch between german and english (Explained here Switching between languages. Switching language works well but not with angular-datatables. When i switch angular-datatables keeps the old translations for the table header.
angular-datatables Controller:
It's loading the datatables data via json with a promise and then draws the table. It also refreshes the table every 5 minutes. I implemented a public function "rerenderTable" which i call when switching the app language.
.controller('DashboardCtrl', ['$scope', '$rootScope', 'DTOptionsBuilder', 'DTColumnBuilder', 'DTInstances', '$resource',
'$interval', '$translate',
function($scope, $rootScope, DTOptionsBuilder, DTColumnBuilder, DTInstances, $resource, $interval, $translate)
{
$scope.initTargetPackaging = function initTargetPackaging()
{
$scope.sourceUrl = 'json/v1.0.0/dashboard/datatables/myjson1.json';
};
$scope.initTargetConversion = function initTargetConversion()
{
$scope.sourceUrl = 'json/v1.0.0/dashboard/datatables/myjson2.json';
};
// Get the TargetPackaging JSON Data with promise AJAX call
var vm = this;
vm.dtOptions = DTOptionsBuilder.fromFnPromise( function()
{
return $resource($scope.sourceUrl).query().$promise;
})
.withOption('bInfo', false)
.withOption('paging', false)
.withOption('filter', false)
.withOption('rowCallback', rowCallback);
// Create the table columns
vm.dtColumns = [
DTColumnBuilder.newColumn('customer')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_CUSTOMER')),
DTColumnBuilder.newColumn('today')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_TODAY')),
DTColumnBuilder.newColumn('week')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_7DAYS')),
DTColumnBuilder.newColumn('month')
.withTitle($translate('DIRECTIVES.DASHBOARD.DATATALBE_TARGET_PACKAGING_COLUMN_30DAYS'))
];
vm.newPromise = newPromise;
vm.reloadData = reloadData;
vm.dtInstance = {};
function newPromise()
{
return $resource($scope.sourceUrl).query().$promise;
}
/**
* Reload the data
*/
function reloadData()
{
var resetPaging = false;
vm.dtInstance.reloadData(resetPaging);
}
// Trigger reloading - 5 mins
$interval(reloadData, 300000);
function rowCallback(nRow, aData)
{
// Add status CSS class if state is true
if (aData['state'] != undefined
&& aData['state'] === true)
{
$(nRow).addClass('ad-status-inactive');
}
}
$rootScope.rerenderTable = function()
{
vm.dtInstance.rerender();
};
}]);
Function to switch language:
$scope.changeLang = function(key)
{
$translate.use(key).then( function(key)
{
console.log("Sprache zu " + key + " gewechselt.");
$rootScope.rerenderTable();
},
function(key)
{
// Trigger log error message (failure of switching language)
});
};
Triggered here in html:
<div id="language-switch" class="col-xs-2" ng-controller="LanguageCtrl">
<div class="inner">
{{ 'MAIN_NAVIGATION.CHOOSE_LANGUAGE' | translate }}
<span ng-click="changeLang('en')" class="lang-sm" lang="en"></span>
<span ng-click="changeLang('de')" class="lang-sm" lang="de"></span>
</div>
</div>
Summary: Switching languages works well. But not in the case of angular-datatables. It does not switch the language. But translating the strings is fine.
How do i get angular-datatables to rerender the table by using the currently chosen language?
1- Listen to the language change to render the table afterwards.
$rootScope.$on('$translateChangeEnd', function (event, lang) {
$scope.dtInstance.rerender();
});
2-Inside constructor function of your table
var headerCallback = function( thead, data, start, end, display ) {
$compile(angular.element(thead).contents())($scope);
}
3-
$scope.dtOptions(your name) = DTOptionsBuilder
.newOptions()
.withOption('headerCallback', headerCallback)
..........your code
$scope.dtColumns = [
DTColumnBuilder.newColumn('code').withTitle(`${'<span translate>'}${'TAG'}${'</span>'}`).renderWith(your_code).withClass('center-text'),
.........
Works for me ;)
A little bit late but here is an answer, which is not the best imho:
$rootScope.$on('$translateChangeSuccess', function (event, lang) {
vm.dtOptions.withLanguageSource('http://cdn.datatables.net/plug-ins/1.10.11/i18n/'+(lang.language == 'de' ? 'German' : 'English')+'.json');
$rootScope.rerenderTable();
});
It's a shame, they doesn't provide language files named like the ISO-codes. So you have to convert them into the english "long" language names.
I would like to pass some static configuration data from my server (.NET, PHP) to my Angular application. I don't want to call a web service for this.
In this question a javascript block is generated, but I don't want this.
How do I pass a value from main html to partials?
What's the best way to do this?
I was thinking of using the ng-init directive for this:
<div data-ng-app="MyApp" data-ng-init="foo='bar'">
Or is there a better way?
What I would recommend doing is using constants in angular. This is an example of one that I have used in a project (the #Url.Content part is c#, but I think you will get the idea).
<script>
angular.module('App.ConfigurationSettings', [])
.constant('ApiBaseUrl','#Url.Content("~/api/")')
.constant('WebBaseUrl','#Url.Content("~")');
</script>
and then when we actually use those constants in our service it looks like this:
var app = angular.module('App.BrandService', ['App.ConfigurationSettings']);
app.factory("BrandService", function ($http, $q, ApiBaseUrl) {
return {
getBrand: function (brandId) {
var deferred = $q.defer();
var url = ApiBaseUrl + 'brands/' + brandId + '.json';
return HttpGet($http, url, deferred);
},
getBrands: function () {
var deferred = $q.defer();
var url = ApiBaseUrl + 'brands.json';
return HttpGet($http, url, deferred);
}
};
});
Usually static information is configurable and related to some service. If so, I would create a provider (which is just a specialized service) and configure it in your config function.
Here is the Angular documentation on Providers
Create a Provider
myApp.provider('tooltip', function () {
var version;
var author;
this.setVersion= function(value) {
version = value;
};
this.setAuthor = function(value) {
author = value;
};
this.$get = function (/* injectibles go here */) {
return {
version: version,
author: author,
etc...
};
};
});
Configure the Provider
When you inject your provider in your config function, make sure that you append 'Provider':
myApp.config(function(tooltipProvider) {
tooltipProvider.setVersion('1.0');
tooltipProvider.setAuthor('John Smith');
// more configuration of static data
});
Using the Provider
Inject the provider where you need it, and make use of your static data. Here, we are binding the static data to scope so that we can display it in the view.
// JS
app.controller('ctrl', function($scope, tooltip) {
$scope.version = tooltip.version;
$scope.author = tooltip.author;
});
// HTML
<div ng-controller='ctrl'>
Tooltip Plugin: <br />
Version: {{ version }} <br />
Author: {{ author }} <br />
</div>
App design question. I have a project which has a very large number of highly customized inputs. Each input is implemented as a directive (and Angular has made this an absolute joy to develop).
The inputs save their data upon blur, so there's no form to submit. That's been working great.
Each input has an attribute called "saveable" which drives another directive which is shared by all these input types. the Saveable directive uses a $resource to post data back to the API.
My question is, should this logic be in a directive at all? I initially put it there because I thought I would need the saving logic in multiple controllers, but it turns out they're really happening in the same one. Also, I read somewhere (lost the reference) that the directive is a bad place to put API logic.
Additionally, I need to introduce unit testing for this saving logic soon, and testing controllers seems much more straightforward than testing directives.
Thanks in advance; Angular's documentation may be… iffy… but the folks in the community are mega-rad.
[edit] a non-functional, simplified look at what I'm doing:
<input ng-model="question.value" some-input-type-directive saveable ng-blur="saveModel(question)">
.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{id: question.id, answer: question.value},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
}
}])
No, I don't think the directive should be calling $http. I would create a service (using the factory in Angular) OR (preferably) a model. When it is in a model, I prefer to use the $resource service to define my model "classes". Then, I abstract the $http/REST code into a nice, active model.
The typical answer for this is that you should use a service for this purpose. Here's some general information about this: http://docs.angularjs.org/guide/dev_guide.services.understanding_services
Here is a plunk with code modeled after your own starting example:
Example code:
var app = angular.module('savingServiceDemo', []);
app.service('savingService', function() {
return {
somethingOrOther: {
save: function(obj, callback) {
console.log('Saved:');
console.dir(obj);
callback(obj, {});
}
}
};
});
app.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{
id: question.id,
answer: question.value
},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
};
}]);
app.controller('questionController', ['$scope', function($scope) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
}]);
The relevant HTML markup:
<body ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="saveModel(question)" />
</body>
An alternative using only factory and the existing ngResource service:
However, you could also utilize factory and ngResource in a way that would let you reuse some of the common "saving logic", while still giving you the ability to provide variation for distinct types of objects / data that you wish to save or query. And, this way still results in just a single instantiation of the saver for your specific object type.
Example using MongoLab collections
I've done something like this to make it easier to use MongoLab collections.
Here's a plunk.
The gist of the idea is this snippet:
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
Notes:
dbUrl and apiKey would be, of course, specific to your own MongoLab info
The array in this case is a group of distinct collections that you want individual ngResource-derived instances of
There is a createResource function defined (which you can see in the plunk and in the code below) that actually handles creating a constructor with an ngResource prototype.
If you wanted, you could modify the svc instance to vary its behavior by collection type
When you blur the input field, this will invoke the dummy consoleLog function and just write some debug info to the console for illustration purposes.
This also prints the number of times the createResource function itself was called, as a way to demonstrate that, even though there are actually two controllers, questionController and questionController2 asking for the same injections, the factories get called only 3 times in total.
Note: updateSafe is a function I like to use with MongoLab that allows you to apply a partial update, basically a PATCH. Otherwise, if you only send a few properties, the entire document will get overwritten with ONLY those properties! No good!
Full code:
HTML:
<body>
<div ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
<div ng-controller="questionController2">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
</body>
JavaScript:
(function() {
var app = angular.module('savingServiceDemo', ['ngResource']);
var numberOfTimesCreateResourceGetsInvokedShouldStopAt3 = 0;
function createResource(resourceService, resourcePath, resourceName, apiKey) {
numberOfTimesCreateResourceGetsInvokedShouldStopAt3++;
var resource = resourceService(resourcePath + '/' + resourceName + '/:id',
{
apiKey: apiKey
},
{
update:
{
method: 'PUT'
}
}
);
resource.prototype.consoleLog = function (val, cb) {
console.log("The numberOfTimesCreateResourceGetsInvokedShouldStopAt3 counter is at: " + numberOfTimesCreateResourceGetsInvokedShouldStopAt3);
console.log('Logging:');
console.log(val);
console.log('this =');
console.log(this);
if (cb) {
cb();
}
};
resource.prototype.update = function (cb) {
return resource.update({
id: this._id.$oid
},
angular.extend({}, this, {
_id: undefined
}), cb);
};
resource.prototype.updateSafe = function (patch, cb) {
resource.get({id:this._id.$oid}, function(obj) {
for(var prop in patch) {
obj[prop] = patch[prop];
}
obj.update(cb);
});
};
resource.prototype.destroy = function (cb) {
return resource.remove({
id: this._id.$oid
}, cb);
};
return resource;
}
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
app.controller('questionController', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('And, I got called back');
});
};
}]);
app.controller('questionController2', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What is the coolest JS framework of them all?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('You better have said AngularJS');
});
};
}]);
})();
In general, things related to the UI belong in a directive, things related to the binding of input and output (either from the user or from the server) belong in a controller, and things related to the business/application logic belong in a service (of some variety). I've found this separation leads to very clean code for my part.