We are in the process of re-writing our components to be stand-alone and re-useable. Please note we are using Angular 16, and can not upgrade to a newer version yet.
I’m trying to create a base class to handle error messages, loading, and other common functionality. In this base class, I have a behavior subject that is not being updated, other than in the ngOnInit. What I’m trying to do is to pass a boolean to show that the service call was successful, and show a false in the base.component.html:6
I’ve tried various things, with no success. Maybe I’m missing something obvious with how base classes work in typescript versus what I’m used to in C#.
Thanks in advance, here’s the stackblitz: https://stackblitz.com/edit/stackblitz-starters-r6ncxp?file=README.md
app.component.html
<div class="content" role="main">
This is from the app.component.html
<app-home></app-home>
</div>
base.component.ts
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit, TemplateRef } from "@angular/core";
import { BehaviorSubject } from "rxjs";
@Component({
selector: 'app-base',
templateUrl: './base.component.html',
styleUrls: ['./base.component.scss'],
standalone: true,
imports: [CommonModule],
})
export class BaseComponent implements OnInit {
@Input() templateRef: TemplateRef<any> | null = null; // reference to the child template to be projected
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
constructor() { }
ngOnInit() {
// if I call this from the home component, then isLoading$ updates
}
handleEvent(foo: any) {
console.log(`from handleTestLoadingEvent: ${foo}.`);
this.isLoading$.next(foo);
}
inheritEvent(foo: boolean) {
console.log(`base component inheritEvent value: ${foo}`);
this.isLoading$.next(foo);
}
}
base.component.html
<div>
<p>
This is the base.component.html
</p>
<p>
isLoading$: {{ isLoading$ | async }}
</p>
<ng-container *ngTemplateOutlet='templateRef' (isLoading)="handleEvent($event)"></ng-container>
</div>
home.component.ts
import { Component, EventEmitter, OnInit, Output, TemplateRef } from '@angular/core';
import { BaseComponent } from '../base.component';
import { FooService } from '../service/foo.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: true,
imports: [BaseComponent, CommonModule],
providers: [FooService]
})
export class HomeComponent extends BaseComponent implements OnInit {
@Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
constructor(private fooService: FooService) {
super();
}
override ngOnInit(): void {
// Emit this to the base component
this.fooService.getFoo(1234).subscribe(f => {
console.log(`foo Service has been called successfully: ${f}`);
this.isLoading.emit(f);
this.inheritEvent(f);
});
}
}
home.component.ts
<app-base [templateRef]='homeTemplate'>
<ng-template #homeTemplate>
This is from home.component.html
</ng-template>
</app-base>
5
i Would recommend you to change your design, but in the meanwhile lets work with this.
There are some changes you need to make to fix this:
1.- The ng-container does not support Output so the BaseComponent nevers get called with the (isLoading).
2.- The BaseComponent is created twice, one from the extends from HomeComponent an another from the declaration tag so you are dealing with two instances.
3.- There is a cyclic dependency between HomeComponent and BaseComponent.
Now lets fix those issued to make it work.
In the HomeComponent use the @ViewChild to access the instance of the base-component, and delete the extends, then replace the access to BaseComponent with the instance.
@ViewChild(BaseComponent, { read: BaseComponent, static: true })
base!: BaseComponent;
Create a new class (An standalone directive) i called CommonComponent (rename it to whatever you want) and make HomeComponent to extends it, with this will be able to inject any component in the BaseComponent.
@Directive({ standalone: true })
export class CommonComponent {
@Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
}
In the HomeComponent providers, provide the new class with the existing HomeComponent.
providers: [
FooService,
{ provide: CommonComponent, useExisting: HomeComponent },
],
And last, inject into the BaseComponent the CommonComponent (remember this can be any component that was provided) and subscribe to the events of it.
constructor(private component: CommonComponent) {
console.log('BaseComponent!');
}
ngOnInit() {
this.component.isLoading.asObservable().subscribe((e) => {
this.handleEvent(e);
});
}
Now you are able to communicate between the decoupled components.
This is the example of your project https://stackblitz.com/edit/stackblitz-starters-hxyvl4 in case i missed something.
1