دستورالعمل ناظر تقاطع برای افزودن و حذف کلاس ها در 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
?
منابع