1

I have an Angular 10 component that displays a list of film titles, the list has a vertical scroll bar. One of these will be the currently selected film and be displayed highlighted in bold etc. The film list and the selected film are updated via observables as so:

<div 
    class="overflow-hidden text-nowrap"
    [ngClass]="{'shadow-sm font-weight-bolder': isSelected(film.id)}"
    *ngFor="let film of films" 
    (click)="onClick(film)">
    {{film.title}}
</div>

and

public ngOnInit(): void {

    this.subs.push(this.filmService.observe().subscribe(films => {
        this.films = films;
        this.changeRef.detectChanges();
    }));

    this.subs.push(this.activeFilmService.observe().subscribe(film => {
        this.activeFilm = film;
        this.changeRef.detectChanges();
    }));
}

public isSelected(id: number): boolean {
    return this.activeFilm && id === this.activeFilm.id;
}

Unfortunately the active film is often not visible without the user manually scrolling down to find it. I want to bring the active film into view every time either the list or the active film changes. I know how I might do this using @ViewChild to find the div and call scrollIntoView() to make it visible. My question is where should I add this code?

Presumably I can't just add the scrollIntoView code to the end of the subscription. I assume I have to wait for Angular to re-render all the elements before trying to change the scroll position. How would I safely do this?

Paul D
  • 601
  • 7
  • 13
  • Try the method suggested in [this answer](https://stackoverflow.com/a/53124292/1009922) (`ViewChildren` + `QueryList.changes` event). – ConnorsFan Aug 03 '20 at 14:54
  • Thank you ConnorsFan, that is exactly what I am looking for. PS, do you know how I accept this suggestion as an answer? – Paul D Aug 04 '20 at 06:39
  • We can mark this question as a duplicate. Feel free to upvote [the original answer](https://stackoverflow.com/a/53124292/1009922) if it helped you. :-) – ConnorsFan Aug 04 '20 at 14:15

2 Answers2

1

You can do following.

1). You should never use function (isSelected) in template (to improve performance)

<div 

    #mySelectedFilm                                                         // added template ref variable

    class="overflow-hidden text-nowrap"

    [ngClass]="{'shadow-sm font-weight-bolder': (film.id === activeFilm.id )}"   // using activeFilm.id

    *ngFor="let film of films; let i = index"                                    // let i=index added 

    (click)="onClick(film)"

     id ="{{i}}">                                                                // line added

    {{film.title}}

</div>

2). make sure to define fixed size to your scrollable viewport/window

3).

 @ViewChildren("mySelectedFilm") ele: QueryList<ElementRef>;


 bringToView(){ 
      for(const ele of this.ele){
         if(ele.nativeElement.id == this.activeFilm.id){
         ele.nativeElement.scrollIntoView(false);
      }
 }

call this function when activeFilm.id is set

this.subs.push(this.activeFilmService.observe().subscribe(film => {
        this.activeFilm = film;
        
        this.bringToView(); // you can try putting it after below line also if doesn't work

        this.changeRef.detectChanges();

    }));
}

DEMO to help you with what I've explained so far

micronyks
  • 54,797
  • 15
  • 112
  • 146
  • it's great but still, i think this can be done with ViewChildren? – critrange Aug 03 '20 at 15:25
  • 1
    @yash. yes it can be done. we just need to query the right element that's it. I'll update it if I get some free time to re-look my demo. – micronyks Aug 03 '20 at 15:26
  • nice. just thought that @ViewChildren is better as it's the angular way. – critrange Aug 03 '20 at 15:27
  • 1
    @yash Updated answer with angular approach... – micronyks Aug 03 '20 at 15:50
  • Thank you for the example micronyks, appreciated, but your demo uses a static film list. My question is not about how to scroll an element to be visible, but what happens when the list is is subscribed from an observeable? When some new data arrives the element that needs to be scrolled to may not exist yet. How to know when angular has torn down the old dom elements and created the new ones? – Paul D Aug 04 '20 at 06:34
  • If you use `trackBy` with *ngFor, it will never refresh the entire DOM again & again, rather it will just update the new DOM elements which will come through observable. Yes, demo uses the static data but if you replace it with observable, it should still work. `When new data arrives, the element that needs to be scrolled which may not exist` => if element doesn't exist, it is obvious you can't scroll to it. You should have some mechanism to scroll to it when element is available. – micronyks Aug 04 '20 at 06:40
  • 1
    Thanks again micronyks. ConnorsFan explains how to do this for dynamic data: I can subscribe to QueryList.changes on the @ViewChildren which will be fired when the new elements are created. The scroll code should go there. – Paul D Aug 04 '20 at 06:48
0

ConnorsFan explains how to do this in this answer here

The gist is that you can reference the list of elements using @ViewChildren and then in afterViewInit subscribe to the QueryList.changes observable.

This will emit after the list has been re-drawn and any new elements are ready to be scrolled into view.

Paul D
  • 601
  • 7
  • 13