###### tags: `Angular` `InfiniteScroll` `IntersectionObserver`
# 🌝[T]Infinite Scroll #IntersectionObserver

## 無限滾輪 (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;
}
```