核心做法

 1var mql = window.matchMedia('(min-width: 1400px)');
 2function place() {
 3  if (mql.matches) {
 4    desktopSlot.appendChild(filter);
 5  } else {
 6    drawer.insertBefore(filter, drawer.firstChild);
 7  }
 8}
 9mql.addEventListener('change', place);
10place();  // 初始化

同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。


這個做法存在的價值

Stateful UI(內含 checkbox 勾選、表單值、scroll 位置等 state)跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。

搬同一份節點 = state 永遠跟著節點走 = 切換無感。


適合的情境

情境為什麼合理
Filter UI 跨 viewport 切換顯示位置checkbox state 跟著節點
Modal 內容 vs 側邊抽屜同一份表單在兩種展示方式間
Tab UI 跨 desktop / mobile 重新組織各 tab 內 state 不重置
任何「同 UI、不同位置」的 responsive 切換不需要 state 同步邏輯

核心特徵:UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。


不適合的情境

情境為什麼不夠改用
兩個位置展示的是不同元件(雖然視覺類似)搬遷會把錯誤元件搬到錯位置各自獨立掛載、不搬
UI 純 stateless(純圖示、純文字)複製兩份成本低、無 state 風險CSS-only 雙顯示 + display 切換
Framework 管的節點整節點搬安全、但複製不安全(id duplicate / framework 困惑)必須搬整節點、不複製
兩個位置視覺差異大搬遷後 UI 不適配新位置各自獨立元件

設計細節

appendChild 是搬遷、不是複製

1parentA.appendChild(node);  // node 從原位置消失、出現在 parentA

DOM API 的 appendChild / insertBefore 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。

初始放在哪

1<!-- 預設位置(mobile / fallback)-->
2<div class="pagefind-ui">
3  <div class="drawer">
4    <div class="filter-panel">...</div>  <!-- 初始在這 -->
5  </div>
6</div>
7
8<!-- 桌面 slot(空、等待搬入)-->
9<aside class="desktop-filter-slot"></aside>

預設放在 fallback 位置 — 當 JS 失敗時仍可見。

跨 slot 切換的時機

matchMedia event 是 viewport 跨過 breakpoint 的瞬間:

1var mql = window.matchMedia('(min-width: 1400px)');
2mql.addEventListener('change', place);
3place();  // 初始也跑一次

不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。matchMedia 只在 cross 的瞬間觸發。

搬遷時 framework 的 reactivity

如果搬遷的節點是 framework 管的(如 Pagefind 的 svelte 元件)— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。

詳細安全規則由 #13 JS 操作 framework 元件:邊界辨識與安全規則 處理。

Focus 跟著搬

搬遷可能讓鍵盤 focus 暫時失去(視瀏覽器)— 加 save/restore:

1function place() {
2  var activeBefore = document.activeElement;
3  if (mql.matches) desktopSlot.appendChild(filter);
4  else drawer.insertBefore(filter, drawer.firstChild);
5  if (activeBefore && filter.contains(activeBefore)) {
6    activeBefore.focus();
7  }
8}

詳細處理由 #37 動態 DOM 移動時的 focus 管理 處理。


設計取捨:兩個 slot 的 stateful UI 共用

四種做法、各自機會成本不同。預設選 A(搬同節點)、其他做法在特定情境合理。

A:搬同一節點(這個專案的預設)

  • 機制matchMedia + appendChild 在兩 slot 間搬同一份節點
  • 選 A 的理由:state 跟著節點、切換無感、不需要 sync 邏輯
  • 適合:stateful UI、需要在兩個位置展示同樣內容
  • 代價:搬遷 callback 在 viewport 跨 breakpoint 時觸發、需要處理 focus / 動畫
  • 詳細:本卡片

B:CSS-only 雙顯示 + display 切換

  • 機制:兩個位置都放同一份節點 (寫兩遍 HTML)、用 @media + display: none 切換顯示
  • 跟 A 的取捨:B 純 CSS 簡單、A 需要 JS;但 B 對 stateful UI 失敗(兩份 state 各自獨立)
  • B 比 A 好的情境:UI 純 stateless(純圖示)、純 CSS 解就夠

C:CSS-only + JS 同步 state

  • 機制:兩份節點 + JS 監聽 state 變動同步
  • 跟 A 的取捨:C 比 B 解 state 問題、但同步邏輯複雜(雙向更新、避免循環)
  • C 比 A 好的情境:兩個位置的 UI 視覺需要差異(不只是位置不同)

D:JS 完全重建 UI

  • 機制:viewport 變動時拆掉舊 UI、在新位置重建一份
  • 成本特別高的原因:state 在重建時遺失、UI 閃爍、輸入中斷
  • D 才合理的情境:UI 是 stateless 的、且重建成本低

跟其他 pattern 的關係

#14 Selector 精準度 的「起點」維度有四種做法、本卡片是「跨 slot 搬遷」這個專門情境的補充:

議題對應 pattern
Query 的起點#46 document / #47 元件根變數 / #48 起點當參數 / #49 closest 反向
Idempotency 過濾#50 attribute 標記 / #51 WeakMap
跨 slot 搬遷(本卡片)同節點 vs 雙節點 + state 同步

應用範例:跨 viewport filter 切換

 1function setupResponsiveFilter(shell, breakpoint) {
 2  var filter = shell.querySelector('.pagefind-ui__filter-panel');
 3  var drawer = shell.querySelector('.pagefind-ui__drawer');
 4  var desktopSlot = document.querySelector('.search-filter-slot');
 5
 6  if (!filter || !drawer || !desktopSlot) return;
 7
 8  var mql = window.matchMedia('(min-width: ' + breakpoint + 'px)');
 9
10  function place() {
11    var activeBefore = document.activeElement;
12
13    if (mql.matches) {
14      desktopSlot.appendChild(filter);
15    } else {
16      drawer.insertBefore(filter, drawer.firstChild);
17    }
18
19    if (activeBefore && filter.contains(activeBefore)) {
20      activeBefore.focus();
21    }
22  }
23
24  place();
25  mql.addEventListener('change', place);
26}

完整 pattern:取元件根 + matchMedia + 搬遷 + focus 處理。


判讀徵兆

訊號該套用本 pattern 嗎?
兩份節點各自 state、用 sync 邏輯保持一致是 — 改成搬同節點、移除 sync
Stateful UI 在 mobile / desktop 兩種 layout 間是 — 直接的應用
切換 viewport 時 UI 閃爍 / 重建是 — 改成搬而非重建
兩個位置展示完全不同的 UI(不是同邏輯)否 — 各自獨立元件
Framework 管的節點是 — 整節點搬安全、但要遵守 #13 的規則

核心原則:Stateful UI 的兩個展示位置共用同一份節點、state 自然跟著走 — 比「兩份節點 + sync 邏輯」乾淨。複製兩份是「state 來源從一變二」的隱形多源(違反 #44 SSoT)。