Angular: Why can't I inject $provide directly into $get? - javascript

I assume there's a straightforward (maybe trivial) answer for this that I just haven't wrapped my head around.
Why does this do what I want -- that is, inject a reference to $provide into my service:
angular.module('error_reporting', [])
.provider('RaygunLogger', function() {
var provide = undefined;
this.setProvide = function(p){
provide = p;
}
this.$get = function() {
// use $provide in p
};
})
.config(function(RaygunLoggerProvider, $provide) {
RaygunLoggerProvider.setProvide($provide);
});
while this produces an error of the form Unknown provider: $provideProvider <- $provide <- RaygunLogger <- RaygunLogger?
angular.module('error_reporting', [])
.provider('RaygunLogger', function() {
this.$get = function($provide) {
// use $provide
};
});
Is RaygunLogger.$get() running before the injector is set up? I presume this is an order-of-operations issue, where I need to wait until the module config phase before I can inject $provide, but I don't know where to verify that in the doc.

$provide is only available during the config phase. Provider's $get function is run after the config phase to create the thing the provider provides.
At this point, you cannot do what you are trying to do.

To use it inside $get your normally start your provider function with
var self = this;
and in $get (factory) definition you use self.
E.g
angular.module('foo',[])
.provider('myFoo', function myFooProviderFn() {
var self = this;
self._debug = false;
self.setDebug = function() { self._debug = true; }
self.$get = ['$location', function($location) {
self.setDebug();
});
});
Its wrong to think you cannot use it inside your $get. As such the actual pure provider instance can only be accessed and manipulated during the config phase. The rationale behind this design is probably that providers are thought of to be some mechanism to configure factories.
More from the docs (Provider recipe)

Related

Loading view configuration

I would like to do something like this:
app.config(function($routeProvider){
$routeProvider.when('products/list', {
controller: 'ProductListCtrl',
templateUrl : 'products/list/view.html',
resolve : { data : function(){
...
},
loadingTemplateUrl : 'general/loader.html'
}
});
I would like to have the loading page in a different view.
This would make the code in the view and controller of every page cleaner, (no <...ng-include ng-show="loading"...>). This would also mean that I don't have to $scope.$watch the data for changes. Is there a clean solution to do something similar (not necessarily in the .config method) or an alternative library to do this?
Assuming you want to show some general template for all state transitions while the data is resolved, my suggestion is to listen to the events fired by the routing library. This allows to use one central point to handle all state transitions instead of polluting the routing config (which I think will not be that easy to do).
Please see the docs for $routeChangeStart, $routeChangeSuccess and of course $routeChangeError at the angular router docs
Maybe someone could be interested in what I did: I created a new service and a new view directive. It could seem like a lot of work, but doing this was much easier than I had expected. The new service enables me to separate the main view from the loading view, that I could reuse in all pages of the application. I also provided the possibility to configure an error template url and error controller, for when the loading failed.
The Angular $injector, $templateRequest and $controller services do most of the work. I just had to connect a directive, that depends on these services, to the right event ($locationChangeSuccess), and to the promise, retrieved (using $q.all) from the resolve object's functions. This connection was done in the route service. The service selects the right template url and comtroller, and passes it on for the directive to handle.
A shortened version (with the getCurrentConfig method left out):
RouteService:
(function () {
'use strict';
// provider:
angular.module('pikcachu')
.provider('pikaRouteService', [function () {
var routeConfigArray;
var otherwiseRouteConfig;
//configuration methods
this.when = function (url, routeConfig){
routeConfigArray.push({url: url, routeConfig: routeConfig});
return this;
}
this.otherwise = function(routeConfig){
otherwiseRouteConfig = routeConfig;
return this;
}
// service factory:
this.$get = ['$rootScope', '$location', '$q', '$injector', '$templateRequest',
function ($rootScope, $location, $q, $injector, $templateRequest) {
function RouteService() {
this.setViewDirectiveUpdateFn = function(){ /*...*/ }
function init(){
$rootScope.$on('$locationChangeSuccess', onLocationChangeSuccess);
}
function onLocationChangeSuccess(){
// get the configuration based on the current url
// getCurrentConfig is a long function, because it involves parsing the templateUrl string parameters, so it's left out for brevity
var currentConfig = getCurrentConfig($location.url());
if(currentConfig.resolve !== undefined){
// update view directive to display loading view
viewDirectiveUpdateFn(currentConfig.loadingTemplateUrl, currentConfig.loadingController);
// resolve
var promises = [];
var resolveKeys = [];
for(var resolveKey in currentConfig.resolve){
resolveKeys.push(resolveKey);
promises.push($injector.invoke(resolve[resolveKey]));
}
$q.all(promises).then(resolveSuccess, resolveError);
function resolveSucces(resolutionArray){
// put resolve results in an object
var resolutionObject = {};
for(var i = 0; i< promises.length;++i){
resolved[resolveKeys[i]] = resolutionArray[i];
}
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
function resolveError(){
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
}
}
init();
}
return new RouteService();
}]
})();
View directive
(function () {
'use strict';
angular.module('pikachu')
.directive('pikaView', ['$templateRequest', '$compile', '$controller', 'pikaRouteService', function ($templateRequest, $compile, $controller, pikaRouteService) {
return function (scope, jQdirective, attrs) {
var viewScope;
function init() {
pikaRouteService.listen(updateView);
}
function updateView(templateUrl, controllerName, resolved) {
if(viewScope!== undefined){
viewScope.$destroy();
}
viewScope = scope.$new();
viewScope.resolved = resolved;
var controller = $controller(controllerName, { $scope: viewScope });
$templateRequest(templateUrl).then(onTemplateLoaded);
function onTemplateLoaded(template, newScope) {
jQdirective.empty();
var compiledTemplate = $compile(template)(newScope);
jQdirective.append(compiledTemplate);
}
}
init();
};
}
]);
})();

How to unit test custom decorator with Jasmine (Angular js)

So I have such decorator in app config:
angular.module('app').config(['$provide', function ($provide) {
$provide.decorator('$rootScope', ['$delegate', function ($delegate) {
$delegate.constructor.prototype.$onRootScope = function (name, listener) {
var unsubscribe = $delegate.$on(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchRootScope = function (name, listener) {
var unsubscribe = $delegate.$watch(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchAfterLoad = function (watchExpression, listener, objectEquality) {
var initialLoad = true;
this.$watch(watchExpression, function () {
if (initialLoad) {
// note: this obviously runs outside of angular, so sometimes the timeout could run after initial load
setTimeout(function () { initialLoad = false; }, 25);
} else {
listener.apply(this, arguments);
}
}, objectEquality);
};
return $delegate;
}]);
}]);
As you can see this decorator lets me use $scope.$onRootScope instead of $rootScope.$on and takes care of automatic listeners removal on scope destroy event...
When I unit test my code which logic contains $scope.$onRootScope I'm getting such error: TypeError: undefined is not a constructor (evaluating 'scope.$onRootScope') in
Before each test I'm loading all required models and do inject which looks like this ~:
beforeEach(function () {
inject(function (_$rootScope_) {
$rootScope = _$rootScope_;
});
});
How should I overcome this problem?
Is there a way to mock / mimic $scope.$onRootScope behaviour?
I'm quite new to unit testing & Jasmine so sorry for not very nicely formatted question.
EDIT #1:
As I'm mocking my $scope object (var $scope = {...}) before passing it as argument to service method which I'm testing I can avoid error by simply defining $scope method:
$scope = {
...
$onRootScope: function() {}
}
Still awaiting for some better ideas :-)
I believe you need to build your $scope based off of the decorated $rootScope, as opposed to creating a new dummy object.
Like so:
var $root, $scope;
beforeEach(function () {
module('app');
inject(function ($rootScope) {
$root = $rootScope;
$scope = $root.$new();
});
});
it('should have the expected property', function () {
expect($scope.constructor.prototype).to.have.property('$watchRootScope');
});
I'll chuck in a link to the spec suite of a mini-lib I put together some time ago, doing roughly the same thing you are now.

What's the proper way to use this extension in AngularJS code?

I'm using a framework called Radiant UI, which is a way to get HTML5 UI into Unreal Engine 4. I'm trying to pick up some modern Javascript while I do that, so I'm building the UI in AngularJS.
My understanding of Angular is still pretty weak though, and I'm a bit confused about what the best practice is here. The extension injects the following Javascript when it sets up.
var RadiantUI;
if (!RadiantUI)
RadiantUI = {};
(function() {
RadiantUI.TriggerEvent = function() {
native function TriggerEvent();
return TriggerEvent(Array.prototype.slice.call(arguments));
};
RadiantUI.SetCallback = function(name, callback) {
native function SetHook();
return SetHook(name, callback);
};
RadiantUI.RemoveCallback = function(name) {
native function RemoveHook();
return RemoveHook(name);
};
})();;
So this is simply pushing RadiantUI into the global namespace. That would be fine if the extension was always there, but it isn't. In the test environment (Chrome), it's not there. It's only there when running in the game engine. That, combined with the fact that globals suck, means I want to encapsulate it.
In the previous iteration of this, I had it wrapped in an AMD module, and it worked well. Like this:
define([], function()
{
if ("RadiantUI" in window)
{
console.log("RadiantUI in global scope already!");
return window.RadiantUI;
}
var RadiantUI;
if (!RadiantUI) {
RadiantUI = {};
RadiantUI.TriggerEvent = function() {}
RadiantUI.SetCallback = function() {}
RadiantUI.RemoveCallback = function() {}
}
console.log("Using fake RadiantUI bindings");
return RadiantUI;
});
So here's what I want to do:
I want to include radiant as a dependency to my app/stateProvider and have it injected, much the same way it would be in AMD. With the stub methods in place if the extension isn't present. What's the proper approach to this? A module? A service provider?
UPDATE: This is the working code using the answer given.
var myapp = angular.module('bsgcProtoApp', ['ui.router' ]);
myapp.value('radiant', window.RadiantUI || {
TriggerEvent: function()
{
console.log("TriggerEvent called");
},
SetCallback: function(name, callback)
{
console.log("Setcallback called");
},
RemoveCallback: function(name)
{
console.log("RemoveCallback called");
}
});
myapp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider )
{
$urlRouterProvider.otherwise("/mainmenu");
$stateProvider.state('mainmenu',
{
name: "mainmenu",
url: "/mainmenu",
templateUrl: 'templates/mainmenu.html',
controller: ['$scope', 'radiant', function($scope, radiant)
{
$scope.tester = function()
{
radiant.TriggerEvent("DuderDude");
console.log("Duder!");
}
}],
});
}]);
You presumably have an Angular module or app. For the sake of this answer, let's call it MyApp.
Now you can do
MyApp.value("RadiantUI", window.RadiantUI || {
TriggerEvent = function(){},
//... more properties
});
Now to access this value as a dependency in a controller for example, you'd do this
MyApp.controller(["$scope", "RadiantUI", function($scope, RadiantUI){
// ... controller code ...
}]);

$log decorator with a service dependency causes circular dependency error

In my app i am writing a decorator for $log so that i can customize the functionality of $log including calling a third party service. The third party service injects $q for its internal activities. Now this causes circular dependency error :
Uncaught Error: Circular dependency: $q <- tploggerService <- $log <- $exceptionHandler <- $rootScope.
Because qProvider uses exceptionHandlerProvider which ultimately uses logProvider which i guess is causing this. Has any one faced similar issues while decorating and is there a solution to work around this or a different pattern to work around the problem?
Here is a simple demonstration of code, Appreciate your help:
///Some third party APP
angular.module('SomeThirdPartyApp', []);
tploggerService.$inject = ['$q']; //<-- $q is a dependency
function tploggerService ($q) {
this.info = function (data) {
var deferred = $q.defer(); //Doing something...
//....
//....
};
}
angular.module('SomeThirdPartyApp').service('tploggerService', tploggerService);
///--------------------------------------------------------------------------------
///MY APP
angular.module('decorApp', ['SomeThirdPartyApp']);
angular.module('decorApp').config([
'$provide', function ($provide) {
$provide.decorator('$log', ['tploggerService','$delegate',
function (tploggerService, $delegate) { //<--- Injecting tpLoggerService causes circular dependency error.
var _info = $delegate.info;
//It is no different even if we use $injector
$delegate.info = function(){
var args; //doing something with the arguments basically formatting massaging it.
var customlogmessage; //doing something with args
tploggerService.info(customlogmessage);
_info.apply(null, args);
}
return $delegate;
}]);
}]);
JSBIN
Plnkr after $q removed
Get $q from inside of your service:
function tploggerService ($injector) {
var $q;
this.info = function (data) {
$q = $injector.get('$q');
var deferred = $q.defer(); //Yes using defered object. some this performs some actions and some internal stuffs.
//Doing something...
};
}
Updated Plunk
It seems that you can only $delegate in the $provider.decorator(), I think if you should put the bussiness logic in the decorator function other than using an inject way.For example:
$provider.decorator('$log',function($delegate){
$delegate.info = function(){
var args = Array.prototype.slice(arguments);
var deferred = $q.defer(); //Doing something...
$delegate.info.apply(null,args);
}
});

Angular js - services and circular injection error

Why does i can't inject services like this?
service('actionService',function (sessionService) {
this.doAction = function () {
return 5;
}
});
service('sessionService',function (userService) {
this.get = function {
if(userService.check()){
//do other stuffs
}
}
});
service('userService',function (actionService) {
this.check = function () {
actionService.doAction();
if(x = 9){
return true;
}else{
return false;
}
}
});
I get "Circular injection error" in console
how to handle this now ?
here a better code : http://pastebin.com/wS32UNCq
Before you can have an instance of actionService, you need to inject an instance of sessionService.
Before you can have an instance of sessionService, you need to inject an instance of userService.
Before you can have an instance of userService, you need to inject an instance of actionService.
It's a chicken or the egg problem... because Angular uses constructor injection, it can't instantiate a service until all of its dependencies are satisfied. But the dependencies go around in a circle, none of them can ever be instantiated.
To solve this problem, you will need to extract a shared dependency out to an additional service which can be instantiated without dependencies.

Categories

Resources