hotwire turbo-frame自作

# **自作で Hotwire Turbo Frame を作ろう！**  

HotwireのTurbo-Frameって知ってます？ページ全体をリロードせず、フレーム単位で更新できる便利な仕組みなんだけど、「これ、自分で作れたらカッコよくない？」って思ったことないですか？今回は **自作 Turbo Frame** を作る方法を解説するよ！  

この記事では、**「Nova Frame」** と名付けたカスタム要素を作りながら学んでいきます！  

---

## **Nova Frame の完成イメージ**  

- **部分更新**: ページ全体ではなく、指定したフレームだけを更新します。  
- **リンクとフォーム対応**: リンククリックやフォーム送信で中身を更新。  
- **XSS 対策**: セキュリティ対策もしっかり対応。  

結果的に、ページ全体をリロードすることなく、まるでモダンな SPA（シングルページアプリケーション）のような動きを実現します。  

---

## **Step 1: HTML を準備しよう**  

まずは、`nova-frame` を動かすための基本的な HTML を書きます。  

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Nova Frame デモ</title>
</head>
<body>
  <h1>Nova Frame ハンズオン</h1>

  <!-- Nova Frame の対象エリア -->
  <nova-frame id="main-frame">
    <p>ここがフレームの中身になります。</p>
    <a href="/example1.html">リンク 1</a>
    <a href="/example2.html">リンク 2</a>
  </nova-frame>

  <!-- JavaScript 読み込み -->
  <script type="module" src="nova-frame.js"></script>
</body>
</html>
```

**`nova-frame` タグ** は自作のカスタム要素。この中身だけが部分的に更新されるようにします。  

---

## **Step 2: Nova Frame の基礎を作ろう**  

ここから JavaScript を書いていきます。まずは、カスタムタグを作る準備をします。  

### カスタムタグの作成  

```javascript
class NovaFrame extends HTMLElement {
  constructor() {
    super();
    this.urls = []; // 内部リンクを保存する配列
    this.id = this.getAttribute("id"); // フレームの ID を取得
  }

  connectedCallback() {
    // nova-frame が DOM に追加されたときに呼ばれる
    const childElements = this.querySelectorAll("a"); // 子要素の <a> を取得
    childElements.forEach((element) => {
      this.urls.push({ url: element.href, element }); // URL を保存
    });
    this.aTagFetch(); // クリックイベントを設定
  }

  async aTagFetch() {
    this.urls.forEach((obj) => {
      obj.element.addEventListener("click", async (e) => {
        e.preventDefault(); // ページ遷移を無効化
        try {
          document.dispatchEvent(loadingEvent); // ローディングイベント発火
          const response = await fetch(obj.url); // URL を取得
          const result = await response.text();

          const parser = new DOMParser();
          const doc = parser.parseFromString(result, "text/html");
          const NovaFrame = doc.querySelector(`nova-frame[id="${this.id}"]`);
          if (NovaFrame) {
            this.innerHTML = NovaFrame.innerHTML; // 中身を更新
            this.connectedCallback(); // 再度イベントを設定
            document.dispatchEvent(loadEvent); // ロードイベント発火
          }
        } catch (e) {
          console.error("エラー:", e);
        }
      });
    });
  }
}

// カスタム要素を定義
customElements.define("nova-frame", NovaFrame);
```

---

## **Step 3: イベントで状態を管理しよう**  

ページの読み込み中や読み込み完了をイベントで通知してみましょう！  

### 独自イベントの設定  

```javascript
const loadEvent = new Event("nova-frame:load"); // 読み込み完了イベント
const loadingEvent = new Event("nova-frame:loading"); // 読み込み中イベント

document.addEventListener("nova-frame:loading", () => {
  console.log("nova-frame:loading イベント発火中...");
});

document.addEventListener("nova-frame:load", () => {
  console.log("nova-frame:load イベント完了！");
});
```

これで、更新のタイミングで何か処理を加えたいときに簡単に対応できるようになります。

---

## **Step 4: フォーム送信にも対応する**  

部分更新はリンクだけじゃない！フォーム送信にも対応してみましょう。  

### フォーム送信の処理  

```javascript
const forms = document.querySelectorAll("form[data-nova-frame-id]");

forms.forEach((form) => {
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const frameId = form.getAttribute("data-nova-frame-id");
    const novaFrame = document.querySelector(`nova-frame[id="${frameId}"]`);

    const formData = new FormData(form);
    const escapedData = new URLSearchParams();
    formData.forEach((value, key) => {
      escapedData.append(key, escapeHTML(value)); // XSS 対策
    });

    try {
      const response = await fetch(form.action, {
        method: form.method,
        body: escapedData,
      });
      const result = await response.text();

      const parser = new DOMParser();
      const doc = parser.parseFromString(result, "text/html");
      const newFrame = doc.querySelector(`nova-frame[id="${frameId}"]`);
      if (newFrame) {
        novaFrame.innerHTML = newFrame.innerHTML; // 内容を更新
      }
    } catch (e) {
      console.error("エラー:", e);
    }
  });
});
```

---

## **Step 5: 実際に試してみよう！**  

リンクをクリックしたときにフレームが切り替わる動きや、フォーム送信による部分更新を試してみてね！  

---

## **補足: セキュリティ対策**  

XSS 対策も忘れずに！以下の関数でフォームデータをエスケープします。  

```javascript
function escapeHTML(str) {
  return str.replace(/[&<>'"]/g, (tag) => {
    const charsToReplace = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      "'": "&#39;",
      '"': "&quot;",
    };
    return charsToReplace[tag] || tag;
  });
}
```

---

## **まとめ**  

これで **「Nova Frame」** を使った部分更新の仕組みが完成です！カスタム要素の力で Turbo Frame に似た動きを実現できましたね。