1

I encountered a scenario where i need to do some processing of data on ngOnchanges(as the data is coming through @Input data bound property from parent component). During this transformation i need to use a property initialized in ngOnInit. I tried to use it directly but the value is coming as undefined. Can someone let me know how can i achieve this. Below is an example code for my scenario.

ParentComp HTML
<myParent [data]="list$ | async"></myParent>

ChildComp TS
export class {
  @Input() data : any ; //data from parent
  breakPoint$: Observable<number>

  constructor(){}
  ngOnInit(){
   this.breakPoint$ = fromEvent(this.window, 'resize').pipe(
      map(() => this.numOfCols())
    );
  }

ngOnChanges(changes: SimpleChanges) {
    if (changes['data']) {
       // need to do some data processing here based on numOfColumns value, so trying to access
       this.breakPoint$.pipe(

       ) // Here i get cannot read property pipe of undefined error
      );

 numOfCols(): number {
    return this.breakpointObserver.isMatched(`(max-width: 1008px)`) ? 4 : 5;
  }
Deadpool_er
  • 215
  • 4
  • 20

4 Answers4

2

You can add another input propery and move numOfCols$ into the parent component.

ParentComp HTML

<myParent [data]="list$ | async"
          [numOfCols]="numOfCols$ | async>
</myParent>

ParentCom TS

numOfCols$: Observable<number>;

constructor(private breakpointObserver: BreakpointObserver) {
   this.numOfCols$ = breakpointObserver.observe('(max-width: 1008px)').pipe(
      map(({matches}) => matches ? 4 : 5)
   );
}

ChildComp TS

@Input() data: any;
@Input() numOfCols: number;

ngOnChanges(changes: SimpleChanges) {
    if (changes['data']) {
        // You can use this.numOfCols here
    }
}
Nifiro
  • 320
  • 2
  • 7
  • Indeed, using a "breakpoint service" would be a lot nicer ! Good idea ! – Random May 09 '20 at 20:04
  • @Nifiro and Random , thank you. I was thinking about doing this, but my bad i missed to check the order of lifecycle hooks triggered and came up with question. – Deadpool_er May 09 '20 at 20:14
1

I would recommend the usage of withLatestFrom rxjs operator here. I will create an Observable for numOfColumns, whenever this gets updated the final subscription would trigger. BUT: it would not trigger until, you have the data from ngOnInit() (second Observable), and will only trigger if numOfColumns is updated after that, i.e. Subsequent changes of BreakPoint won't trigger the subscription, If you want it to be triggered on both then you can use zip instead.

I am doing this in a setter, you can use ngOnChanges as well

_numOfColumns;
numOfColumnsObs$ = new BehaviorSubject(false);

set numOfColumns(val) {
    this._numOfColumns = val;
    this.numOfColumnsObs$.next(val);
}

get numOfColumns() {
    return this._numOfColumns
}

ngOnInit() {
    this.breakPoint$ = fromEvent(this.window, 'resize').pipe(
        map(() => this.numOfCols())
        );

        // new code here.
        this.numOfColumnsObs$.pipe(withLatestFrom(this.breakPoint$))
        .pipe(filter(([columns]) => columns !== false))
        .subscribe(([columns, breakPoint]) => {
            // do the changes you wanted in `ngOnChanges`;
        })
}

You would need a BehaviorSubject here, because the observable would be emitted before subscription. Since there is a BehaviorSubject, there is a hack, I have initialized it with false. Assuming that numOfColumns would never come as false. If it would, you can initialize it with any value which is not possible to come and filter it later by piping it.

Ashish Ranjan
  • 12,760
  • 5
  • 27
  • 51
0

ngOnChanges is first triggered before ngOnInit. Because Inputs are filled before ngOnInit.
If you need values comming from ngOnInit, you may use a boolean "isInitDone" inside the ngOnChanges. And don't forget to set this boolean to true inside your ngOnInit.

But then, you should also trigger what is done in the ngOnChanges method. so maybe putting it inside amethod, where both ngOnChanges and ngOnInit would call...

An other solution would be to move ngOnInit code to ngOnChanges, inside an if (this.breakpoint$)

Random
  • 3,158
  • 1
  • 15
  • 25
0

Angular's component lifecycle hooks are triggered in the following order:

  1. ngOnChanges
  2. ngOnInit
  3. ngDoCheck
  4. ngAfterContentInit
  5. ngAfterContentChecked
  6. ngAfterViewInit
  7. ngAfterViewChecked
  8. ngOnDestroy

As you can see, you're trying to access a property that's not defined the in the first ngOnChanges. You could simple add a check in your if statement like this:

ngOnChanges(changes: SimpleChanges) {
    if (changes['data'] && !!this.breakPoint$) {
....

or you could even define breakPoint$ outside of ngOnInit, like this:

   @Input() data : any ; //data from parent
   breakPoint$: Observable<number> = fromEvent(this.window, 'resize').pipe(
      map(() => this.numOfCols())
    );
avramz
  • 441
  • 2
  • 8