Angular LAB: بیایید یک دستورالعمل دید ایجاد کنیم

در این مقاله میخواهم نحوه ایجاد یک دستورالعمل زاویهای بسیار ساده را نشان دهم که وضعیت دید یک عنصر یا به عبارت دیگر، زمانی که در ویوپورت وارد و خارج میشود را پیگیری میکند. امیدوارم این یک تمرین خوب و شاید مفید باشد!
برای انجام این کار، ما از IntersectionObserver
JavaScript API که در مرورگرهای مدرن موجود است.
چیزی که می خواهیم به آن برسیم
ما می خواهیم از دستورالعمل به این صورت استفاده کنیم:
visibility
[visibilityMonitor]="true"
(visibilityChange)="onVisibilityChange($event)"
>
I'm being observed! Can you see me yet?
-
visibility
انتخاب کننده دستورالعمل سفارشی ما است -
visibilityMonitor
یک ورودی اختیاری است که مشخص میکند آیا باید به مشاهده عنصر ادامه داد یا خیر (اگرfalse
، هنگام ورود به ویوپورت، نظارت را متوقف کنید) -
visibilityChange
به ما اطلاع خواهد داد
خروجی به این شکل خواهد بود:
type VisibilityChange =
| {
isVisible: true;
target: HTMLElement;
}
| {
isVisible: false;
target: HTMLElement | undefined;
};
تعریف نشده داشتن target
به این معنی است که عنصر از DOM حذف شده است (به عنوان مثال، توسط یک @if
).
ایجاد دستورالعمل
دستورالعمل ما به سادگی یک عنصر را نظارت می کند، ساختار DOM را تغییر نمی دهد: یک عنصر خواهد بود دستورالعمل ویژگی.
@Directive({
selector: "[visibility]",
standalone: true
})
export class VisibilityDirective implements OnInit, OnChanges, AfterViewInit, OnDestroy {
private element = inject(ElementRef);
/**
* Emits after the view is initialized.
*/
private afterViewInit$ = new Subject<void>();
/**
* The IntersectionObserver for this element.
*/
private observer: IntersectionObserver | undefined;
/**
* Last known visibility for this element.
* Initially, we don't know.
*/
private isVisible: boolean = undefined;
/**
* If false, once the element becomes visible there will be one emission and then nothing.
* If true, the directive continuously listens to the element and emits whenever it becomes visible or not visible.
*/
visibilityMonitor = input(false);
/**
* Notifies the listener when the element has become visible.
* If "visibilityMonitor" is true, it continuously notifies the listener when the element goes in/out of view.
*/
visibilityChange = output<VisibilityChange>();
}
در کد بالا می بینید:
- را
input
وoutput
قبلا در موردش صحبت کردیم - ملکی به نام
afterViewInit$
(یک قابل مشاهده) که به عنوان یک همتای واکنشی عمل می کندngAfterViewInit
قلاب چرخه زندگی - ملکی به نام
observer
که ذخیره خواهد کردIntersectionObserver
مسئول نظارت بر عنصر ما است - ملکی به نام
isVisibile
که آخرین حالت دید را ذخیره می کند تا از انتشار مجدد همان حالت دو بار متوالی جلوگیری شود
و به طور طبیعی، ما تزریق می کنیم ElementRef
به منظور گرفتن عنصر DOM که دستورالعمل خود را بر روی آن اعمال می کنیم.
قبل از نوشتن روش اصلی، بیایید به چرخه حیات دستورالعمل توجه کنیم.
ngOnInit(): void {
this.reconnectObserver();
}
ngOnChanges(): void {
this.reconnectObserver();
}
ngAfterViewInit(): void {
this.afterViewInit$.next();
}
ngOnDestroy(): void {
// Disconnect and if visibilityMonitor is true, notify the listener
this.disconnectObserver();
if (this.visibilityMonitor) {
this.visibilityChange.emit({
isVisible: false,
target: undefined
});
}
}
private reconnectObserver(): void {}
private disconnectObserver(): void {}
حالا این چیزی است که اتفاق می افتد:
- داخل هر دو
ngOnInit
وngOnChanges
ناظر را دوباره راه اندازی می کنیم. این به منظور واکنشی کردن دستورالعمل است: اگر ورودی تغییر کند، دستورالعمل متفاوت رفتار می کند. توجه داشته باشید که حتی اگرngOnChanges
قبل نیز اجرا می شودngOnInit
، ما هنوز نیاز داریمngOnInit
چونngOnChanges
اگر ورودی در قالب وجود نداشته باشد اجرا نمی شود! - وقتی نما مقدار دهی اولیه می شود، آن را فعال می کنیم
Subject
، تا چند ثانیه دیگر به این موضوع خواهیم رسید - ما اتصال خود را قطع می کنیم
observer
هنگامی که دستورالعمل برای جلوگیری از نشت حافظه از بین می رود. در نهایت، اگر توسعهدهنده آن را درخواست کرد، به اطلاع میرسانیم که با ارسال یک عنصر از DOM حذف شده است.undefined
عنصر
IntersectionObserver
این قلب بخشنامه ما است. ما reconnectObserver
روش شروع به مشاهده خواهد بود! چیزی شبیه این خواهد بود:
private reconnectObserver(): void {
// Disconnect an existing observer
this.disconnectObserver();
// Sets up a new observer
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
const { isIntersecting: isVisible, target } = entry;
const hasChangedVisibility = isVisible !== this.isVisible;
const shouldEmit = isVisible || (!isVisible && this.visibilityMonitor);
if (hasChangedVisibility && shouldEmit) {
this.visibilityChange.emit({
isVisible,
target: target as HTMLElement
});
this.isVisible = isVisible;
}
// If visilibilyMonitor is false, once the element is visible we stop.
if (isVisible && !this.visibilityMonitor) {
observer.disconnect();
}
});
});
// Start observing once the view is initialized
this.afterViewInit$.subscribe(() => {
this.observer?.observe(this.element.nativeElement);
});
}
به من اعتماد کنید، آنقدرها هم که به نظر می رسد پیچیده نیست! مکانیزم این است:
- ابتدا مشاهدهگر را که قبلاً در حال اجرا بود، قطع میکنیم
- ما ایجاد می کنیم
IntersectionObserver
و رفتار آن را تعریف کنید. راentries
حاوی عناصر نظارت شده خواهد بود، بنابراین حاوی عنصر ما خواهد بود. ملکisIntersecting
نشان می دهد که آیا دید عنصر تغییر کرده است یا خیر: ما آن را با حالت قبلی (ویژگی ما) مقایسه می کنیم و اگر به دلیل آن است، ما منتشر می کنیم. سپس حالت جدید را برای بعد در ملک خود ذخیره می کنیم. - اگر
visibilityMonitor
استfalse
، به محض اینکه عنصر قابل مشاهده شد، ناظر را قطع می کنیم: کار آن تمام شده است! - سپس ما باید شروع کنید ناظر با عبور از عنصر ما، بنابراین برای انجام این کار منتظریم تا نمای ما مقداردهی اولیه شود.
در نهایت، بیایید روشی را پیاده سازی کنیم که ناظر را قطع می کند، آسان پیزی:
private disconnectObserver(): void {
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
کد نهایی
این بخشنامه کامل است. این فقط یک تمرین بود، پس آزادانه آن را به هر چیزی که دوست دارید تغییر دهید!
type VisibilityChange =
| {
isVisible: true;
target: HTMLElement;
}
| {
isVisible: false;
target: HTMLElement | undefined;
};
@Directive({
selector: "[visibility]",
standalone: true
})
export class VisibilityDirective
implements OnChanges, OnInit, AfterViewInit, OnDestroy {
private element = inject(ElementRef);
/**
* Emits after the view is initialized.
*/
private afterViewInit$ = new Subject<void>();
/**
* The IntersectionObserver for this element.
*/
private observer: IntersectionObserver | undefined;
/**
* Last known visibility for this element.
* Initially, we don't know.
*/
private isVisible: boolean = undefined;
/**
* If false, once the element becomes visible there will be one emission and then nothing.
* If true, the directive continuously listens to the element and emits whenever it becomes visible or not visible.
*/
visibilityMonitor = input(false);
/**
* Notifies the listener when the element has become visible.
* If "visibilityMonitor" is true, it continuously notifies the listener when the element goes in/out of view.
*/
visibilityChange = output<VisibilityChange>();
ngOnInit(): void {
this.reconnectObserver();
}
ngOnChanges(): void {
this.reconnectObserver();
}
ngAfterViewInit(): void {
this.afterViewInit$.next(true);
}
ngOnDestroy(): void {
// Disconnect and if visibilityMonitor is true, notify the listener
this.disconnectObserver();
if (this.visibilityMonitor) {
this.visibilityChange.emit({
isVisible: false,
target: undefined
});
}
}
private reconnectObserver(): void {
// Disconnect an existing observer
this.disconnectObserver();
// Sets up a new observer
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
const { isIntersecting: isVisible, target } = entry;
const hasChangedVisibility = isVisible !== this.isVisible;
const shouldEmit = isVisible || (!isVisible && this.visibilityMonitor);
if (hasChangedVisibility && shouldEmit) {
this.visibilityChange.emit({
isVisible,
target: target as HTMLElement
});
this.isVisible = isVisible;
}
// If visilibilyMonitor is false, once the element is visible we stop.
if (isVisible && !this.visibilityMonitor) {
observer.disconnect();
}
});
});
// Start observing once the view is initialized
this.afterViewInit$.subscribe(() => {
this.observer?.observe(this.element.nativeElement);
});
}
private disconnectObserver(): void {
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
}