I'm getting started using Angular 2 using ES5, and I've hit a brick wall when setting up my providers. In essence, I want to make a provider depend on HTTP. Here's how i have it set up:
var Provider = ng.core.Class({
constructor: [ng.http.Http, function(http) {
// some code here that uses HTTP
}]
});
var Component = ng.core.Component({
providers: [Provider, ng.http.HTTP_PROVIDERS]
}).Class({
constructor: [Provider, function(provider) {
// some code here that uses my Provider
}]
});
I keep getting the following error: No provider for t! (e -> t)
I've omitted the rest of the boilerplate code because this is where I'm stuck. Do I have a misunderstanding about how I'm injecting my dependencies? How do I set up hierarchical dependencies in Angular 2 in ES5?
I figured it out. In the actual code I was using, I ended up having something like this: providers: [[Provider, ng.http,HttpProviders]] which shouldn't have been a nested array
Related
I was wondering whether I can use a factory to initialize a controller and then add it to a module. Code could look something like this, but this is not working:
const controllerFactory = {
provide: DefinitionController,
useFactory: async (service: DefinitionService) => {
//initialization of controller
return new DefinitionController();
},
inject: [DefinitionService],
};
#Module({
controllers: [controllerFactory],
providers: [DefinitionService],
})
export class DefinitionModule {}
It looks like using factories for controllers is not supported, but I am not sure. There is an example of using factory for providers, but I cannot find anything for controller in documentation or on google.
It's not possible to define your controller with an async factory comparable to custom providers. You cannot add dynamic endpoints/routes unless using the native express/fastify instance:
At the moment there is no way to register a route dynamically except
by using the internal HTTP / Fastify / Express instance
There is an issue where a dynamic routing module is discussed but this will probably not be part of nest very soon:
At the moment both Kamil and I are really busy, so this issue may take
some time - except someone else takes on the task :)
But you can use the OnModuleInit lifecycle event to do static initialization:
#Injectable()
export class DefinitionController implements OnModuleInit {
onModuleInit() {
console.log(`Initialization...`);
}
It will be called once when your app starts and has access to the injected providers in your controller, e.g. your DefinitionService.
With angular 5.0 the upgrade module now has the option of using downgradeModule which runs angularjs outside of the angular zone. While experimenting with this I have run into a problem with using downgradeInjectable.
I am receiving the error:
Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.
Bootstrapping angular in angular js works fine
import 'zone.js/dist/zone.js';
import * as angular from 'angular';
/**
* Angular bootstrapping
*/
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { decorateModuleRef } from 'src/environment';
import { AppModule } from 'src/app/app.module';
import { downgradeModule } from '#angular/upgrade/static';
export const bootstrapFn = ( extraProviders ) => {
const platformRef = platformBrowserDynamic( extraProviders );
return platformRef
.bootstrapModule( AppModule )
.then( decorateModuleRef );
};
angular.module( 'app.bootstrap', [
downgradeModule( bootstrapFn ),
] );
However...
Since the bootstrapping takes place after angularjs has been initialized I can no longer get the downgrade injectable working.
Service to be downgraded
import { Injectable, Inject, OnInit } from '#angular/core';
#Injectable()
export class MobileService implements OnInit{
constructor(
#Inject( 'angularjsDependency1' ) public angularjsDependency1 : any,
#Inject( 'angularjsDependency2' ) public angularjsDependency2 : any,
) {}
}
Downgrade injectable attempt
import * as angular from 'angular';
import { downgradeInjectable } from '#angular/upgrade/static';
import { MyService } from 'src/services/myService/myService';
export const myServiceDowngraded = angular.module( 'services.mobileService', [
angularjsDependency1,
angularjsDependency2,
] )
.factory(
'mobileService',
downgradeInjectable( MyService ),
).name;
When "downgradeInjectable( MyService ) runs the angular injector is not available yet since angular hasn't been bootstrapped. Hence the error:
Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.
Does anyone have an idea how I might fix this?
Answers in this thread helped me find a solution, but none contains the holy grail:
Creating a service-boostrap component aside the app's code does not work, because Angular is loaded asynchronously, unlike AngularJS. This gives the same error Trying to get the Angular injector before bootstrapping an Angular module.
Creating a service-bootstrap component wrapping the AngularJS code kind of worked, but then I experienced issues with change detection inside Angular composants, as described in this issue on github.
In the github issue, someone suggested to edit #angular/upgrade source code to change a false to true to force components to be created in the Zone. But in this case it seems to cause performance issues (it seemed to launch ngZone's code multiple times on user events)
In order for the app to work correctly, I needed :
Not to have ng components containing AngularJS components containing Angular components. We need to only have AngularJS containing Angular components.
Make sure that AngularJS components using Angular services are created after a first angular component, named service-bootstrap
To acheive this, I created a slightly modified service-bootstrap component:
import { Component, Output, EventEmitter, AfterViewInit } from "#angular/core";
#Component({
selector: 'service-bootstrap',
template: ``
})
export class ServiceBootstrapComponent implements AfterViewInit{
#Output()
public initialized: EventEmitter<void> = new EventEmitter();
public ngAfterViewInit(){
this.initialized.emit();
}
}
Declared this component as entryComponent in the Angular module and called downgradeComponent to register it in AngularJS:
import { downgradeModule, downgradeInjectable, downgradeComponent } from '#angular/upgrade/static';
const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = platformBrowserDynamic(extraProviders);
return platformRef.bootstrapModule(AppModule);
};
const downgradedModule = downgradeModule(bootstrapFn);
const app = angular.module('ngApp', [
downgradedModule,
'app'
]);
app.directive('serviceBootstrap', downgradeComponent({ component: ServiceBootstrapComponent }));
Then (and the magic happens here), I created a new AngularJS component:
angular.module("app")
.directive('ng1App', ng1AppDirective);
function ng1AppDirective(){
return {
template: `
<service-bootstrap (initialized)="onInit()"></service-bootstrap>
<section ng-if="initialized">
<!-- Your previous app's code here -->
</section>
`,
controller: ng1AppController,
scope: {},
};
}
ng1AppController.$inject = ['$scope'];
function ng1AppController($scope){
$scope.onInit = onInit;
$scope.initialized = false;
function onInit(){
$scope.initialized = true;
}
}
Then, my index.html only referenced this component
<body>
<ng1-app></ng1-app>
</body>
With this approach, I'm not nesting AngularJS components inside Angular components (which breaks change detection in Angular components), and still I ensure that a first Angular component is loaded before accessing the Angular providers.
Note: The answer below follows the convention of calling angular 1.x as angularjs and all angular 2+ versions as simply angular.
Expanding on JGoodgive's answer above, basically, if you're using downgradeModule, then angular module is bootstrapped lazily by angularjs when it needs to render the first angular component. Until then, since the angular module isn't initialised, if you are accessing any angular services inside angularjs using downgradeInjectable, those services aren't available too.
The workaround is to force bootstrapping of the angular module as early as possible. For this, a simple component is needed:
import {Component} from '#angular/core';
#Component({
selector: 'service-bootstrap',
template: ''
})
export class ServiceBootstrapComponent {}
This component doesn't do anything. Now, we declare this component in the top level angular module.
#NgModule({
// ...providers, imports etc.
declarations: [
// ... existing declarations
ServiceBootstrapComponent
],
entryComponents: [
// ... existing entry components
ServiceBootstrapComponent
]
})
export class MyAngularModule {}
Next, we also need to add a downgraded version of this component to angularjs module. (I added this to the top level angularjs module I had)
angular.module('MyAngularJSModule', [
// ...existing imports
])
.directive(
'serviceBootstrap',
downgradeComponent({ component: ServiceBootstrapComponent }) as angular.IDirectiveFactory
)
Finally, we throw in this component in our index.html.
<body>
<service-bootstrap></service-bootstrap>
<!-- existing body contents -->
</body>
When angularjs finds that component in the markup, it needs to initialise angular module to be able to render that component. The intended side effect of this is that the providers etc. also get initialised and are available to be used with downgradeInjectable, which can be used normally.
This was pointed out to me in an angular github thread.
https://github.com/angular/angular/issues/16491#issuecomment-343021511
George Kalpakas's response:
Just to be clear:
You can use downgradeInjectable() with downgradeModule(), but there are certain limitations. In particular, you cannot try to inject a downgraded injectable until Angular has been bootstrapped. And Angular is bootstrapped (asynchronously) the first time a downgraded component is being rendered. So, you can only safely use a downgraded service inside a downgraded component (i.e. inside upgraded AngularJS components).
I know this is limiting enough that you might decide to not use downgradeInjectable() at all - just wanted to make it more clear what you can and can't do.
Note that the equivalent limitation is true when using an upgraded injectable with UpgradeModule: You cannot use it until AngularJS has been bootstrapped. This limitation usually goes unnoticed though, because AngularJS is usually bootstrapped in the Angular module's ngDoBootstrap() method and AngularJS (unlike Angular) bootstraps synchronously.
I had the same issue, and the reasons are explained in the above answer.
I fixed this by dynamically injecting the downgraded angular service using $injector.
Steps
Register your downgraded service to angularjs module
angular.module('moduleName', dependencies)
angular.factory('service', downgradeInjectable(Service));
Inject $injector to your controller and use this to get the downgraded service
const service = this.$injector.get('service');
service.method();
I had the same issue and it sucked up several hours before finding this.
My workaround was to create a ServiceBootstrapComponent that does nothing but injects all the services that we need to downgrade.
I then downgrade that component, mark it as en entry in #NgModule and add it to index.html.
Works for me.
I was getting the same error in our hybrid app. We are using the following versions:
AngularJS 1.7.x
Angular 7.3.x
As mentioned in this answer, I also used a dummy component called <ng2-bootstrap> to force boostrapping of Angular. And then, I created an AngularJS service which checks if Angular has been bootstrapped:
// tslint:disable: max-line-length
/**
* This service can be used in cases where Angular fails with following error message:
*
* `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
*
* Above error occurs because of how `downgradeModule` works.
*/
/*#ngInject*/
export class Ng2BootstrapDetectionService {
private bootstrapDone = false;
constructor(private $q: ng.IQService) {}
public whenBootstrapDone(): ng.IPromise<void> {
if (this.bootstrapDone) {
return this.$q.resolve();
}
const deferred = this.$q.defer<void>();
angular.element(document).ready(() => {
const intervalId = setInterval(() => {
const el = document.querySelector('ng2-bootstrap');
if (el && el.outerHTML.includes('ng-version=')) {
this.bootstrapDone = true;
clearInterval(intervalId);
deferred.resolve();
}
}, 500);
});
return deferred.promise;
}
}
Ng2BootstrapDetectionService can be used like below:
import {NotificationService} from 'ng2-app/notification.service';
// This can be used in cases where you get following error:
// `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
// You will need access to following
// $injector: AngularJS Injector
// Ng2BootstrapDetectionService: our custom service to check bootsrap completion
this.Ng2BootstrapDetectionService
.whenBootstrapDone()
.then(() => {
const notificationService = this.$injector
.get<NotificationService>('ng2NotificationService');
notificationService.notify('my message!');
});
You can find more details about this solution at the end of this blog post.
I know there are 2 similar questions about this but with no solution for any of them.
So I found this issue in Angular repo where they ask for the same, i.e an alternative for templateCache in Angular 2 but they close it saying you can use CachedResourceLoader.
So my question is how to use this CachedResourceLoader to replace the templateCache, I have been searching on google about this but couldn't find any related content so maybe I am not pointing to the right direction or I missed something.
The answer to this question could be a valid answer for the other 2 similar questions.
Code example for the functionality that templateCache provided in AngularJS:
Adding:
var myApp = angular.module('myApp', []);
myApp.run(function($templateCache) {
$templateCache.put('templateId.html', 'This is the content of the template');
});
Retrieval via $templateCache:
$templateCache.get('templateId.html')
Or retrieval:
myApp.component('myComponent', {
templateUrl: 'templateId.html'
});
CachedResourceLoader is existing yet undocumented Angular 2+ substitution for AngularJS $templateCache:
An implementation of ResourceLoader that uses a template cache to
avoid doing an actual ResourceLoader.
The template cache
needs to be built and loaded into window.$templateCache via a
separate mechanism.
It is supposed to work by providing ResourceLoader provider:
{provide: ResourceLoader, useClass: CachedResourceLoader}
Which was already defined in existing RESOURCE_CACHE_PROVIDER export.
And window.$templateCache is supposed to contain pairs of URLs and responses.
Since ResourceLoader should be specified before compilation, it should be provided not in application module but in compiler options.
Here is an example:
import {RESOURCE_CACHE_PROVIDER} from '#angular/platform-browser-dynamic';
import {COMPILER_OPTIONS} from '#angular/core';
window['$templateCache'] = { 'app.html': `...`};
platformBrowserDynamic({
provide: COMPILER_OPTIONS,
useValue: { providers: [RESOURCE_CACHE_PROVIDER] },
multi: true
}).bootstrapModule(AppModule)
As opposed to AngularJS $templateCache, CachedResourceLoader doesn't allow to make requests for missing templates. This is desirable behaviour most of the time. If it has to be changed, a custom implementation that extends default ResourceLoader implementation can be used instead.
I have built a shared data service that's designed to hold the users login details which can then be used to display the username on the header, but I cant get it to work.
Here's my (abbreviated) code:
// Shared Service
#Injectable()
export class SharedDataService {
// Observable string source
private dataSource = new Subject<any>();
// Observable string stream
data$ = this.dataSource.asObservable();
// Service message commands
insertData(data: Object) {
this.dataSource.next(data)
}
}
...
// Login component
import { SharedDataService } from 'shared-data.service';
#Component({
providers: [SharedDataService]
})
export class loginComponent {
constructor(private sharedData: SharedDataService) {}
onLoginSubmit() {
// Login stuff
this.authService.login(loginInfo).subscribe(data => {
this.sharedData.insertData({'name':'TEST'});
}
}
}
...
// Header component
import { SharedDataService } from 'shared-data.service';
#Component({
providers: [SharedDataService]
})
export class headerComponent implements OnInit {
greeting: string;
constructor(private sharedData: SharedDataService) {}
ngOnInit() {
this.sharedData.data$.subscribe(data => {
console.log('onInit',data)
this.greeting = data.name
});
}
}
I can add a console log in the service insertData() method which shoes the model being updated, but the OnInit method doesn't reflect the change.
The code I've written is very much inspired by this plunkr which does work, so I am at a loss as to what's wrong.
Before posting here I tried a few other attempts. This one and this one again both work on the demo, but not in my app.
I'm using Angular 2.4.8.
Looking through different tutorials and forum posts all show similar examples of how to get a shared service working, so I guess I am doing something wrong. I'm fairly new to building with Angular 2 coming from an AngularJS background and this is the first thing that has me truly stuck.
Thanks
This seems to be a recurring problem in understanding Angular's dependency injection.
The basic issue is in how you are configuring the providers of your service.
The short version:
Always configure your providers at the NgModule level UNLESS you want a separate instance for a specific component. Only then do you add it to the providers array of the component that you want the separate instance of.
The long version:
Angular's new dependency injection system allows for you to have multiple instances of services if you so which (which is in contrast to AngularJS i.e. Angular 1 which ONLY allowed singletons). If you configure the provider for your service at the NgModule level, you'll get a singleton of your service that is shared by all components/services etc. But, if you configure a component to also have a provider, then that component (and all its subcomponents) will get a different instance of the service that they can all share. This option allows for some powerful options if you so require.
That's the basic model. It, is of course, not quite so simple, but that basic rule of configuring your providers at the NgModule level by default unless you explicitly want a different instance for a specific component will carry you far.
And when you want to dive deeper, check out the official Angular docs
Also note that lazy loading complicates this basic rule as well, so again, check the docs.
EDIT:
So for your specific situation,
#Component({
providers: [SharedDataService] <--- remove this line from both of your components, and add that line to your NgModule configuration instead
})
Add it in #NgModule.providers array of your AppModule:
if you add it in #Component.providers array then you are limiting the scope of SharedDataService instance to that component and its children.
in other words each component has its own injector which means that headerComponentwill make its own instance of SharedDataServiceand loginComponent will make its own instance.
My case is that I forget to configure my imports to add HttpClientModule in #NgModules, it works.
I wonder what is the right way to use http service. For example all my requests to server starts with /Api.
Should I write this.http.get('/Api/SomeRoute/:id') every time, or more elegant way exists to omit Api?
Or should I create some other managerService which will use http?
Something like an endpoint url is probably a good example for a general configuration file.
In my angular 2 apps i use the dependency injection for it. In this case i have something like an app.config.ts using OpaqueToken to make it injectable:
import { OpaqueToken } from '#angular/core';
export interface IAppConfig {
appTitle: string;
endpointUrl: string;
};
export const CONFIG: IAppConfig = {
appTitle: 'MyApp',
endpointUrl: 'http://api.myrealservice.com/'
};
export const LOCALCONFIG: IAppConfig = {
appTitle: 'MyApp (LOCAL)',
endpointUrl: 'http://localhost:8080/api/'
};
export let APP_CONFIG = new OpaqueToken('app.config');
This way you can have several configs (for example for local development or production etc..) and define this as a provider in your module like this:
providers: [
...,
{
provide: APP_CONFIG,
useValue: CONFIG
},
...
]
Then i just inject this config wherever i may need it, for example in a backend service to use the endpointUrl:
...
constructor(#Inject(APP_CONFIG) private _config:Config) {
console.log(this._config.endpointUrl);
}
In your module you can just change the kind of Config you want to provide (in this example CONFIG or LOCALCONFIG for example) and don't have to change it anywhere else anymore.
Hope this helps.
After your edit, you added a second question Or should I create some other managerService which will use http?:
Short answer: yes. You should seperate logic of your components, services, etc. as much as possible.
Let's say you have a an API which serves information about cats and dogs. What you want to have in your frontend would probably be a CatService, a DogService and a BackendService or ApiService, whatever you want to call it.
The CatService and DogService are the ones your view components will interact with, for example they will have methods like getCatById() or something like that. These services will most likely interact with anohter service, the BackendService which will inject Http and use general methods like post(), get() and so on.
So your BackendService is the one who has to know the specific urls, handle the responses or errors and report back to the calling services with the results, while the other ones will just be used as a pipeline and handle specific business logic.
You are right, it is better to put repeating values in a variable. You can put the base URL in a const:
const apiUrl = '/Api/';
and then if there is a change to the URL, you only change it in one place, and also your code looks cleaner. The usage is like this:
this.http.get(apiUrl + 'SomeRoute/:id')
Of course you can also create a function that does that for you, but that may be too much abstraction.