Reputation: 278
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
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
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