8

I'm building a vertically scrolling calendar. I'm getting the initial days to load, but when new days are added to the list, they aren't being rendered.

<cdk-virtual-scroll-viewport
  class="demo-viewport"
  [itemSize]="100"
  (onContentScrolled)="handleScrollChange($event)"
>
  <calendar-day
    *cdkVirtualFor="let day of days; trackBy: trackByFn"
    [day]="day"
  ></calendar-day>
</cdk-virtual-scroll-viewport>
<button (click)="goToToday()">go</button>

I have a service with a BehaviorSubject updating the days. I know the list of days is being updated, but the change doesn't seem to be detected.

  ngOnInit() {
    this._daysService.days$.subscribe(days => {
      this.days = days;
    })
    this.watchScroll();
    this.handleScrollingUp();
    this.handleScrollingDown();
  }

For more info, the StackBlitz repo is public https://stackblitz.com/edit/material-infinite-calendar

Will Luce
  • 1,497
  • 1
  • 16
  • 30
  • Try to use `async` pipe to subscribe to an observable. Maybe this would help. Also, I'm not sure about rightfulness of making `this.days = days` since it would make a reference instead of copying an array. I've been having some problems due to JavaScript nature and was forced to use `lodash` to make deep copies of arrays and objects – Sergey Apr 05 '19 at 16:34

4 Answers4

5

The *cdkVirtualFor would only be updated if you update it immutably i.e. you cannot update the array after it is initialized. We use the spread operator to get what you are looking for.

Check this very simple stackblitz... here i have used 2 methods which you can try and see:

  • addCountryOld method mutates the array by pushing an object to our array and hence the rendered view is not updated.
  • addCountryNew method uses immutability through the spread operator which results in the rendered view getting updated.

This is the code for addCountryNew:

addCountryNew(){
    let  newObj = {'name':'stack overflow country', "code":'SO'};
    this.myList = [...this.myList, newObj];
  }
Akber Iqbal
  • 12,257
  • 11
  • 34
  • 52
  • I understand this, but I’m not sure why it is considered mutation when I’m setting days = the new value that comes through from the BehaviorSubject. I’m not mutating, I’m replacing this.days every time the BehaviorSubject changes. – Will Luce Mar 27 '19 at 13:35
  • From what I understand, once an array is initialized, it can't change, when you assign a new value, you change it and hence mutation occurs... – Akber Iqbal Mar 27 '19 at 13:46
  • Yep. That’s what mutation is, but I’m not doing that. I’m reassigning the array to the value emitted by the BehaviorSubject. If I’m wrong about that, I’d sure like to know why. – Will Luce Mar 27 '19 at 13:56
  • Yes you're reassigning, but you're assigning the same reference again, so it's not actually changing anything. – Ingo Bürk Apr 06 '19 at 07:00
5

I figured this out.

Originally, I was adding new days by grabbing the current value like this

let items = this.items$.value;
items.push(newItem);
this.items$.next(items)

Apparently, this is actually a mutation of the value of the BehaviorSubject's value, therefore not creating a new array to return and not triggering change detection.

I changed it to

let items = [...this.items$.value];
items.push(newItem);
this.items$.next(items)

and all is good.

So, although the answers here are correct in that I was mutating the original array, the information I needed was calling next() with a mutated version BehaviorSubject's current value does not emit a new array. An emit event does not guarantee immutability.

Will Luce
  • 1,497
  • 1
  • 16
  • 30
2

Instead of

this.days = days;

do

this.days = [...days];

Works. https://stackblitz.com/edit/material-infinite-calendar-amcapx?file=src/app/calendar/calendar.component.ts

(But does it mean that BehaviorSubject keeps reusing and mutating the same object? Weird!)

mbojko
  • 9,720
  • 1
  • 10
  • 20
0

It can be done like this:

You can initialise another variable as observale of your behaviorsubject "days$".

in calendar-days.service.ts

  public days$: BehaviorSubject<Date[]>;
  public dayObs$: Observable<Date[]>

  constructor() {
    this.days$ = new BehaviorSubject<Date[]>(this._initialDays);
    this.dayObs$ = this.days$.asObservable();
  }

And then subscribe that observable in calender.component.ts inside ngOnInit like this:

this._daysService.dayObs$.subscribe(days => {
  this.days = days;
})
Sumit Vekariya
  • 462
  • 2
  • 9
  • There shouldn’t be a need for multiple variables. A BehviorSubject is an observable with some special qualities (mainly that it emits its current value on subscription). – Will Luce Mar 27 '19 at 13:47
  • https://stackoverflow.com/questions/39494058/behaviorsubject-vs-observable – Will Luce Mar 27 '19 at 13:53