برنامه نویسی

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;
    }
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا