Function inside provider not recognized by Angular - javascript

First a little background on what I want to achieve. I have a service that requires some configuration before it gets injected and used. After doing some research I figured that having a provider for the service would be the best solution. So I implemented the provider according to this example. Even thought the Typescript compiler (I'm using typescript to compile my code into valid JavaScript) thinks it's oké, JavaScript does not recognize the function that's available through the provider to set some options.
My code looks as follows (some code has been left out or renamed for a reason)
export interface ICustomServiceProvider extends ng.IServiceProvider {
setOptions(options: ICustomOptions): void;
$get($http, $window, $resource): ICustomService;
}
class CustomServiceProvider implements ICustomServiceProvider {
private options: ICustomOptions;
public $get($http, $window, $resource) {
return new CustomService($http, $window, $resource);
}
public setOptions(options: ICustomOptions): void {
this.options = options;
}
}
angular.module('custom', ['ngResource'])
.service('CustomService', CustomService)
.provider('CustomService', CustomServiceProvider);
The problem occurs when using the provider in one of my unit tests (I'm using Karma with Mocka for testing) and calling the setOptions function. Which is done like this.
describe('CustomService', function() {
var provider: ICustomServiceProvider;
var options: {hello: 'world'};
beforeEach(inject(function($injector: ng.auto.IInjectorService) {
provider = <ICustomServiceProvider> $injector.get('');
}));
it('CustomService provider test', function() {
provider.setOptions(options);
});
}
When running this test Karma throws an error saying
TypeError: provider.setOptions is not a function at Context.[anonymous]
Also the compiled JavaScript Visual Studio Code is giving me a green warning (I don't think it's a error) on the provider variable on the line .provider('CustomService', CustomServiceProvider);
Argument of type '() => void' is not assignable to parameter of type 'IServiceProvider'. Property '$get' is missing in type '() => void'.
(local var) CustomServiceProvider: () => void
Providers
I already spent hours on fixing this problem but cannot seem to find the solution. Any idea on how to fix this of what I'm doing wrong?
Thanks in advance.
Edit (30-09-2015)
The service I'm am talking about looks like this:
export interface ICustomService {
// Some function definitions
}
class CustomService implements ICustomService {
static $inject = ['$http', '$window', '$resource'];
constructor(httpService: ng.IHttpService, windowService: ng.IWindowService, resourceService: ng.resource.IResourceService, options: ICustomOptions) {
// Setting variables
}
}

The main problem here, IHMO, is the fact that your are trying to register a service & a provider with the same name (CustomService). You don't have to do this.
In your case, it seems that you just have to register the provider. Rename your CustomServiceProvider class to CustomService, then change your last lines of code to just:
angular
.module('custom', ['ngResource'])
.provider('CustomService', CustomService);
This way, in the config phase of your app, you will be able to inject CustomServiceProvider (and use setOptions), but also CustomService in controllers, directives & other services.
Please read Angular Documentation on providers (mainly "Provider Recipe") for more information.

Related

Angular: manually instantiate class with dependency injection

I am using Angular 10.0 and I have a problem with --prod compiling.
I need to instantiate classes manually and need to support dependency injection.
The following code works fine during development to instantiate my classes:
public instantiateWithDi(parentInjector: Injector, myClass: any): any {
const reflector = ReflectiveInjector.resolveAndCreate([], parentInjector);
const newInstance = reflector.resolveAndInstantiate(myClass);
return newInstance;
}
When I build my project with --prod (or --optimization=true), then I get the following error at runtime:
ERROR Error: Cannot resolve all parameters for 'e'(?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'e' is decorated with Injectable.
Decorating the constructor parameters of the classes with #Inject did not work either. Using injection tokens does not help as well.
The classes are already decorated with #Injectable() and in the "providers" array of their respective angular module.
I know, the ReflectiveInjector is deprecated, but simply using the get method of the injector does not work either, because it seems to cache the classes once created and does not re-instantiate them each time I call my "instantiateWithDi" method.
Example usage
I've created a small demo at stackbliz for this: https://stackblitz.com/edit/angular-plugin-mechanism?file=src/app/plugin-execution.service.ts
Basically the magic happens here (plugin-execution.service.ts):
#Injectable({
providedIn: 'root'
})
export class PluginExecutionService {
public static readonly eventListeners = [];
constructor(private injector: Injector){}
private instantiateWithDi(parentInjector: Injector, myClass: any): any {
const reflector = ReflectiveInjector.resolveAndCreate([], parentInjector);
const newInstance = reflector.resolveAndInstantiate(myClass);
return newInstance;
}
public onApplicationEvent(event: ApplicationEvent){
const injector = Injector.create({
parent: this.injector,
providers: [{
provide: ApplicationEvent,
useValue: event
}]
});
PluginExecutionService
.eventListeners
.forEach(cls => this.instantiateWithDi(injector, cls));
}
}
This allows developers to create a class and push their class into a eventListener array. It gets executed every time, an application event occurs.
See the example "plugin" some.plugin.ts in the stackblitz example.
The real usecase is of course much more complex and involves custom decorators and stuff, but that would be quite an overkill for a demo.
You see the result in the console. The "plugins" work fine as intended. But when i build it using --prod, the app does not work any longer...
Any help is very much appreciated!
Thanks,
Manuel

extend AngularJs's angular global object in TypeScript

I upgraded my typescript version to 3.8.3
my project is written with AngularJS and includes .js and .ts files.
I have a legacy code that extends the angular global object with 2 functions.
angular.isUndefinedOrNull = function (val) {
...
};
angular.isDefinedAndNotNull = function (val) {
...
}
after upgrading the typescript version I have got this error message:
Property 'isDefinedAndNotNull' does not exist on type 'IAngularStatic'
I understand that I need to create an extended interface for angular object, but I am struggling with how, everything that I'm trying does not succeed.
tried to add a global.d.ts file to the root directory with the following :
declare namespace angular {
interface IAngularStatic {
isUndefinedOrNull: (any) => boolean;
isDefinedAndNotNull: (any) => boolean;
}
}
what am I missing?
you're close to it but it's like this:
import angular from 'angular';
declare module angular {
interface IAngularStatic {
isUndefinedOrNull: (any) => boolean;
isDefinedAndNotNull: (any) => boolean;
}
}
however, I wouldn't recommend this approach and would instead suggest refactoring to use simple imported functions where needed and using newish typescript features like the null coalescence operator ??

Subscribing to a mocked variable in a service unit test

I'm trying to unit test a component that subscribes to a data service variable. I'm trying to mock the service and override the component's private data service variable but I'm not sure how to test the subscription of the mocked variable. Here's my (limited) code:
Service:
export class Service {
constructor(private dataService: DataService) {
this.dataService.newData.subscribe(data => {
//do something
});
}
}
Data Service:
export class DataService {
private source = new BehaviorSubject<any>([]);
newData = this.source.asObservable();
//code to update source
}
unit test for Service:
mockDataService {
private source = new BehaviorSubject<any>([]);
newData = this.source.asObservable();
}
describe('Service', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
Service,
{provide: DataService, useClass: mockDataService} // is this correct?
]
}).overrideComponent(Service, {
set: {
providers: [
{provide: DataService, useClass: mockDataService}
]
}
}).compileComponents();
});
it('should register subscription', inject([Service, DataService], (service: ServiceA, mock: DataService) => { //should I be injecting the mock class?
expect(before()).toBeTruthy();
mock.newData.next("test"); // what's the correct way to test?
expect(after()).toBeTruthy();
}));
}
Am I overriding correctly? And if so, how do I correctly test that my component does the correct actions when subscribing to a private service's variable?
Thanks!
First, it would be helpful if you could provide the annotations alongside the classes. I am assuming that the Service class is a component, because you reference is when calling TestBed.overrideComponent. In that case the naming is confusing. It should have at least a suffix "Component" (see Angular style guide.)
If Service should actually be a service class, nesting services into another one is probably not a good practice (see docs.)
You are basically asking for two things.
1. Do I need to override the providers property of the module via TestBed.configureTestingModule?
For your example above, this is not necessary. You can easily omit the providers attribute from the object. It will then looks like
TestBed.configureTestingModule({})
There might be some cases where changing the providers is needed - but not in your case.
2. How should I test the service properly?
It seems like you are mixing up integration testing with unit testing. You want to test the service in both ways.
First: Unit test the service (Angular docs)
Second: Integration test – what you seem to be doing here. There is a recommended best practice as of the docs (link):
it('should register subscription', () => {
const fixture = TestBed.createComponent(Service);
dataService = fixture.debugElement.injector.get(DataService);
// do things
});
Regarding the mock.newData.next("test"), it is not really clear what you are trying to achieve here. This method call would probably give you an undefined function test error. Why? You are referring to this.source.asObservable() which returns an Obersvable. This object does not have a next method. You should maybe do some basic tutorials on RxJs.
Hope this helps!
Best,
Benji

Angular services with browserfy

I've using the angular fullstack generator for a long time and I recently updated it and noticed it no longer uses bower and that it uses require to get the modules in the client. My issue is that I can't understand how this works at all when I try to declare a service.
if I run, for example, yo angular-fullstack:service example it creates a example.service.js in the client/app/example path with the following code:
'use strict';
const angular = require('angular');
/*#ngInject*/
export function exampleService() {
// AngularJS will instantiate a singleton by calling "new" on this function
}
export default angular.module('sisaApp.example', [])
.service('example', exampleService)
.name;
Let's say that I want to use $http here. Do I pass it as a parameter to the function?
Apart from that, how do I go about injecting this service into a controller?
If I run yo angular-fullstack:route myroute and it generates th following controller file:
'use strict';
const angular = require('angular');
const uiRouter = require('angular-ui-router');
import routes from './myroute.routes';
export class MyrouteComponent {
/*#ngInject*/
constructor() {
this.message = 'Hello';
}
}
export default angular.module('sisaApp.myroute', [uiRouter])
.config(routes)
.component('myroute', {
template: require('./myroute.html'),
controller: MyrouteComponent,
controllerAs: 'myrouteCtrl'
})
.name;
Doing it the old was by passing it as a parameter into the constructor doesn't seem to be working for me.
Any help would be appreciated!
Thanks in advance!

'ShouldWorkController' is not a function. Got undefined

When I try to bind a controller to a template using the angular-ui-router $stateProvider, I run into the following error:
'ShouldWorkController' is not a function. Got undefined.
However, when I declare the controller inside the template using ng-controller, everything works fine. What could be wrong here?
app.ts
module App {
var dependencies = [
MyControllers
]
function configuration($stateProvider: ng.ui.IStateProvider) {
$stateProvider
.state("shouldWork", {
url: "/shouldWork",
templateUrl: "app/shouldWork.html"
controller: "ShouldWorkController" // does not work
});
}
}
shouldWorkController.ts
module App.MyControllers {
interface IShouldWorkViewModel {
}
class ShouldWorkController implements IShouldWorkViewModel {}
}
ShouldWork.html
<div ng-controller="ShouldWorkController as viewModel" us-spinner spinner-key="spinner-1">
^ --- this works nicely
That message means, that such controller "ShouldWorkController" is not loaded int he main angular module. Be sure that you do call register at the end:
module App.MyControllers {
...
class ShouldWorkController implements IShouldWorkViewModel {}
}
// we have to register this controller into some module (MyControllers)
// which is also referenced in the main app module
angular.module('MyControllers')
.controller('ShouldWorkController', App.MyControllers.ShouldWorkController );
I realise this is old, but I came here via Google with the same issue, not for the firs time. Things to check include:
Export statement for your controller class. From the code you posted I see that you are missing an export statement for your ShouldWorkController class. This may not be the issue in your case, but it is something you should check. I can reproduce this error by removing the export statement from my controller classes.
Check your HTML template exists (if using UI-Router). As described in the UI-Router documentation: "The controller will not be instantiated if template is not defined."
Register your controllers in the same file as the controller. Some tutorials do demonstrate controllers being registered in the module file. While this does work, I personally have found it more error prone than directly registering the controller within the controller file.
Check your typescript references. Make sure that you add typescript references (e.g. ///<reference path="../app/services/shouldWorkService.ts">) to typescript files that contain any types that you reference
Check the name of your controller matches that declared in your $stateProvider configuration.

Categories