###### tags: `Angular` `InfiniteScroll` `IntersectionObserver` # 🌝[T]Infinite Scroll #IntersectionObserver ![](https://i.imgur.com/9UwAhjm.png) ## 無限滾輪 (Infinite Scroll) 近年因為社群軟體的興起(Twitter,Facebook,Pinterest,Instagram),讓以==滾動==呈現內容的方式很流行,因此藉著機會我也來實作無限滾輪。 > 呈現內容的方式,目前主要被使用的有兩種 : > - 滾動 (Scroll) > - 分頁 (Pagination) > > 使用的時機也有些不同,下面有相關的文章可以看。 [UX: Infinite Scrolling vs. Pagination](https://uxplanet.org/ux-infinite-scrolling-vs-pagination-1030d29376f1) [Infinite Scrolling Is Not for Every Website](https://www.nngroup.com/articles/infinite-scrolling/) ## 實現筆記 [MDN-Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) [IntersectionObserver:上篇-基本介紹及使用](https://www.letswrite.tw/intersection-oserver-basic/) [IntersectionObserver:下篇-實際應用 lazyload、進場效果、無限捲動](https://www.letswrite.tw/intersection-oserver-demo/) [認識 Intersection Observer API:實作 Lazy Loading 和 Infinite Scroll](https://medium.com/%E9%BA%A5%E5%85%8B%E7%9A%84%E5%8D%8A%E8%B7%AF%E5%87%BA%E5%AE%B6%E7%AD%86%E8%A8%98/%E8%AA%8D%E8%AD%98-intersection-observer-api-%E5%AF%A6%E4%BD%9C-lazy-loading-%E5%92%8C-infinite-scroll-c8d434ad218c) ### IntersectionObserver IntersectionObserver 先簡稱 IO ,我們可以用IO設定`鏡頭`,觀察`目標`是否出現在鏡頭內,若目標`進入鏡頭`或`離開鏡頭`IO就告訴我們。 > *網路上的舉例* > IO 就像是超商的電鈴,當有客人進出入門口時會發出聲音,通知店員有客人進出入門口。 > 那在沒有 IO 前,大多是使用`監聽scroll事件`來實現無限滾輪,只不過這樣十分消耗資源,就像是超商在沒有電鈴的情況下,店員需要一直注意門口是否有客人。 ==範例.== [StackBlitz-Infinite-scroll-practice](https://stackblitz.com/edit/infinite-scroll-practice?file=src/app/app.component.ts) #### step1. 建立observer #設定鏡頭 ```typescript this.observer = new IntersectionObserver(this.callback, this.option); ``` *callback:* 當目標進入鏡頭內會call的function ```typescript (entries, observer) => { } ``` *option:* 鏡頭的設定 ``` { root: null, rootMargin: "0px 0px 0px 0px", threshold: [0] } ``` - root: 鏡頭 1. 預設值是`null`(表示window,就是螢幕正在看的區域) 2. 可設定element為鏡頭 ex: `document.getElementById('container');`。 - rootMargin: 鏡頭邊界 (上、右、下、左) 1. 預設值是`"0px 0px 0px 0px"`。 2. 放大/縮小鏡頭邊界用(不過實際用起來好像沒用?)。 - threshold: 目標出現的百分比 1. 預設值是`[0]`。 2. 指定目標出現多少就執行callback,可以設多個 ex: `[0, 0.25, 0.5, 0.7, 1.0]`。 #### step2. 建立entry #設定鏡頭要觀察的目標 ```typescript const entry = document.querySelector("#item"); this.observer.observe(entry); ``` *entry:* 目標 - 設定element為目標 *observer:* 鏡頭 - observe 開始觀察 - unobserve 取消觀察 - disconnect 關閉鏡頭 #### step3. callback執行 #當目標進入/離開鏡頭 ```typescript callback = (entries, observer) => { entries.forEach(entry => { if (!entry.isIntersecting) { console.log("ruby 離開鏡頭"); } else { console.log("ruby 進入鏡頭"); } } // Each entry describes an intersection change for one observed // target element: // entry.boundingClientRect #ReadOnly:目標元素的矩形區域的信息 // entry.intersectionRatio #獵物的可見比例 // entry.intersectionRect #ReadOnly:獵物與root的交叉區域 // entry.isIntersecting #是否出現在鏡頭(root))中 // entry.rootBounds #ReadOnly:鏡頭(root)的資訊 // entry.target #獵物本身在DOM上的節點 // entry.time } ``` *entry:* 目標 - boundingClientRect `目標(entry)`元素的矩形區域的信息 #ReadOnly - intersectionRatio `目標(entry)`的可見比例 - intersectionRect `目標(entry)`與`鏡頭`(root)的交叉區域 #ReadOnly - isIntersecting 是否出現在`鏡頭(root)`中 - rootBounds `鏡頭(root)`的資訊 #ReadOnly - target `目標(entry)`本身在DOM上的節點 - time #### 完整程式 ==app.component.ts== ```typescript= import { AfterViewInit, Component, VERSION } from "@angular/core"; @Component({ selector: "my-app", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"] }) export class AppComponent implements AfterViewInit { itemlist = [ { value: 1, name: "RB01" }, { value: 2, name: "RB02" }, { value: 3, name: "RB03" }, { value: 4, name: "RB04" }, { value: 5, name: "RB05" }, { value: 6, name: "RB06" }, { value: 7, name: "RB07" }, { value: 8, name: "RB08" }, { value: 9, name: "RB09" }, { value: 10, name: "RB10" }, { value: 11, name: "RB11" }, { value: 12, name: "RB12" }, { value: 13, name: "RB13" }, { value: 14, name: "RB14" }, { value: 15, name: "RB15" }, { value: 16, name: "RB16" }, { value: 17, name: "RB17" }, { value: 18, name: "RB18" }, { value: 19, name: "RB19" }, { value: 20, name: "RB20" }, { value: 21, name: "RB21" }, { value: 22, name: "RB22" }, { value: 23, name: "RB23" }, { value: 24, name: "RB24" }, { value: 25, name: "RB25" }, { value: 26, name: "RB26" }, { value: 27, name: "RB27" }, { value: 28, name: "RB28" }, { value: 29, name: "RB29" }, { value: 30, name: "RB30" } ]; item = [ { value: 1, name: "RB01" }, { value: 2, name: "RB02" }, { value: 3, name: "RB03" }, { value: 4, name: "RB04" }, { value: 5, name: "RB05" } ]; options = { root: document.getElementById("container"), rootMargin: "0px 0px 0px 0px", threshold: [1.0] }; observer; constructor() {} ngAfterViewInit(): void { this.observer = new IntersectionObserver(this.callback, this.options); this.setEntry(this.item.length); } setEntry(len: number) { if (len >= this.itemlist.length) { return; } const entry = document.querySelectorAll("#item")[len - 2]; this.observer.observe(entry); } callback = (entries, observer) => { entries.forEach(entry => { if (!entry.isIntersecting) { console.log("ruby 離開鏡頭"); } else { console.log("ruby 進入鏡頭"); this.item.push(this.itemlist[this.item.length]); this.observer.unobserve(entry.target); this.setEntry(this.item.length); } // Each entry describes an intersection change for one observed // target element: // entry.boundingClientRect #ReadOnly:目標元素的矩形區域的信息 // entry.intersectionRatio #獵物的可見比例 // entry.intersectionRect #ReadOnly:獵物與root的交叉區域 // entry.isIntersecting #是否出現在鏡頭(root))中 // entry.rootBounds #ReadOnly:鏡頭(root)的資訊 // entry.target #獵物本身 // entry.time }); }; } ``` ==app.component.html== ```htmlembedded= <div id="container" class="container container-one"> <div id="item" class="item" *ngFor="let i of item"> {{i.name}} </div> </div> ``` ==app.component.css== ```css= p { font-family: Lato; } .container { border: 5px solid grey; height: 400px; width: 200px; margin: auto; overflow: auto; display: inline-block; } .item { border: 1px solid #000; height: 100px; } ```