Tonya
Tonya

Reputation: 278

Angular 2: Listen to a scroll event of parent element inside Directive

I have a directive for validation [appInvalidField], it's like a custom tooltip. Since I need to use it inside dialogs and show it above everything else, I append that to body and position near a field it has to be shown next to.

But there is a problem with scrolling. I need to listen to a scroll event on form and change a tooltip position. How can I do that without changing my html? Just inside the directive file. Here is my HTML(an example of using it inside a form):

<form #ngForm="ngForm" [formGroup]="form" (ngSubmit)="onSave()">
  <div class="form--edit">
    <div class="form__group p-grid">
         <label class="p-col-12 form__label">{{'substance.IUPACName-title' | translate}}</label>
         <div appInvalidField="names">
               <span *appInvalidFieldType="'required'" [translate]="'substance.IUPACName-field-required'"></span>
                <span *appInvalidFieldType="'maxlength'"
                                [translate]="'substance.IUPACName-field-maxlength'"></span>
          </div>
          <input class="p-col" pInputText [maxLength]="formService.maxLength" appAutofocus  formControlName="names" />
    </div>
  </div>
</form>

Upvotes: 2

Views: 2279

Answers (2)

Tomas Katz
Tomas Katz

Reputation: 1844

A much more elegant native solution would be using the IntersectionObserver as shown in the following post: How to check if element is visible after scrolling?

combining it with a directive would looks something like this:

@Directive({
  selector: '[fixedscroll]'
})
export class FixedscrollDirective{
@Input() windowOnly = false;
constructor(
    @Inject(ElementRef) {nativeElement}: ElementRef<Element>,
) {
    var observer = new IntersectionObserver(onIntersection, {
      root: null,   // default is the viewport
      threshold: .5 // percentage of taregt's visible area. Triggers "onIntersection"
    })

    // callback is called on intersection change
    function onIntersection(entries, opts){
      entries.forEach(entry =>
        entry.target.classList.toggle('visible', entry.isIntersecting)
      )
    }

    // Use the bserver to observe an element
    observer.observe( nativeElement )
}
}

Upvotes: -1

waterplea
waterplea

Reputation: 3661

Here's a directive that subscribes to all scroll events starting from current upwards:

@Directive({
    selector: '[allParentsScroll]',
})
export class AllParentsScrollDirective implements OnInit {
    @Output('allParentsScroll')
    readonly allParentsScroll$: Observable<Event>;

    private readonly ready$ = new Subject<void>();

    constructor(
        @Inject(ElementRef) {nativeElement}: ElementRef<Element>,
    ) {
        const eventTargets: EventTarget[] = [window, nativeElement];

        while (nativeElement.parentElement) {
            nativeElement = nativeElement.parentElement;
            eventTargets.push(nativeElement);
        }

        const allScroll$ = merge<Event>(
            ...eventTargets.map<Observable<Event>>(element => fromEvent(element, 'scroll')),
        );

        this.allParentsScroll$ = this.ready$.pipe(swithMapTo(allScroll$));
    }

    ngOnInit() {
        // Kickstart the listener when everything is ready
        this.ready$.next();
    }
}

Basically we just walk the DOM tree upwards, subscribe to scroll events on every container and merge it all into one big stream. I use it for the same case as you, but there is a known issue that when you try to position your element, querying any DOM positioning (offsets, client rects etc.) will cause a reflow, so even if you try to update your element position inside requestAnimationFrame — it would still lag behind a little bit on fast scrolling. From what I can tell there's not going around it, what I did is position it absolutely rather than fixed so body scroll will not actually change any calculation — this minimizes the issue in most common case.

Upvotes: 2

Related Questions