برنامه نویسی

دستورالعمل ناظر تقاطع برای افزودن و حذف کلاس ها در Angular

در این سری می‌خواهیم یک دستورالعمل یا دستورالعمل‌های متعدد در Angular ایجاد کنیم تا ویژگی‌های مختلفی را که از ناظر تقاطع استفاده می‌کنند، عمدتاً، تطبیق دادن کلاس‌های عناصر DOM، بارگذاری تنبل تصاویر و انتشار رویدادها ایجاد کنیم.

اول، اصول اولیه، این دستورالعمل باید در صورت SSR بازگردد.

من تعریف را قرض گرفتم isBrowser از فایل پلتفرم Angular CDK، مانند 37.

طرح اصلی دستورالعمل ما به این شکل است. اسمشو گذاشتم crUiio زیرا io اشتباه بود و iobs اشتباه تر بود!

کد نهایی را می توان در StackBlitz یافت.

// cruiio directive
@Directive({
    selector: '[crUiio]',
    standalone: true
})
export class UiioDirective implements AfterViewInit {
  @Input() crUiio: string;

  // at least ElementRef, platform, and renderer
  constructor(private el: ElementRef,
    @Inject(PLATFORM_ID) private _platformId: Object,
    private renderer: Renderer2) {
  }
  // this is a nice property copied from Angular CDK
  isBrowser: boolean = this._platformId
    ? isPlatformBrowser(this._platformId)
    : typeof document === 'object' && !!document;

  ngAfterViewInit() {
      // if on server, do not show image
      if (!this.isBrowser) {
        return;
      }

      // if intersection observer is not supported
      if (!IntersectionObserver) {
        // do nothing
        return;
      }

      const io = new IntersectionObserver(
        (entries, observer) => {
          // there is only one entry per directive application
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
               // in view

            } else {
               // out of view
            }
          });
        }
      );

      // observe the native element
      io.observe(this.el.nativeElement);
  }
}
وارد حالت تمام صفحه شوید

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

با توجه به این پست وبلاگ عالی در مورد کشش احتمالی عملکرد ناظران متعدد، در مقابل یک ناظر در هر سند، به نظر من وجود یک ناظر در هر عنصر، برای کمتر از 1000 عنصر، خوب است. اجازه دهید هر دو راه را امتحان کنیم و راحت تر را انتخاب کنیم.

یک ناظر در هر پنجره

این ناظر تقاطع جهانی را کجا نجات دهیم؟ من در window گستره جهانی برای ارجاع به window من یک را ایجاد خواهم کرد const با نوع any به طوری که VSCode من را در مورد نوع آزار ندهد.

// keep on observer on window

const _global: any = window;

@Directive({ ... })
export class UiioDirective implements AfterViewInit {
 //...
   private getIo() {
    if (_global['crUiio']) {
      return _global['crUiio'];
    } else {
      _global['crUiio'] = new IntersectionObserver((entries, observer) => {
        // this call back is called once
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            // in view, check which behavior to carry out
          } else {
            // out of view
          }
        });
      });
      return _global['crUiio'];
    }
  }
  ngAfterViewInit() {
      // ...
    const io = this.getIo();
    io.observe(this.el.nativeElement);
  }
}
وارد حالت تمام صفحه شوید

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

از آنجایی که می خواهیم از این دستورالعمل برای انجام کارهای دیگر نیز استفاده کنیم، بیایید یک ویژگی ایجاد کنیم که رفتار را مشخص کند. انتخاب آشکار ارزش فوری دستورالعمل است. این هست یک string، اما می توانیم نوع را به رشته های خاصی تغییر دهیم.

@Input() crUtil: 'lazy'|'class'|'load' //... etc;

همچنین برای هر کدام یک روش خصوصی ایجاد خواهیم کرد و اجازه دهید تماس گیرنده اصلی را برای جابجایی بین رفتارهای مختلف تطبیق دهیم

// adapt getIo
_global['crUiio'] = new IntersectionObserver((entries, observer) => {
 entries.forEach((entry) => {
    switch (this.crUtil) {
      case 'lazy':
        this.lazyLoad(entry, observer);
        break;
      case 'load':
        // do something...
        break;
      case 'class':
      default:
        this.classChange(entry, observer);
        break;
    }
 });
});
وارد حالت تمام صفحه شوید

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

خوب، بیایید آداپتور کلاس را پیاده سازی کنیم.

تطبیق کلاس های css

رفتار اضافه کردن یک کلاس به عنصر یا حذف یک کلاس. ما می خواهیم هر دو گزینه را داشته باشیم:

  • در تقاطع، کلاس را اضافه کنید، کلاس را حذف کنید.
  • یا در تقاطع حذف کلاس، در خارج از دید اضافه کردن کلاس.

منعطف ترین راه این است که فقط با کلاس هایی که باید اضافه یا حذف شوند، صریح باشید:

// example usage
<div crUiio="class" inview="a b" outofview="c d"></div>
وارد حالت تمام صفحه شوید

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

علاوه بر آن، (من از روی تجربه صحبت می کنم)، ما می خواهیم یک ظرف بالاتر را با لیست کلاس هدف قرار دهیم، بهترین گزینه ما در اینجا این است که body. من انتخاب می‌کنم تا آنجا که می‌توانم انعطاف‌پذیر باشم، نه به این دلیل که می‌خواهم در مورد نیازهای آینده که ممکن است رخ دهد یا نه، باز شوم، از آن سطح غرور عبور کرده‌ام، بلکه به این دلیل که بارها توسط این موضوع گزیده شده‌ام.

// example use
<div crUiio="class" inview="a b" inviewbody="x y" ></div>
وارد حالت تمام صفحه شوید

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

حالا همه مواد لازم را داریم، بیایید بپزیم.

// directive
// that's a whole lot of inputs and statements
private classChange(entry: IntersectionObserverEntry, observer: IntersectionObserver) {
  if (entry.isIntersecting) {
    // in view, separate then add tokens
    document.body.classList.add(this.inviewbody);
    entry.target.classList.add(this.inView);
    document.body.classList.remove(this.outofViewBody);
    entry.target.classList.remove(this.outofView);

  } else {
    // out of view
    document.body.classList.remove(this.inViewBody);
    entry.target.classList.remove(this.inView);
    document.body.classList.add(this.outofViewBody);
    entry.target.classList.add(this.outofView);
  }
}
وارد حالت تمام صفحه شوید

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

بیایید قبل از اینکه ادامه دهیم این را تا اینجا آزمایش کنیم. یک مثال در StackBlitz بیابید، یک صفحه بسیار ساده با جعبه های زشت، ما می خواهیم اعتبار سنجی کنیم که body و جعبه ها در صورت درخواست کلاس های مناسب را دریافت می کنند. در اینجا مشکلاتی وجود دارد که نیاز به رفع دارند:

  • ما باید لیست کلاس را قبل از استفاده از آن پردازش کنیم، باید به چندین رشته جدا کنیم و رشته های خالی را حذف کنیم، زیرا classList.add('') شکست خواهد خورد، اما classList.add(null) نمی خواهد.
  • ما باید راهی برای عبور این مقادیر از طریق آن پیدا کنیم هدف ورودی، از آنجایی که داریم یک ناظر مشترک، که با اولین دایرکتیو استفاده شده در تماس برگشتی شروع می شود

آماده سازی کلاس: ما همه آنها را در یک شی قرار می دهیم و آنها را آماده می کنیم AfterViewInit. با فرض اینکه آنها پس از مقداردهی اولیه تغییر نخواهند کرد. من هنوز به شرایطی برخورد نکرده‌ام که کلاس‌هایم را از قبل نمی‌دانم، اما اگر با آن مواجه شدید، احتمالاً باید این مورد را با یک روش عمومی که مقادیر را تغییر می‌دهد تطبیق دهید.

همچنین مقدار پیش‌فرض را به رشته‌های خالی اختصاص می‌دهیم که هیچ ضرری ندارد. یک عبارت کوتاه برای حذف رشته های خالی، و روش آماده سازی به این شکل است

// directive

private _allClasses: any;

private prepClasses(): any {
  // split at spaces and remove empty ones
  const clean = (str: string) => str!.split(' ').filter((n) => n !== '');

  return {
    inView: clean(this.inview),
    outofView: clean(this.outofview),
    inViewBody: clean(this.inviewbody),
    outofViewBody: clean(this.outofviewbody),
  };
}
// ...
ngAfterViewInit() {
  // ...
  // prep classes
  this._allClasses = this.prepClasses();
}
وارد حالت تمام صفحه شوید

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

اکنون تابع تغییر کلاس از آنها عبور می کند، کلاس ها را به صورت مورد نیاز به این شکل خرج کنید

// use by expand
document.body.classList.add(...this._allClasses.inViewBody)
وارد حالت تمام صفحه شوید

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

جداسازی پاسخ به تماس: از آنجا که changeClass تابع یک است پاسخ به تماس به راه اندازی جهانی intersectionObserver، کلاس های پاس شده آنهایی هستند که متعلق به بخش اول هستند. این باحال نیست. در عوض، ما باید بارگیری کنیم nativeElement که در Angular می آید ElementRef با کلاس های خودش، برای استفاده مجدد entry.target.

بارگیری اطلاعات در ویژگی های داده HTML به صورت برنامه ریزی شده

یه جورایی دلم برای قدیمی های خوب تنگ شده jQuery. تنها کاری که در آن زمان باید انجام می دادیم این بود $(element).data(json) و جادو! ما نمی توانیم این کار را مستقیماً خارج از جعبه انجام دهیم JavaScript. ما می توانیم استفاده کنیم dataset ویژگی، که باید JSON باشد رشته دار شده، سپس JSON تجزیه شد. یک … وجود دارد راه ساده تر با این حال. ما می‌توانیم ویژگی را مستقیماً روی the تنظیم کنیم HTML عنصر، مانند این

this.el.nativeElement['newprop'] = {...this._allClasses};

و اینجوری بخون

entry.target['newprop'];

این شبیه یک تقلب است! اما من می دانم JavaScript، و DOM، برخورد با آنها واقعاً به همین سادگی است. من به آن پایبند خواهم بود تا زمانی که ناله کند.

بنابراین پس از مقداردهی اولیه view، کلاس ها را اضافه می کنیم، و هنگامی که مشاهده مشاهده شد، آنها را بازیابی می کنیم:

private classChange(entry: IntersectionObserverEntry,observer: IntersectionObserver) {
  // load from props, cast to magical "any"
  const c = (<any>entry).target['data-classes'];

  if (entry.isIntersecting) {
    // in view
    document.body.classList.add(...c.inViewBody);
    // ... the rest
  }
}
ngAfterViewInit() {
  //...

  // prep classes
  this._allClasses = this.prepClasses();

  // load classes to element
  this.el.nativeElement['data-classes'] = { ...this._allClasses };

  // observe the native element
  const io = this.getIo();
  io.observe(this.el.nativeElement);
}
وارد حالت تمام صفحه شوید

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

آستانه

از همه گزینه های تقاطع موجود root، rootMargin، و threshold، فقط threshold یکی از مواردی است که ممکن است در یک دستورالعمل کاربردی جهانی تفاوت ایجاد کند. با توجه به تجربه‌ام، من فقط به دو مقدار نیاز داشتم: 0، یا 1. اما بهتر است دقیق‌تر باشیم. پاس دادن یک آرایه کار بیش از حدی خواهد بود، بنابراین این کار را نمی کنیم.

نکته جانبی: داشتن rootMargin روی چیزی تنظیم کنید، که معمولاً به گرفتن تقاطع ها با اجزای لبه کمک می کند، مانند فوتر چسبیده به پایین بدنه. اما با threshold، می توانیم بر این مسئله غلبه کنیم. بنابراین، نیازی به استفاده نیست rootMargin.

با این حال، اینجا جایی است که ما راه خود را با یک ناظر در هر صفحه جدا می کنیم. این threshold ویژگی مقداردهی اولیه می شود و فقط خوانده می شود.

یک ناظر برای هر عنصر

برای عبور از آستانه، باید کد خود را طوری تغییر دهیم که ناظر برای هر دستورالعمل منحصر به فرد است. همانطور که در بالا گفتیم، سود عملکرد داشتن یک ناظر واحد زیاد نیست. با این حال، سادگی کد ناظران متعدد شگفت‌انگیز است. اما اولاً، از آنجایی که ما دیگر یک ناظر نداریم، هیچ فایده ای ندارد که دستورالعمل ما برای همه رفتارها عمل کند، چندین مورد خواهیم داشت. یکی برای انطباق کلاس، یکی برای بارگذاری تنبل و یکی برای خانه! و غیره.

دیگر نیازی نیست که فهرست عناصر تقاطع را مرور کنیم، اولین مورد به اندازه کافی خوب است: entries[0].

// invew.directive
// change intersection observer instance
const io = new IntersectionObserver(
  (entries, observer) => {
    this.classChange(entries[0], observer);
  },
  {
    threshold: _todoprop,
  }
);
io.observe(this.el.nativeElement);
وارد حالت تمام صفحه شوید

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

همچنین دیگر مجبور نیستیم کلاس ها را بگذرانیم data-classes ویژگی، عضو خصوصی به اندازه کافی خوب است.

ورودی های Refactor

قرار دادن هر کدام خوب است input از نظر خصوصیات خاص خود، اما در طبیعت، شانس عبور از هر 4 کلاس چقدر است؟ احتمال تغییر آستانه چقدر است؟ شاید بتوانیم با این زندگی کنیم:

<div crInview="A B" ></div>

بیایید ورودی‌هایمان را کمی دقیق‌تر کنیم و دستوری را که برای رایج‌ترین حادثه استفاده می‌شود نام ببریم: in-view.

// inview.directive
// refactor

@Input() options: {
  outofview?: string,
  inviewbody?: string,
  outofviewbody?: string,
  threshold?: number;
} = {
  // have defaults
  outofview: '',
  inviewbody: '',
  outofviewbody: '',
  threshold: 0
};

private prepClasses(): any {

  return {
    inView: clean(this.crInview), // this is the main string
    outofView: clean(this.options?.outofview),
    inViewBody: clean(this.options?.inviewbody),
    outofViewBody: clean(this.options?.outofviewbody),
  };
}
وارد حالت تمام صفحه شوید

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

کمی تایپ اضافی برای اینکه مطمئن شویم ورق نزنیم و بخشنامه آماده ارائه است.

مشاهده را متوقف کنید

آخرین چیزی که ممکن است مفید باشد این است که به دستورالعمل اجازه می دهیم در هنگام وقوع تقاطع، مشاهده را متوقف کند. در اینجا یک ویژگی گزینه جدید است: once.

// directive new property
@Input() options: {
  // ...
  once?: boolean
} = {
  //...
  once: false
};

// stop observing after first intersection
if (entry.isIntersecting) {
  // ...
  if(this.options.once) {
    observer.unobserve(entry.target);
  }
} else {
  // out of view
  // ...
}
وارد حالت تمام صفحه شوید

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

ما نیز ممکن است disconnect کل ناظر تقاطع از آنجایی که ما در حال تماشای یک ورودی هستیم، اما اگر بخواهیم رصد را از سر بگیریم چه؟ این یک ویژگی خوب است که ما برای یک سه شنبه خوب حفظ خواهیم کرد.

با این کار، ما به اندازه کافی برای ایجاد یک افکت بارگذاری تنبل با استفاده از آن داریم تصاویر پس زمینه css.

<div class="footer-bg" crInview="footer-bg-inview"
    [options]="{threshold: 0.5, once: true}">
    here is the footer with initial image that changes to new image when in view
    then stops observing
</div>
وارد حالت تمام صفحه شوید

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

کلاس اصلی دارای تصویر کوچک است footer-bg-inview کلاس تصویر کامل را خواهد داشت. تصاویر پس زمینه معمولاً آرایشی هستند و SSR نیازی به دانستن آنها ندارد. بنابراین این کار خواهد کرد. اما، ما می توانیم در مورد تصاویر دقت بیشتری داشته باشیم. بیایید عمیق تر کاوش کنیم و یک دستورالعمل متفاوت برای بارگذاری تنبل تصاویر، انشاءالله سه شنبه آینده ایجاد کنیم. 😴

آیا شما اشاره به iobs?

منابع

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

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

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

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