I want to develop a generic translator component with configurable url and paramsFn. Here paramsFn can either be a plain function or a function with service dependencies. paramsFn is expected to return a promise.
(function () {
"use strict";
angular.module("translator-app", [])
.provider(translatorProvider);
function translatorProvider() {
var
url,
paramsFn;
//Provider Config Functions
function setUrl (pUrl) {
url = pUrl
};
function setParamsFn (pParamsFn) {
paramsFn = pParamsFn;
};
function factory ($http, $q) {
//Service Function Pseudo
function translate(key) {
if (translateions are cached) {
//return promis of cached translations
} else {
/*
make http call with configured url and
paramsFnto fetch translations.
Cache translations.
Return promise with translations.
*/
}
} //translate
//Service Object
return {
translate: translate
};
} // factory
factory .$inject = [
"$http"
"$q"
];
//Exposed functionality
this.setUrl = setUrl;
this.setParamsFn = setParamsFn;
this.$get = factory;
}
}();
An application can use translator after configuring it. User app provide will be able to provide paramFn with service dependencies. paramFn will be invoked later when translator.translate(...) method is called.
(function () {
"use strict";
angular.module('the-app', ["translator-app"])
.config(translatorConfigurator)
.controller(AppController)
function translatorConfigurator (translatorProvider) {
function theParamsFn (someService) {
//use someService to generate and return params object
}
theParamsFn.$inject = [
"someService"
];
translatorProvider.setUrl("/url/to/translator");
translatorProvider.setParamsFn(theParamsFn);
}
function AppController (translator) {
translator.translate("the-key").then(function (translated) {
//do somethid with 'translated'.
});
}
translatorConfigurator.$injec = [
"translatorProvider"
];
AppController.$inject = [
"translator"
];
}());
How can I achieve this?
Short Story:
According to Angular $injector documentation
// inferred (only works if code not minified/obfuscated)
$injector.invoke(function(serviceA){});
// annotated
function explicit(serviceA) {};
explicit.$inject = ['serviceA'];
$injector.invoke(explicit);
// inline
$injector.invoke(['serviceA', function(serviceA){}]);
Novel
Once upon a time there was a poor translatorProvider. Angular, a great super hero, helped translatorProvider to be feature rich by its $injector weapon. translatorProvider built its getParameters function inside factory function and used it in translate.
(function () {
"use strict";
angular.module("translator-app", [])
.provider(translatorProvider);
function translatorProvider() {
var
url,
paramsFn;
//Provider Config Functions
function setUrl (pUrl) {
url = pUrl
};
function setParamsFn (pParamsFn) {
paramsFn = pParamsFn;
};
function factory ($injector, $http, $q) {
function getParameters() {
var
promise,
fn;
if (paramsFn) {
fn = $injector.invoke(paramsFn);
promise = $q.resolve(fn());
} else {
promise = $q.resolve()
}
return promise;
}
//Service Function Pseudo
function translate(key) {
if (translateions are cached) {
//return promis of cached translations
} else {
getParameters()
.then(function (params) {
return $http({
url: url,
params: params
});
})
.then(function (response) {
var extracted = ...; //extract field from response.data
//put extracted into cache
return $q.resolve(extractedField)
});
}
} //translate
//Service Object
return {
translate: translate
};
} // factory
factory .$inject = [
"$injector",
"$http"
"$q"
];
//Exposed functionality
this.setUrl = setUrl;
this.setParamsFn = setParamsFn;
this.$get = factory;
}
}();
Now translator can be configured as below.
(function () {
"use strict";
angular.module('the-app', ["translator-app"])
.config(translatorConfigurator)
.controller(AppController)
function translatorConfigurator (translatorProvider) {
function theParamsFn (someService) {
return function () {
//returns some parameters object
}
}
theParamsFn.$inject = [
"someService"
];
translatorProvider.setUrl("/url/to/translator");
translatorProvider.setParamsFn(theParamsFn);
}
function AppController (translator) {
translator.translate("the-key").then(function (translated) {
//do somethid with 'translated'.
});
}
translatorConfigurator.$inject = [
"translatorProvider"
];
AppController.$inject = [
"translator"
];
}());
After these changes translatorprovider becomes more powerful and help many other modules and they lived happily ever after.
Related
Is there any way to spy on isMyResponse
(function() {
'use strict';
angular
.module('mymodule')
.factory('MyInterceptor', MyInterceptor);
MyInterceptor.$inject = [...];
function MyInterceptor(...) {
var self = this;
self.isMyResponse = isMyResponse;
return {
request: {},
response: function (response) {
if(self.isMyResponse(response)){
}
}
};
function isMyResponse(response){
...
}
}
})();
Your code is not structured particularly well to enable testing. You would be much better off to add isMyResponse to the MyInterceptor prototype. This way, you could spy on the the prototype in your tests:
function MyInterceptor(...) { ... }
MyInterceptor.prototype.isMyResponse = function (...) {
// the original isMyResponse method goes here
};
Then in the tests, you should be able to do something like this:
beforeEach(inject(MyInterceptor => {
spyOn(MyInterceptor, 'isMyResponse');
});
I have a controller and factory like below and can easily handle success..But how can I handle errors?
Controller
app.controller("draftsCtrl", ["$scope", "DashboardFactory", function ($scope, DashboardFactory) {
DashboardFactory.drafts(function (successCallback) {
$scope.rooms listings= successCallback;
});
}]);
Factory
app.factory('DashboardFactory', function ($http) {
var DashboardFactory = {};
DashboardFactory.active_listings = function (successCallback) {
$http.get('active.json').success(successCallback);
}
DashboardFactory.inactive_listings = function (successCallback) {
$http.get('inactive.json').success(successCallback);
}
DashboardFactory.drafts = function (successCallback) {
$http.get('drafts.json').success(successCallback);
}
return DashboardFactory;
});
Instead of passing callbacks around, prefer proper promises workflow. For this make your service methods return promise objects:
app.factory('DashboardFactory', function ($http) {
var DashboardFactory = {};
DashboardFactory.active_listings = function () {
return $http.get('active.json');
}
DashboardFactory.inactive_listings = function () {
return $http.get('inactive.json');
}
DashboardFactory.drafts = function () {
return $http.get('drafts.json');
}
return DashboardFactory;
});
Then use promise API to handle success (then callback) and errors (catch):
app.controller("draftsCtrl", ["$scope", "DashboardFactory", function ($scope, DashboardFactory) {
DashboardFactory.drafts().then(function (response) {
$scope.rooms_listings = response.data;
})
.catch(function() {
console.log('Error ocurred');
});
}]);
"service" looks more elegantly in this case
function DashboardFactory($http) {
this.active_listings = function () {
return $http.get('active.json');
};
this.inactive_listings = function () {
return $http.get('inactive.json');
};
this.drafts = function () {
return $http.get('drafts.json');
};
});
DashboardFactory.$inject = ['$http'];
app.factory('DashboardFactory', DashboardFactory);
I am trying to decorate the $log service in angularjs for sending the logs to the backend over http. I managed to do this using something like this:
app.config(['$provide', function ($provide) {
$provide.decorator('$log', ['$delegate', loggerDecorator]);
}]);
var loggerDecorator = function ($delegate) {
...decorating logic
$delegate.log = prepareLogFn($delegate.log);
return $delegate;
}
This is fine but, i would prefer to use a provider as the decorator so that I can configure it during the config phase of the module, for setting stuff like delimiters, remote api url etc.
I tried creating a provider like this:
angular.module('mods.logging').factory(serviceId, [backendLoggingDecorator]);
function backendLoggingDecorator() {
var remoteUri = '';
return {
setRemoteUri: function (uri) {
remoteUri = uri;
},
$get: function ($delegate) {
var ajaxLogger = log4javascript.getLogger();
var ajaxAppender = new log4javascript.AjaxAppender(remoteUri);
ajaxAppender.setThreshold(log4javascript.Level.ALL);
ajaxLogger.addAppender(ajaxAppender);
function prepareLogFn(loggingFunc) {
return function () {
if (ajaxLogger) ajaxLogger.info(arguments);
loggingFunc.apply(null, args);
};
}
$delegate.log = prepareLogFn($delegate.log);
return $delegate;
}
};
}
and then doing something like:
app.config(['$provide', function ($provide) {
$provide.decorator('$log', ['backendLoggingDecorator', function(bld) {
return bld;
}]);
}]);
but I am confused on how the actual $delegate can be passed inside the provider and returned to the decorator.
Here you have an example. In the enhanceLogging function you can do anything with your log, decorate it with extra info, post it to a external service, etc.
var myApp = angular.module('myApp', []).provider('logEnhancer', [function () {
this.$get = function () {
return {
enhanceAngularLog: function ($log) {
$log.getInstance = function (context) {
return {
log: enhanceLogging($log.log, context, 'log'),
info: enhanceLogging($log.info, context, 'info'),
warn: enhanceLogging($log.warn, context, 'warn'),
debug: enhanceLogging($log.debug, context, 'debug'),
error: enhanceLogging($log.error, context, 'error')
};
}
function enhanceLogging(loggingFunc, context, level) {
return function () {
var modifiedArguments = [].slice.call(arguments);
modifiedArguments[0] = moment().format('dddd h:mm:ss a') + ' || ' + modifiedArguments[0] + ' || from Ctrl: ' + context;
loggingFunc.apply(null, modifiedArguments);
};
}
}
};
};
}]).run(['$log', 'logEnhancer', function ($log, logEnhancer) {
logEnhancer.enhanceAngularLog($log);
}]).controller('LogCtrl', ['$log', '$scope', function ($log, $scope) {
var $log = $log.getInstance('LogCtrl');
$scope.doTest = function () {
$log.debug("This *will* appear in your console");
};
}]);
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.1/moment.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="LogCtrl">
<h3>Enhance $log service</h3>
<button ng-click="doTest()">test logger</button>
<p>Look the console for the modified <code>$log.debug()</code> message.</p>
</div>
We have a project based on John Papa's HotTowel SPA Using Angular, Breeze and UI-Bootstrap.
We are running in to problems using clientside caching to load Drop downs from SQL Lookup tables. The view loads before the data is cached resulting in empty drop downs when the view first loads. If we refresh the view, the drops downs then populate. We realize that this is sequencing and routing issue but cannot figure out how to make it work.
The issue seems to center around the use of promises and how to recognize when they return successfully. We use the prime function to load the cached data from the database at startup but the search view is loaded before the data is accessible in the getActionDomain() function.
We would appreciate any pointers or ideas.
Thanks,
JG
app.run within app.js is the starting point
(function () {
'use strict';
var serviceId = 'app';
var app = angular.module('app', [
// Angular modules
'ngAnimate', // animations
'ngRoute', // routing
'ngSanitize', // sanitizes html bindings (ex: sidebar.js)
// Custom modules
'common', // common functions, logger, spinner
//'common.bootstrap', // bootstrap dialog wrapper functions
// 3rd Party Modules
'ui.bootstrap' // ui-bootstrap (ex: carousel, pagination, dialog)
]);
// Handle routing errors and success events
app.run(['$route', '$rootScope', '$location', '$http', 'Auth', 'datacontext', 'common', function ($route, $rootScope, $location, $http, Auth, datacontext, common) {
var getLogFn = common.logger.getLogFn;
var log = getLogFn(serviceId);
var logError = getLogFn(serviceId, 'error');
var logSuccess = getLogFn(serviceId, 'success');
var $q = common.$q;
//breeze.core.extendQ($rootScope, $q);
primeData();
function primeData() {
return datacontext.prime()
.then(startRouting)
.then(querySucceeded, _queryFailed, null);
function querySucceeded(data) {
log('Retrieved [Lookups] from remote data source', data, true);
return true;
}
}
function startRouting() {
$rootScope.$on('$routeChangeStart', function (event, next, current) {
$rootScope.error = null;
if ($rootScope.user) {
return true;
} else {
$rootScope.user = {};
var defered = $q.defer();
checkRouting($q, $rootScope, $location);
return defered.promise;
}
});
var checkRouting = function ($q, $rootScope, $location) {
var defered = $q.defer();
Auth.getCurrentUser()
.then(function (data) {
$rootScope.user.isInUserGroup = data.data.IsInUserGroup;
$rootScope.user.firstName = data.data.FirstName.replace(/\"/g, "");
$rootScope.user.lastName = data.data.LastName.replace(/\"/g, "");
$rootScope.user.userName = data.data.UserName.replace(/\"/g, "");
});
return defered.promise;
};
}
function _queryFailed(error) {
var msg = config.appErrorPrefix + 'Error priming data.' + error.message;
logError(msg, error);
throw error;
}
}]);
})();
The prime function is found in the datacontext.js module:
function prime() {
if (primePromise) return primePromise;
var deferred = $q.defer();
primePromise = $q.all([getLookupLists()])
.then(extendMetadata)
.then(success);
function success() {
setLookups();
dataPrimed = true;
//apps.startRouting();
log('Primed the data');
};
function extendMetadata() {
var metadataStore = manager.metadataStore;
var types = metadataStore.getEntityTypes();
types.forEach(function (type) {
if (type instanceof breeze.EntityType) {
set(type.shortName, type);
}
});
function set(resourceName, entityName) {
metadataStore.setEntityTypeForResourceName(resourceName, entityName);
}
}
deferred.promise = primePromise;
return deferred.promise;
}
function setLookups() {
service.lookupCachedData = {
actions: _getAllLocal('ActionDomain', 'sortorder'),
statusCodes: _getAllLocal('StatusDomain', 'sortorder')
}
}
function _getAllLocal(resource, ordering) {
return EntityQuery.from(resource)
.orderBy(ordering)
.using(manager)
.executeLocally();
}
function getLookupLists() {
return EntityQuery.from('Lookups')
.using(manager).execute()
.then(querySucceeded, _queryFailed);
function querySucceeded(data) {
log('Retrieved [Lookups] from remote data source', data, false);
return true;
}
}
The code is called in the search.js view controller module
function activate() {
common.activateController([getActionDomain(), getStatusDomain(), getCpuLog()], controllerId)
.then(function () { log('Activated search View', null, false); });
}
function getActionDomain() {
if (datacontext.lookupCachedData && datacontext.lookupCachedData.actions) {
vm.actions.push({ value: 0 });
datacontext.lookupCachedData.actions.forEach(function (actionItem) {
vm.actions.push(actionItem);
})
}
}
function getStatusDomain() {
if (datacontext.lookupCachedData && datacontext.lookupCachedData.statusCodes) {
vm.statusList.push({ value: 0 });
datacontext.lookupCachedData.statusCodes.forEach(function (statusItem) {
vm.statusList.push(statusItem);
})
}
}
If you want to wait that the promise is resolved before display the view you can use the resolve property in your service $routeProvider when you configure the app module.
There an example:
$routeProvider.when('/staff_profil/edit', {
templateUrl: 'app/app/assets/partials/profil-edit.html',
controller: 'ProfilEditController'
resolve: {
'currentUser':function( UserService){
return UserService.getCurrentUser();
}
});
You have to return a promise in the resolve!!
In this example, I wait to get the currentUser before display my profil-edit page. The UserService.getCurrentUser() is a promise create by the $http service in my UserService.
Moreover you can use this promise resolve in my controller ProfilEditController by injecte the 'currentUser' in my controller like if it is a service and then you can use currentUser.name
in your controller and view.
I hope this will help you!
I have written a service, depending on an other service. But initialisation is not working.
You can find a plunker as showcase
Should be close to working... Any tipps?
Thanks in advance!
edit: The plunker is fixed now and can be used as reference.
You need to either change your testServiceMockConfig and testService from factory to service, for example:
.service('testServiceMockConfig', function ()
or keep them as factories and add return.this; at the bottom of both of them or restructure them like this (recommended):
angular.module('testServiceMockConfig', [])
.factory('testServiceMockConfig', function() {
console.log("setup cqrs mock config.");
return {
doLoadItems: function(callback) {
console.log("mock loading data");
if (!this.configuredLoadItems) {
throw Error("The mock is not configured to loadItems().");
}
callback(this.loadItemsError, this.loadItemsSuccess);
},
whenLoadItems: function(success, error) {
this.configuredLoadItems = true;
this.loadItemsSuccess = success;
this.loadItemsError = error;
}
};
});
I also assume that loadItems in testService should call:
testServiceMockConfig.doLoadItems(callback);
instead of:
testService.doLoadItems(callback);
As I see from your example,
you didn't define properly the factory. The this key used for service
in testService.doLoadItems(callback); replace with testServiceMockConfig.doLoadItems(callback);
The difference between service - factory - provider and definition you can find in this simple demo:
Fiddle
Fixed example:
angular.module('testServiceMockConfig', [])
.factory('testServiceMockConfig', function () {
console.log("setup cqrs mock config.");
return{
doLoadItems : function (callback) {
console.log("mock loading data");
if (!this.configuredLoadItems) {
throw Error("The mock is not configured to loadItems().");
}
callback(this.loadItemsError, this.loadItemsSuccess);
},
whenLoadItems : function (success, error) {
this.configuredLoadItems = true;
this.loadItemsSuccess = success;
this.loadItemsError = error;
}
}
});
angular.module('testService', ['testServiceMockConfig'])
.factory('testService', ['testServiceMockConfig', function (testServiceMockConfig) {
console.log("mock version. testServiceMockConfig: ");
return {
loadItems : function (callback) {
testServiceMockConfig.doLoadItems(callback);
}
}
}])
angular.module('ItemApp', ['testService'])
.controller('ItemsCtrl', ['$scope', 'testService', function ($scope, testService) {
$scope.text = 'No items loaded';
testService.loadItems(function (error, items) {
if (error) {
$scope.text = "Error happened";
}
$scope.text = '';
for (i = 0; i < items.length; i++) {
$scope.text = $scope.text + items[i].name;
}
})
}]);
Demo Plunker