I'm experiencing strange behavior of services injected to a component that is loaded dynamically. Consider the following service
#Injectable({
providedIn: 'root'
})
export class SomeService {
private random = Math.random() * 100;
constructor() {
console.log('random', this.random);
}
}
The service is added as a dependency to two components. First component is a part of lazy-loaded module. While second one is loaded dynamically. The following service loads modules with dynamic component
export const COMPONENT_LIST = new InjectionToken<any>('COMPONENT_LIST');
export const COMPONENT_TYPE = new InjectionToken<any>('COMPONENT_TYPE');
#Injectable({
providedIn: 'root'
})
export class LoaderService {
constructor(
private injector: Injector,
private compiler: Compiler,
) { }
getFactory<T>(componentId: string): Observable<ComponentFactory<T>> {
// COMPONENT_LIST is passed through forRoot() from the module that declares first component
const componentList = this.injector.get(COMPONENT_LIST);
const m = componentList.find(m => m.componentId === componentId);
const promise: Promise<ComponentFactory<T>> = (!m) ? null :
m.loadChildren
.then((mod: any) => {
return this.compiler.compileModuleAsync(mod);
})
.then((mf: NgModuleFactory<any>) => {
const mr: NgModuleRef<any> = mf.create(this.injector);
const type: Type<T> = mr.injector.get<Type<T>>(COMPONENT_TYPE); // DYNAMIC_COMPONENT is provided in loaded module
return mr.componentFactoryResolver.resolveComponentFactory<T>(dynamicComponentType);
});
return from(promise);
}
}
In the same module I declare the following component (to place component that is loaded dynamically) and directive
#Component({
selector: 'dynamic-wrapper',
template: `<ng-container dynamicItem></ng-container>`
})
export class DynamicWrapperComponent implements AfterViewInit, OnDestroy {
#Input() itemId: string;
#Input() inputParameters: any;
#ViewChild(DynamicItemDirective)
private dynamicItem: DynamicItemDirective;
private unsubscribe$: Subject<void> = new Subject();
constructor(
private loaderService: LoaderService
) { }
ngAfterViewInit(): void {
this.loaderService.getComponentFactory(this.itemId).subscribe((cf: ComponentFactory<any>) => {
this.dynamicItem.addComponent(cf, this.inputParameters);
});
}
}
...
#Directive({
selector: '[dynamicItem]'
})
export class DynamicItemDirective {
constructor(protected viewContainerRef: ViewContainerRef) { }
public addComponent(cf: ComponentFactory<any>, inputs: any): void {
this.viewContainerRef.clear();
const componentRef: ComponentRef<any> = this.viewContainerRef.createComponent(cf);
Object.assign(componentRef.instance, inputs);
// if I do not call detectChanges, ngOnInit in loaded component will not fire up
componentRef.changeDetectorRef.detectChanges();
}
}
SomeService is defined in a separate module that is imported in both lazy-loaded module with first component and dynamically loaded module.
After both components are initialed I see output of console.log('random', this.random) with two different numbers in console, despite providedIn: 'root' in decorator. What is the reason for such a strange behavior?
Lazy loaded modules won't react to providedIn: 'root' as one might expect.
This option will push services to the AppModule(root module) only if the module that they are imported in is not lazy loaded.
Why do you see different random numbers?
Because the module that SomeService is defined in is initiated twice! (feel free to put console.log in the constructor of this module) - therefore the service is initiated twice (I encountered this issue myself in my project lazy load platform).
Why the lazy module that contain SomeService is initiated twice?
loadChildren or moduleFactory.create(this.injector) don't hold a cache of lazy loaded modules. They initiate a module and attach its injector as a leaf to the input injector.
If the module that contains SomeService was created from moduleFactory.create - you can add a cache layer to return the cached initiated lazy loaded module.
For example:
private lazyLoadedModulesCache: Map<Type<any>, NgModuleRef<any>>;
Related
validationAR: any = "/^([0-9\s#,.=%$#&_\u0600-\u06FF])+$/";
Above is the variable I used to store the regex.
Say you have this common variable which you use it across the application, you create an appService in app.service.ts file and store this variable in it.
In the component where you want this variable, inject with appService using Dependency Injection there by having a single instance of appService.
In component.ts
//.. some imports ..
constructor(private appService: AppService) {}
someFormControl: new FormControl(
{ value: null, disabled: false },
{
validators: [
Validators.pattern(this.appService.myPattern)
]
}
)
This way you can use that variable in service file.
Suppose , you stored your regex in a service variable
*//.. some imports ..
#Injectable({
providedIn: 'root'
})
export class DataService{
validationRegEx = "/^([0-9\s#,.=%$#&_\u0600-\u06FF])+$/";
constructor() {}
}*
Now, the component in which you want to use this regex just import the service in the component & you can access all the required service variables.
*
//.. some imports ..
#Component({
selector: 'app-user-data',
templateUrl: './user-data.component.html',
styleUrls: ['./user-data.component.scss']
})
export class UserDataComponent implements OnInit {
constructor(private fb:FormBuilder,private dataService:DataService)
ngOnInit(): void {
this.userDetailsForm = this.fB.group({
userMobile: ['',[Validators.pattern(this.dataService.validationRegEx )]]
})
}
}
I'm trying to test an Angular Resolver which accesses children routes param.
My guard works fine but I cannot create an unit test easily because I cannot create an ActivatedRouteSnapshot with children routes (read only property).
My resolver
#Injectable({
providedIn: 'root'
})
export class MyResolverGuard implements Resolve<string> {
constructor() {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): string {
return route.firstChild.paramMap.get('my-param');
}
}
My test :
it('should resolve chilren route params', () => {
guard = TestBed.get(MyResolverGuard);
const route = new ActivatedRouteSnapshot();
// Cannot assign children because it's a read only property
route.children = [...];
const myResolverParams = guard.resolve(route, null);
});
Is there other any other ways than using mock ActivatedRouteSnapshot ?
Does my approach to test guard is good ?
Thanks for sharing your strategy.
I created a component that holds a loader:
#Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.scss'],
providers: [LoaderService]
})
export class LoaderComponent implements OnInit {
showloader: boolean = false;
constructor (private loader_service:LoaderService) {}
ngOnInit () {
this.loader_service.showLoaderEventChange.subscribe(state => {
console.log(state); // Nothing prints !!
this.showloader = state;
})
}
}
and I have a service that I call from another component, and the call does work:
public grabJsonData (enviroment: string, language: string) {
this.loader_service.showLoader(true);
}
It uses the LoaderService:
#Injectable({
providedIn: 'root'
})
export class LoaderService {
showLoaderEventChange = new Subject<any>();
constructor() { }
showLoader(state){
this.showLoaderEventChange.next(state);
}
}
The loader component uses the LoaderService, listens for showLoaderEventChange to pass next().
But when I do this.showLoaderEventChange.next(state) it does not catch anything in the subscribe function.
Did I miss something?
Its because you are not using same instance of service for your components since you are injecting it in root as well as in Loader Component.
Try removing Providers : [LoaderService] from your Loadercomponent.
I'm not sure about Angular 6 since I haven't tested it out yet but in Angular 5 you have to create an observable to observe it from your Subject as such :
showLoaderEventChangeEmitter = new Subject<any>();
showLoaderEventChangeEvent$ = this.showLoaderEventChangeEmitter.asObservable();
Then use showLoaderEventChangeEmitter.next(); to trigger events and listen to them with showLoaderEventChangeEvent$.subscribe()
I'm trying to create a service to share the data between two components. I injected the service into root module to make it accessible throughout the application by doing DI into the root module provider. My code looks roughly like this.
Service
#Injectable(){
export class ForumService{
forum: any;
setForum(object){
this.forum = object;
}
getForum(){
return this.forum;
}
}
Root Module
.......
import { ForumService } from 'forumservice';
.......
#NgModule({
declarations: [.....],
imports: [.....],
providers: [....., ForumService],
bootstrap: [AppComponent]
})
export class AppModule{}
Component One
//A bunch of import statements
import { ForumService } from 'forumservice'; //Without this Angular throws a compilation error
#Component({
selector: 'app-general-discussion',
templateUrl: './general-discussion.component.html',
styleUrls: ['./general-discussion.component.css'],
providers: [GeneralDiscussionService] //Not injecting ForumService again
})
export class GeneralDiscussionComponent implements OnInit{
constructor(private forumService: ForumService){}
ngOnInit(){
helperFunction();
}
helperFunction(){
//Get data from backend and set it to the ForumService
this.forumService.forum = data;
console.log(this.forumService.forum); //prints the data, not undefined
}
}
Component Two
//A bunch of import statements
import { ForumService } from 'forumservice'; //Without this Angular throws a compilation error
#Component({
selector: 'app-forum',
templateUrl: './forum.component.html',
styleUrls: ['./forum.component.css'],
providers: []
})
export class ForumComponent implements OnInit {
forumData: any;
constructor(private forumService: ForumService){}
ngOnInit(){
this.forumData = this.forumService.forum; // returns undefined
}
}
Once I navigate from Component One to Component Two I'm expecting "This is a string". However I get undefined. Is it because of the import statements in the component? If I remove that I see a compilation error saying that ForumService is not found.
Instead of using getter and setter, use the object (not primitibe such as string) directly In your components.
Your service
#Injectable(){
export class ForumService{
forum:any = {name:string};
}
Component one
export class GeneralDiscussionComponent implements OnInit{
constructor(private forumService: ForumService){}
ngOnInit(){
this.forumService.forum.name="This is a string";
}
}
Component two
export class ForumComponent implements OnInit {
// forumTitle: string; // do not need this anymore
forum:any; // use the forum.name property in your html
constructor(private forumService: ForumService){}
ngOnInit(){
this.forum = this.forumService.forum; // use the
}
}
I know encapsulating is preferable, and with your current code you are probably encountering some timing problems. But when working with shared data in a service you can two-way bind the variable like above, and your components will be in sync.
EDIT:
Also an important notice, the variable you want to sync between components needs to be an object. instead of forumTitle:string, make it forumTitle:any = {subject:string} or something similar.
Otherwise you need to make your components as listeners for data when data changes in your service.
I'd use BehaviorSubject in this case, should be something like that:
#Injectable(){
export class ForumService{
private _forum: BehaviorSubject<any> = new BehaviorSubject<any>(null);
public forum: Observable<any> = this._forum.asObservable();
setForum(object){
this._forum.next(object);
}
}
Then just bind it in template with async pipe: {{forumService.forum|async}} or subscribe to it.
I am trying to pass user info object to all low level component,
the issue is what is the best way to pass it to lover component even if they are grandchildren?
If the #input will work or have anther way to pass it?
my code for root component is:
constructor(private _state: GlobalState,
private _imageLoader: BaImageLoaderService,
private _spinner: BaThemeSpinner, public af: AngularFire) {
this._loadImages();
this._state.subscribe('menu.isCollapsed', (isCollapsed) => {
this.isMenuCollapsed = isCollapsed;
});
// this.af.auth.subscribe(
// user => this._changeState(user),
this.af.auth.subscribe( user => this._changeState(user));
}
Have you considered creating a service class? They're singletons, so the same instance of that class gets injected into each and every component that asks for it.
https://angular.io/docs/ts/latest/guide/dependency-injection.html
A simple one would look like this
import { Injectable } from '#angular/core';
#Injectable()
export class DummyService {
public userInfo: UserInfo = {
//User stuff goes here
};
}
And you would add it to a component like this.
import {DummyService} from 'dummy.service';
#Component({
selector: 'my-component',
templateUrl: 'my.component.html'
})
export class MyComponent{
constructor(private myDummyService: DummyService){}
}
At runtime, this would inject the same instance of the class into every component you inject it into. So it's a super handy way of syncronizing data across multiple components.