How to implement and ngif with two conditions when one of them has an async pipe

Viewed 210

I have one component that can either recieve [patient] or [patientId] as @Input parameter

  • selectedPatient has a value when it comes from a search result
  • viewOrdersClientId has a value when it comes from another page

Currently I'm showing one or the other in this way (as a workaround)

<div *ngIf="selectedPatient | async as patient">
    <wss-patient-detail-component [patient]="patient">
    </wss-patient-detail-component>
</div>
<div *ngIf="viewOrdersClientId">
    <wss-patient-detail-component [patientId]="viewOrdersClientId">
    </wss-patient-detail-component>
</div>

I would like to have an *ngIf with multiple conditions and pass one of the required values, something similar to this:

<div *ngIf="viewOrdersClientId || (selectedPatient | async as patient)">
    <wss-patient-detail-component
    [patient]="patient" 
    [patientId]="viewOrdersClientId">
    </wss-patient-detail-component>
</div>

When I do that I get this error :

Property 'patient' does not exist on type 'PatientComponent'

Is there some way I can achieve having only one wss-patient-detail-component instead of my current approach ?

4 Answers

tipical you can create "onfly" an object using *ngIf with the pattien and pass as argument

<!--see that the *ngIf is always true-->
<div *ngIf="{patient:selectedPatient | async} as data">
  <!--you get the value in "data.patient-->
  <div *ngIf="data.patient || viewOrdersClientId">
    <wss-patient-detail-component [patient]="data.patient" [patientId]="viewOrdersClientId">
    </wss-patient-detail-component>
  </div>
</div>

I would suggest using an observable to determine when your data is ready to be rendered. This is assuming your viewOrdersClientId is just a component input not waiting on any data. Like this:

isReady$ = new BehaviorSubject<boolean>(false);
selectPatients: any[];


ngOnInit()
{
    ...
    const getSelectPatients = [your service to get selected patients]

    getSelectPatients.subscribe(res => 
    {
        if (viewOrdersClientId)
        {
            this.selectPatients = res;
            this.isReady$.next(true); 
        }
    });

    ...
}

Then in your HTML template display the components only when the observable is ready:

    <div *ngIf="isReady$ | async">  
        <wss-patient-detail-component
            [patient]="selectedPatients" 
            [patientId]="viewOrdersClientId">
        </wss-patient-detail-component>
    </div>

This would ensure your patient variable would not be undefined and valid when passed as a parameter into the component wss-patient-detail-component.

The appropriate way to do this is with *ngIf Else, however one can then use else ng-template based conditions, which is what you want I believe this can be expanded very easily and is the correct way to approach this problem:

   <div *ngIf="selectedPatient | async as patient; else viewOrdersClient">
  <wss-patient-detail-component [patient]="patient">
  </wss-patient-detail-component>
</div>

<ng-template #viewOrdersClient>
  <wss-patient-detail-component [patientId]="viewOrdersClientId">
  </wss-patient-detail-component>
</ng-template>

this is covered here: https://angular.io/guide/structural-directives

ORIGINAL QUESTION:

<div *ngIf="viewOrdersClientId || (selectedPatient | async as patient)">
    <wss-patient-detail-component
    [patient]="patient" 
    [patientId]="viewOrdersClientId">
    </wss-patient-detail-component>
</div>

When I do that I get this error :

Property 'patient' does not exist on type 'PatientComponent' Is there some way I can achieve having only one wss-patient-detail-component instead of my current approach ?

ANSWER: You are calling an object or string named patient - from the parent which is called PatientComponent. The component is unaware of this variable. Either you did not declare it on PatientComponent or the ngIf statement is not allowing the data to pass through if its false because it discards the component instruction - however the component is still rendered without an answer from the parent, as it was ignored.

POSSIBLE FIXES:

NG TEMPLATE WAY

ng-template for this to give an alternative template to render the answer if there is no component instance, which could be a simple open and close div with no content.

<ng-container *ngIf="viewOrdersClientId || (selectedPatient | async as patient); else falseTemplate">
        <wss-patient-detail-component
        [patient]="patient" 
        [patientId]="viewOrdersClientId">
        </wss-patient-detail-component>
    </ng-container>

     <ng-template #falseTemplate>
    <div></div>
    </ng-template>

NG CONTAINER WAY

ng container waits for the instruction before rendering the component as the html div element does not - it process the html in first order and javascript in second order.

<ng-container *ngIf="viewOrdersClientId || (selectedPatient | async as patient)">
    <wss-patient-detail-component
    [patient]="patient" 
    [patientId]="viewOrdersClientId">
    </wss-patient-detail-component>
</ng-container>

NG CLASS WAY The other way to accomplish your goal is with [ngClass] div is to just show or hide based upon your if - this is the cleanest way out of all of them and it fixes your variable issue. *ngIf statements run in order of the rendered viewport and without an alternative template you get stuck with few choices to fix the problem.

<div [ngClass]="viewOrdersClientId || (selectedPatient | async as patient) ? 'show':'hide'">
<wss-patient-detail-component
        [patient]="patient" 
        [patientId]="viewOrdersClientId">
        </wss-patient-detail-component>
  </div>

on your patient.component.scss or .css the fix is to just use simple css to hide it or show it, set the height to 0px on hide and 100% on show.

.show{
visibility: visible;
height: 100%;
}

.hide{
visibility: hidden;
height: 0px;
}

DIRECTIVE WAY

from cli ng g d patientdetail on the module where you import the wss-patient-detail-component add this after exports:[] exports:[wss-patient-detail-component] entryComponents: [wss-patient-detail-component]

    import {
  Directive, ViewContainerRef, ChangeDetectorRef,
  ComponentFactoryResolver,
  Input
} from '@angular/core';
import { from, Observable } from 'rxjs';

import { WssPatientDetailComponent } from './YOUR PATH/wsspatientdetailcomponent.component';

@Directive({
  selector: '[elPatientDetail]'
})
export class PatientDetailDirective{

@Input() patient?: any;
@Input() patientId: any;
elRef: any;
alias: string;
readonly templateMapper: any = {
    patientdetail: WssPatientDetailComponent
} 
 constructor(  private cdr: ChangeDetectorRef,
    private factory: ComponentFactoryResolver,

    public viewContainerRef: ViewContainerRef) {
      this.elRef = viewContainerRef;

     }
    async ngOnInit(): Promise<void> {
      console.log(await this.viewportLayout)
      // console.log('template directive started');
      this.buildViewPort();
    }
    async buildViewPort(): Promise<void> {
      this.elRef.clear();

      const componentFactory = this.factory.resolveComponentFactory(
        this.getComponentByAlias(this.viewportLayout as unknown as string)
       );

     /** send data through the dom to the newly created component this in theory makes the component "reactive" */
       const instance  = this.elRef.createComponent(componentFactory, 0).instance;
     // tslint:disable-next-line: max-line-length
        instance.patient = this.patient;
        instance.patientId = this.patientId
      //  console.log(newcomponent);
              this.cdr.detectChanges();

          }
        // }

  
    // tslint:disable-next-line: typedef
    private getComponentByAlias(alias: string) {
      // connects directly to the templateMapper and allows us to import the components we want at this time
      console.log('alias', alias);
      return this.templateMapper[alias];
    }
}

}

on the parent component after you import the directive to that same module. the data is passed through to the component via an instance which allows it to stay as an async.

on the parent component

<div *ngIf="viewOrdersClientId || 
    (selectedPatient | async as patient)" 
    [patient]="patient" 
    [patientId]="viewOrdersClientId"
    elPatientDetail></div>
Related