I am using the @ViewChild
decorator to create new instances of an already declared component. The process is taking place dynamically by using a dialog window for data input on form submit. The components are being created successfully, but each time the data from the latest component (all data is basically supposed to come under the same model, but from different API calls) overwrites the displayed data of all previous instances. As a result, all components display data from the exact same source. I am trying to find a way to prevent this from happening.
The container of those components: container.component.ts
@Component({
selector: 'container-component',
templateUrl: 'container.component.html',
styleUrls: ['container.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContainerComponent implements OnInit, AfterViewInit {
addressData: AddressAddition;
loginData: any;
loginAddress: string;
index: number;
@ViewChild("devicesStatusTemplate", { static: true, read: ViewContainerRef })
devicesStatusComponent: ViewContainerRef;
componentRef: ComponentRef<any>;
constructor(
public dialog: MatDialog,
private authService: AuthService,
private store: Store<AppState>
) { }
async createDevicesStatus() {
const devicesStatusComponentRef = this.devicesStatusComponent;
const { DevicesStatusComponent } = await import('./devices-status/devices-status.component');
this.componentRef = devicesStatusComponentRef.createComponent(DevicesStatusComponent);
this.componentRef.changeDetectorRef.detectChanges();
DevicesStatusComponent.counter ;
}
openDialog(): void {
const dialogRef = this.dialog.open(AddressAdditionDialog, {
width: '250px',
data: AddressAddition
});
dialogRef.afterClosed().subscribe((res: AddressAddition) => {
this.loginData = { userName: res?.username, password: res?.password };
this.loginAddress = res?.address;
if (!res) {
return;
}
this.authService.login(this.loginAddress, this.loginData).subscribe(() => {
this.createDevicesStatus();
});
this.store.dispatch(authLogin());
});
}
}
The container's template: container.component.html
<div >
<ng-template #devicesStatusTemplate></ng-template>
</div>
The reproductible component: devices-status.component.ts
@Component({
selector: 'devices-status-component',
templateUrl: 'devices-status.component.html',
styleUrls: ['devices-status.component.scss'],
providers: [DevicesStatusService],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DevicesStatusComponent implements OnInit {
allStatusProps: number[] = [];
allStatusPropsSub: Subscription;
static counter: number = 0;
index: number;
constructor(
private ref: ChangeDetectorRef,
private devicesStatusService: DevicesStatusService
) { }
ngOnInit() {
this.getAllStatusPropsSubscribe();
}
getAllStatusPropsSubscribe() {
this.index = DevicesStatusComponent.counter;
this.allStatusPropsSub = timer(0, 5000)
.pipe(
exhaustMap((_) => this.devicesStatusService.getAllStatusProps())
)
.subscribe((response: number[]| any) => {
this.allStatusProps = response;
this.ref.detectChanges();
});
}
}
The component's template: devices-status.component.html
<div id="{{ index }}" >
<div >
<div >
<gateways-status-count-component
[allStatusProps]="allStatusProps"
></gateways-status-count-component>
<lum-status-count-component
[allStatusProps]="allStatusProps"
></lum-status-count-component>
<extenders-status-count-component
[allStatusProps]="allStatusProps"
></extenders-status-count-component>
<wlessIO-status-count-component
[allStatusProps]="allStatusProps"
></wlessIO-status-count-component>
</div>
</div>
</div>
The service injecting the API call to the component: devices-status.service.ts
@Injectable({
providedIn: 'any'
})
export class DevicesStatusService {
index: number = 0;
constructor(
private apiHttpService: ApiHttpService,
private apiEndpointsService: ApiEndpointsService,
private authService: AuthService,
private constants: Constants
) { }
getAllStatusProps(): Observable<number[] | any> {
this.index = DevicesStatusComponent.counter - 1;
this.constants.API_ENDPOINT = this.constants.ENDPOINTS[this.index];
return this.apiHttpService.get(<any>this.apiEndpointsService.getAllStatusProps(), this.authService.httpOptions(this.index));
}
}
The idea was to grab the data input from the dialog window and save them in arrays. Namely, the authentication token and the API address. Then, every time the user submits a new dialog window form, store the address, internally process a login procedure to grab the token and store it along, somewhere. With the help of a counter that keeps track of the number of created components, starting from zero, I try to access the arrays with the stored data.
Upon the creation of the first instance of the DevicesStatusComponent
, it is populated with the correct data. Upon the creation of another, the latter's data from the different API address overwrites the former's data and both components display the same thing.
I could pack all code together, but decided to keep it cleaner by creating the service to handle the API calls. I know there is some way for each component to retain its own scope and data values, but it temporarily eludes me.
I have tried solving this, without the presence of the service, without a provider for the service, using switch cases to determine which component gets which data. I do not like the idea of having to create a service for every instance of the component, if this is even possible. Any ideas are most welcome.
CodePudding user response:
The problem with your current code lies in the service. It always grabs the static index from the component class and thus returns the same API for every component. You need to pass it the index:
getAllStatusProps(index: number): Observable<number[] | any> {
this.constants.API_ENDPOINT = this.constants.ENDPOINTS[index];
return this.apiHttpService.get(<any>this.apiEndpointsService.getAllStatusProps(), this.authService.httpOptions(index));
}
This should fix your current problem, but I don't really understand why you are doing this so complicated. I would just have an array with the needed values and go through it with *ngFor
. Something like this:
@Component({
selector: 'container-component',
templateUrl: 'container.component.html',
styleUrls: ['container.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContainerComponent implements OnInit, AfterViewInit {
addressData: AddressAddition;
loginData: any;
loginAddress: string;
index: number;
apiData = [];
constructor(
public dialog: MatDialog,
private authService: AuthService,
private store: Store<AppState>
) { }
openDialog(): void {
const dialogRef = this.dialog.open(AddressAdditionDialog, {
width: '250px',
data: AddressAddition
});
dialogRef.afterClosed().subscribe((res: AddressAddition) => {
this.loginData = { userName: res?.username, password: res?.password };
this.loginAddress = res?.address;
if (!res) {
return;
}
this.authService.login(this.loginAddress, this.loginData).subscribe(() => {
this.apiData.push({
address: this.loginAddress,
// add whatever data you need
})
});
this.store.dispatch(authLogin());
});
}
}
<div >
<ng-container *ngFor="let api of apiData">
<devices-status-component
[api]="api"
></devices-status-component>
</ng-container>
</div>
And then add an input in the DeviceStatusComponent
and pass all the data through.