CustomCategories 插件作業(進階版)
===
<div style="position: relative; padding-bottom: 56.25%; height: 0;"><iframe src="https://jumpshare.com/embed/HhncWqIJ9Jk4llL9PFSu" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>
---
## 這不是插件:
這是我自己研究和 base on React的格式去撰寫的一組slider。
上一章是說基本款。接下來這裡將介紹進階版:
《**點選以後發生Rerender將回到點選的位置**》
如果上一章還沒看呢,建議點擊[這裡](https://hackmd.io/@myrealstory/SyAbmHae0)看看上一章。
## 概念講解:
- 要先記錄每一個按鈕的位置,供點擊時使用。
- 當點擊了按鈕以後,取得ID然後馬上取得當下按鈕資訊,,並且馬上記錄到存放的地方。
- 當回到這個頁面的時候,嘗試讀取是否有記錄,如果有就馬上轉移到該按鈕的位置,這個方式適用 desktop 或 mobile 版本。
## 先做讀取每個按鈕的位置:
```jsx
const [buttonPosition, setButtonPosition] = useState<Record<number, {id:number, position:number}>| null>(null);
useEffect(()=>{
if(sliderRef.current) {
const position: Record<number,{id:number,position:number}>= {};
data.forEach((value,index) =>{
const button = sliderRef.curent!.children[index] as HTMLDivElement;
position[value.id] = {id:value.id, position: (button.offsetWidth + 8 ) * index};
});
setButtonPosition(position);
}
},[data]);
```
1. Record<TKey,TType> 是 TS一個泛型工具類型,用來創建一個物件類型,其中物件的key是"TKey" 類型,且每個鍵對應的值(value)是 TType類型。這裡的Record用來確定 buttonPosition 可以是一個 object,然後object的每個key 都是 number類型,且每個key 都會映射到TType裡。
2. 然後建立一個臨時變數來記錄運算過程。整合以後再丟到setState裡面以便記錄。
3. 這裡選擇forEach的原因是因為需要針對data陣列中的每一項元素執行操作,不需要製造新的array,所以靠foreach讀取就好。
4.
```jsx
const button = sliderRef.current!.children[index] as HTMLDivElement;
positions[value.id] = { id: value.id, position: (button.offsetWidth + 8) * index };
```
- button 變數記錄從 sliderRef.current 的子元素中取得對應的按鈕元素,根據 index。
- 計算每個按鈕的位置,位置是基於按鈕的寬度加上一定的間隔乘與以它的index.
- 將每個按鈕的 id 和計算以後的 position 存儲到 positions的object裡。
5. 然後更新 setButtonPosition。
6. 每當data有更新,就會執行一次。(客戶很愛對比現在和CMS更新以後是否能即時反應)
## 點擊以後,尋找當下的位置記錄:
```jsx
const handleButtonClick = (id:number) =>{
if(buttonPosition === null) return;
const positions = buttonPosition[id] ? buttonPosition[id].position : null;
const {containerWidth} = calculateRefPosition();
const boxSize = sliderRef?.current?.lastElementChild?.clientWidth;
const currentRoll = position && boxSize ? Math.floor((position + boxSize)/containerWidth) : 0;
const storageObject = {
position : positions,,
currentRoll: currentRoll,
id:id,
width:width,
}
if(position !== null) sessionStorage.setItem("categoryPosition", JSON.stringify(storageObject));
}
```
1. 如果 buttonPosition 沒有東西,那這個功能就廢掉了。
2. 然後建立一個變數用來記錄當下傳入 handleButtonClick 的id ,但也要看buttonPositions是否有漏掉的,如果沒有就回傳 null。
3. 接下來就取得 顯示大小。
4. 然後計算當下的所屬區間,如果位置不超過顯示位置,自然在0號區間,如果超出了,就滑到當下的區間因此而記錄。
5. 如如果資料準備好,然後上傳到 sessionStorage 裡。
6. (update 4/23) 追加 boxSize,因為這裡統計的position 並不包含所有盒子的寬度,只是盒子的最左邊的。所以還要加上盒子的長度,才會成為完成的sliderRef 長度(這裡指的是最後一個盒子。)
---
## 重點!
```jsx
const width = window?.visualViewport?.width ?? 0;
useEffect(()=>{
const currentCategory = JSON.parse(sessionStorage.getItem("categoryPosition") || "{}");
const storedItemId = currentCategory.id || 0;
const storeWidth = currentCategory.width || 0;
if(storedWidth !== width) {
handleButtonClick(storedItemId);
}
const storedPOosition = currentCategory.positions || 0;
const storedRoll = currentCategory.currentRoll || 0;
const {containerWidth, lastItemPosition,, numRolls} = calculateRefPosition();
const containerSlider = containerRef.current;
let currentPosition = 0;
if(storedPosition !== 0 && containerSlider && sliderRef.current && width > 0) {
if(width < 768) {
setTimeout(()=>{
containerSlider.scrollTo({
left: storedPosition,
behavior: "smooth",
})
},100);
}else{
if(parseInt(storePosition) > containerWidth && numRolls > storedRoll){
currentPosition = containerWidth * (Math.floor(parseInt(storedPosition)/ containerWidth));
}
if(numRolls === storeRoll) {
currentPosition = lastItemPosition - contianerWidth;
}
sliderRef.current.style.transform = `translateX(-${currentPosition}px)`;
setCurrentRoll(storedRoll);
}
}
},[buttonPosition, width]);
```
1. 這裡是需要re-render過程時要及時觸發的。
2. width的部分呢,會在另一個章節介紹我自己寫的 customHook ,用來統計目前熒幕高寬和目前scrollY的高度 和 某些客制化用來隱藏bar的機能。
3. 但目前先用直接取 window width 做法。
4. 先做好在sessionStorage裡面值的分類。
5. 這裡先做個小小的預防,防客戶很愛拉來拉去去做比較,所以寫了個保險:如果熒幕寬度被改變的話,那就重新取值,再跑一次。
6. 如果 storedPosition 不是0,在讀取到 div 和 顯示寬度大於 0,這裡要注意:**因為window 的 visualViewport 一開始都讀不到,所以必定是0,等讀到了寬度,再執行**。
7. 當熒幕是mobile web的時候,因為改用scroll,所以我們要控overflow-x-auto 的那個div,然後進行scrollto 的動作。這裡需要注意的是,要等所有的值都準備好了,才scroll,故設計 setTimeout延後執行。
8. 到desktop web 的部分,我們只要回到那個區間就好。所以 只要點擊的位置比熒幕還要大的話,就把哪個區間的位置傳到 currentPosition 裡。
9. 如果 最大的區間跟現在區間是一樣的話,那就最大的長度減去熒幕寬度。如圖:

那就可以如設想一樣移動到最後一格。
10. 最後記得要更新現在的roll,以便接下來有其他的動作。不然就會出現意想不到的結果。
---
## 總結:
我覺得這個slider並沒有很完善,也因為是這次作業被要求要有這樣的效果。但我覺得可以按照這樣的思考方向去找到出路,畢竟想要完成slider,先了解底層運作和設計才能做出更好的 hook 和效果! 大家加油和感謝把文章看完!希望大家都有收穫。
