I have an issue with a directive that I created in Angular.
In this directive, I want to execute some code after the window.onload event to be sure that all the page resources have been loaded (because I want to know the page top offset of some elements in the directive and if the images are not loaded, this offset is not correct)
If I refresh the page, it works because of the window.onload event is fired but when using the angular navigation to reach my component, this event is not fired anymore.
I tried to use the lifecycle of angular like AfterViewInit but most of the time the AfterViewInit method is executed before the images are loaded.
UPDATE
This is the code of my directive :
export class ParallaxDirective implements OnInit, OnDestroy {
#Input() coef = 1;
start = 0;
path = 0;
initialTranslate = 0;
intersectObserver: IntersectionObserver;
inView: boolean;
constructor(
private element: ElementRef
) {
}
ngOnInit(): void {
window.addEventListener('load', () => this.initParallax());
}
ngOnDestroy(): void {
this.intersectObserver.unobserve(this.element.nativeElement);
window.removeEventListener('scroll', () =>this.setTransform());
}
initParallax(): void {
this.intersectObserver = new IntersectionObserver(this.intersect.bind(this));
this.intersectObserver.observe(this.element.nativeElement);
const initialY = new DOMMatrixReadOnly(window.getComputedStyle(this.element.nativeElement).getPropertyValue('transform')).m42;
this.start = this.element.nativeElement.offsetTop + initialY;
this.path = this.element.nativeElement.clientHeight;
this.initialTranslate = (initialY /
this.element.nativeElement.clientHeight) * 100;
if (window.pageYOffset > 0) {
this.setTransform(true);
}
window.addEventListener('scroll', () => this.setTransform());
}
setTransform(force = false): void {
if (!this.inView && !force) {
return;
}
const offset = window.pageYOffset + window.innerHeight - this.start;
const t = (offset * 10) / this.path;
const i = t * this.coef + this.initialTranslate;
this.element.nativeElement.style.setProperty('transform', `translate3d(0, ${i}%, 0)`);
}
intersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
entries.forEach(entry => {
this.inView = entry.isIntersecting || entry.intersectionRatio > 0;
});
}
}
As you can see, I need to retrieve some offsets and dom elements height. The problem is that these offsets and heights are not the same after resources in the page (like images) are completely loaded that's why I need to have an event like window.onload to be sure that everything is loaded. And I didn't find any Angular lifecycle that should be triggered after resources load.
I hope someone can help me with this.
Thanks.
After a long long exchange of comments, I finally understood your problem, Romain, and here's what I came up with.
My solution involves adding a directive and a service. The directive will be attached to all <img> tags, and will subscribe to their load events, and the service will coordinate all the load events firing and maintain a running list of images that are still being loaded.
Here's the directive:
#Directive({
selector: 'img'
})
export class MyImgDirective {
constructor(private el: ElementRef, private imageService: ImageService) {
imageService.imageLoading(el.nativeElement);
}
#HostListener('load')
onLoad() {
this.imageService.imageLoadedOrError(this.el.nativeElement);
}
#HostListener('error')
onError() {
this.imageService.imageLoadedOrError(this.el.nativeElement);
}
}
Here's the service:
#Injectable({
providedIn: 'root'
})
export class ImageService {
private _imagesLoading = new Subject<number>();
private images: Map<HTMLElement, boolean> = new Map();
private imagesLoading = 0;
imagesLoading$ = this._imagesLoading.asObservable();
imageLoading(img: HTMLElement) {
if (!this.images.has(img) || this.images.get(img)) {
this.images.set(img, false);
this.imagesLoading++;
console.log('images loading', this.imagesLoading);
this._imagesLoading.next(this.imagesLoading);
}
}
imageLoadedOrError(img: HTMLElement) {
if (this.images.has(img) && !this.images.get(img)) {
this.images.set(img, true);
this.imagesLoading--;
console.log('images loading', this.imagesLoading);
this._imagesLoading.next(this.imagesLoading);
}
}
}
And here's how I would use it in your parallax directive:
constructor(private imageService: ImageService) {}
ngOnInit() {
this.sub = imageService.imagesLoading$.pipe(filter(r => r === 0)).subscribe(_ => {
this.initParallax()
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
How does this work? The image directive gets attached to all images regardless of how deep they are in your component tree, registers the image with the service, and then tracks the loading progress by listening to the load event. Every time a new directive is created it means a new image is created, so a counter is incremented, and every time load fires, it means an image has finished loading, so the counter is decremented. We emit this counter in an observable, and so we can detect when all images are ready by waiting for the observable to emit the value 0.
Stackblitz demo
EDIT Added error handling in case an image points to a broken link
You can use HostListener
import { HostListener, Component } from "#angular/core";
#Component({
selector: 'app',
template: `<h1>{{message}}</h1>`
})
class AppComponent {
message = "Window not loaded";
#HostListener('load', ['$event'])
windowLoadedEvent(event) {
this.message = "Window loaded";
}
}
Note: It will work only if the component is initialized before window is loaded. Otherwise it will not work
Related
Please don't mark it as duplicate
I'm new in angular material design and I have a problem with mat-autocomplete. I have multiple Mat-Autocomplete in FormArray of FromGroup. On keyup in the input field, it's getting data from API calls and filled the autocomplete. After getting data on Keyup it will open the panel.
when I press word then a list of autocomplete opens then I want this list as infinite-scroll
I have multiple autocomplete in formArray of formGroup.
I don't want to use third-party dependency in the project like ngx-infinite-scroll.
Working Demo in this Stackblitz Link
When you want to detect autocomplete scroll end position you can use custom directive. In this directive you can calculate position of panel control and detect scroll end position, and once scroll end detected you can emit event to component. Directive Name is mat-autocomplete[optionsScroll] so that it auto detects mat-autocomplete component with optionScroll event and this custom directive is applied to all of this matching component. Directive is as follow..
export interface IAutoCompleteScrollEvent {
autoComplete: MatAutocomplete;
scrollEvent: Event;
}
#Directive({
selector: 'mat-autocomplete[optionsScroll]',
exportAs: 'mat-autocomplete[optionsScroll]'
})
export class MatAutocompleteOptionsScrollDirective {
#Input() thresholdPercent = 0.8;
#Output('optionsScroll') scroll = new EventEmitter<IAutoCompleteScrollEvent>();
_onDestroy = new Subject();
constructor(public autoComplete: MatAutocomplete) {
this.autoComplete.opened
.pipe(
tap(() => {
// Note: When autocomplete raises opened, panel is not yet created (by Overlay)
// Note: The panel will be available on next tick
// Note: The panel wil NOT open if there are no options to display
setTimeout(() => {
// Note: remove listner just for safety, in case the close event is skipped.
this.removeScrollEventListener();
this.autoComplete.panel.nativeElement.addEventListener(
'scroll',
this.onScroll.bind(this)
);
}, 5000);
}),
takeUntil(this._onDestroy)
)
.subscribe();
this.autoComplete.closed
.pipe(
tap(() => this.removeScrollEventListener()),
takeUntil(this._onDestroy)
)
.subscribe();
}
private removeScrollEventListener() {
if (this.autoComplete?.panel) {
this.autoComplete.panel.nativeElement.removeEventListener(
'scroll',
this.onScroll
);
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.removeScrollEventListener();
}
onScroll(event: Event) {
if (this.thresholdPercent === undefined) {
console.log('undefined');
this.scroll.next({ autoComplete: this.autoComplete, scrollEvent: event });
} else {
const scrollTop = (event.target as HTMLElement).scrollTop;
const scrollHeight = (event.target as HTMLElement).scrollHeight;
const elementHeight = (event.target as HTMLElement).clientHeight;
const atBottom = scrollHeight === scrollTop + elementHeight;
if (atBottom) {
this.scroll.next();
}
}
}
}
Now, you have to call scroll event to mat-autocomplete. On every scroll end onScroll() event is called by our directive.
<mat-autocomplete (optionsScroll)="onScroll()" > ... </mat-autocomplete>
Now, You have to load first and next chunk of data to mat-autocomplete like this..
weightData$ = this.startSearch$.pipe(
startWith(''),
debounceTime(200),
switchMap(filter => {
//Note: Reset the page with every new seach text
let currentPage = 1;
return this.next$.pipe(
startWith(currentPage),
//Note: Until the backend responds, ignore NextPage requests.
exhaustMap(_ => this.getProducts(String(filter), currentPage)),
tap(() => currentPage++),
//Note: This is a custom operator because we also need the last emitted value.
//Note: Stop if there are no more pages, or no results at all for the current search text.
takeWhileInclusive((p: any) => p.length > 0),
scan((allProducts: any, newProducts: any) => allProducts.concat(newProducts), [] ) );
})
);
private getProducts(startsWith: string, page: number): Observable<any[]> {
const take = 6;
const skip = page > 0 ? (page - 1) * take : 0;
const filtered = this.weightData.filter(option => String(option).toLowerCase().startsWith(startsWith.toLowerCase()));
return of(filtered.slice(skip, skip + take));
}
onScroll() {
this.next$.next();
}
So at first time we load only first chunk of data, when we reached end of scroll then we again emit event using next$ subject and our stream weightData$ is rerun and gives us appropiate output.
I have a web page. Where I have 2 text boxes.
I want to calculate the overall time spent on each box while clicking submit button at the end of the page.
clickInside() {
this.text = 'clicked inside';
this.wasInside = true;
this.activeTime.activeStartDate();
}
#HostListener('document:click')
clickout() {
if (!this.wasInside) {
this.text = 'clicked outside';
}
this.wasInside = false;
this.activeTime.activeEndDate();
}
I've created a little POC to manage your request.
You could write a really simple directive to implement in each input where you want to track the time spent.
import { Directive, HostListener } from '#angular/core';
#Directive({
selector: '[calcTime]'
})
export class CalcTimeDirective {
private timeSpentMs: number = 0;
private lastDate: Date;
constructor() { }
#HostListener('focus') onFocus(){
this.lastDate = new Date();
}
#HostListener('blur') onBlur(){
this.timeSpentMs += (new Date() as any) - (this.lastDate as any);
}
public reset(){
this.timeSpentMs = 0;
}
public getTime(){
return this.timeSpentMs;
}
public getTimeSeconds(){
return this.timeSpentMs / 1000;
}
}
You can find the code with a simple example here.
Hope it can help you.
I need help in updating the CSS for selectedElement, currently, this works but only for the first element.
Basically, I have a link, when I click on it:
(click)="showUserDetails($event)"
I'm passing the x & y coordinates to my method and want to use this to update the css and enabled a boolean flag which shows the div:
showUserDetails(el: any) {
let target = el.target || el.srcElement;
let selectUsername = (document.getElementById('userDetailsInfo') as HTMLInputElement);
selectUsername.style.top = target.offsetY + "px";
selectUsername.style.left = target.offsetX + "px";
this.selected = true;
el.preventDefault();
}
Currently, the above code only seems to work for the first element and I'm trying to figure out a way to have it so based on the clicked element, go up the dom and find me the div with the id of 'userDetailsInfo' and apply the top/left values to that, and not just to the first item.
I actually solved it like so:
private hostEl: HTMLElement;
constructor(el: ElementRef) {
this.hostEl = el.nativeElement;
}
ngOnInit() {
window.addEventListener('scroll', this.scroll, true);
}
ngOnDestroy() {
window.removeEventListener('scroll', this.scroll, true);
}
//Get the header
let header = (document.querySelector('.header') as HTMLInputElement);
if (window.pageYOffset >= 100){
header.classList.add('sticky');
} else {
header.classList.remove('sticky');
}
I have some description text from an API that I am inserting as HTML into the DOM.
<div class="activity-description" [innerHTML]="description"></div>
The description is set within ngOninit();
if (this.eventDetail.description.length > 255) {
this.description = this.eventDetail.description.substring(0, 255) + '<span class="more-description"> ...Learn More</span>';
}
I am trying to add an event listener to the "more-description" class within the ngAfterViewInit()
var el = this.elementRef.nativeElement.querySelector('.more-description');
if (el)
el.addEventListener('click', this.displayFullDescription());
The element is null and does not allow the event listener to be attached. How do I add this event listener to html elements that are dynamically added?
You can manually render view by calling cdRef.detectChanges:
constuctor(private cdRef: ChangeDetectorRef) {}
ngOnInit() {
if (this.eventDetail.description.length > 255) {
this.description = this.eventDetail.description.substring(0, 255) +
'<span class="more-description"> ...Learn More</span>';
}
}
ngAfterViewInit() {
this.cdRef.detectChanges();
var el = this.elementRef.nativeElement.querySelector('.more-description');
}
Update
Perhaps you made some mistake in this code:
el.addEventListener('click', this.displayFullDescription());
I don't know what displayFullDescription function does.
Here is working example:
#Component({
selector: 'event',
template: `
<div class="activity-description" [innerHTML]="description"></div>
`,
})
export class Event {
#Input() eventDetail: any;
description: string;
constructor(private elementRef: ElementRef) { }
ngOnInit() {
if (this.eventDetail.description.length > 255) {
this.description = this.eventDetail.description.substring(0, 255) + '<span class="more-description"> ...Learn More</span>';
}
}
displayFullDescription() {
this.description = this.eventDetail.description;
}
ngAfterViewInit() {
var el = this.elementRef.nativeElement.querySelector('.more-description');
if(el) {
el.addEventListener('click', this.displayFullDescription.bind(this));
}
}
}
Plunker Example
Note: It would be better if you store handler in class property so that you can unsubscribe later.
I want to know what event will be fired when the innerHtml property of any div is changed and DOM the tree for that div is completely loaded into memory.
I want to invoke the following function after that event is fired. I want to remove the setTimeout() hack in the following function as it may fail sometime.
private registerEventListenersForLink() {
let _self = this;
setTimeout(function () {
var AElemList = document.querySelectorAll('.appmedia-content-wrapper a');
for (let i = 0; i < AElemList.length; i++) {
AElemList[i].addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
var url = event.target["href"];
if (url && url.trim() != '') {
_self.utilService.openUrlExternaly(url);
}
});
}
}, 80);
}
I have found following links related to above issue but I haven't got any ideas out of it:
Event to determine when innerHTML has loaded
https://www.w3.org/html/wg/spec/apis-in-html-documents.html#dom-innerhtml
I fixed my issue. I have implemented a directive as follows and it has worked like a charm. Thank you Rory McCrossan for pointing me at MutationObserver api.
import { Directive, ElementRef } from '#angular/core';
import { UtilService } from '../../providers/util-service';
#Directive({
selector: '[external-links]' // Attribute selector
})
export class ExternalLinks {
private observer;
constructor(private elRef: ElementRef, public utilService: UtilService) {
}
ngAfterViewInit() {
var _self = this;
this.observer = new MutationObserver(mutations => {
mutations.forEach(function (mutation) {
if (mutation.type == 'childList') {
var AElemList = _self.elRef.nativeElement.querySelectorAll('a');
for (let i = 0; i < AElemList.length; i++) {
AElemList[i].addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
var url = event.target["href"];
if (url && url.trim() != '') {
_self.utilService.openUrlExternaly(url);
}
});
}
}
});
});
var config = { childList: true };
this.observer.observe(this.elRef.nativeElement, config);
}
}