iframe内にマークアップをレンダリングしたいときに便利なwhyframeを使ってみる

[Ateam LifeDesign Advent Calendar 2022(カレンダー2)](https://qiita.com/advent-calendar/2022/ateam-life-design)の8日目は
株式会社エイチームライフデザインの@oekazumaが担当します。

# whyframeとは
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/69c8aa6e-acd0-bf63-3999-67c6ebac78fd.png" width="600">

[whyframe](https://whyframe.dev/)を使えば、Svelte、Vue、Solid、Preact、Reactなど、あらゆるUIフレームワークのマークアップをiframe内でレンダリングすることができます。

どういうことをしたい場合に使うと便利か？
- 実際の使用例を示したコンポーネントライブラリのドキュメントを作りたい
- コンポーネントの使用例を埋め込む記事を書きたい
- StoryBookのようなものを作りたい

# 使い方
<font color="red">※今回はSvelte + Viteの例で使い方を紹介します。</font>
他の構成で試したい場合は[公式ドキュメント](https://whyframe.dev/docs/overview)を参照ください。

## インストール

まず、新しいプロジェクトを作成して、依存関係をインストールします。
```bash
$ npm create vite@latest whyframe-demo -- --template svelte
$ cd whyframe-demo
$ npm install
```

次に`whyframe`を使うために必要なパッケージをインストールします。

```bash
$ npm install -D @whyframe/core @whyframe/svelte
```

### 執筆時のバージョン
- @whyframe/core： 0.1.9
- @whyframe/svelte：0.1.5

## 設定
`vite.config.js`に設定を記述していきます。

:::note warn
注意
`whyframe`はコンパイル済みコードではなく生のフレームワークコードを前処理する必要があるので`whyframeSvelte()`は`svelte()`の前に必ず記述するようにしてください。
:::

```diff_javascript:vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
+ import { whyframe } from '@whyframe/core';
+ import { whyframeSvelte } from '@whyframe/svelte';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
+   whyframe(),
+   whyframeSvelte()
    svelte(),
  ],
});
```

## 使い方

`App.svelte`に以下のようにコードを書きます。
`<iframe>`に`data-why`を加えるだけでiframe内にマークアップをレンダリングすることができます。

```html:App.svelte
<iframe data-why>
  Hello World!
</iframe>
```

ちゃんとHello World!が表示されてます！

![スクリーンショット 2022-12-06 12.27.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/dcda8c69-4817-dc3b-f96b-35b7c395cb7a.png)

次は、コンポーネントを表示してみましょう。

ポップアップのコンポーネントを作って表示してみます。

<details><summary>Popup.svelteのコード</summary>

```html:Popup.svelte
<script>
  export let content;
  let open = false;
</script>

<button
  class="activator"
  aria-haspopup="dialog"
  on:click={() => (open = !open)}
>
  <slot />
</button>

{#if open}
  <button class="background" on:click={() => (open = false)}>
    <p>{content}</p>
  </button>
{/if}

<style>
  .activator {
    display: block;
    border-radius: 8px;
    border: 1px solid transparent;
    padding: 0.5em 1em;
    font-size: 0.9em;
    font-weight: 500;
    font-family: inherit;
    color: #efefef;
    background-color: #1e1e1e;
    cursor: pointer;
    transition: border-color 0.25s;
  }

  .activator:hover {
    border-color: #ffed24;
  }

  .activator:focus,
  .activator:focus-visible {
    outline: 4px auto -webkit-focus-ring-color;
  }

  .background {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: 0;
    background-color: #090909;
    color: #efefef;
    font-size: 1.5rem;
    opacity: 0.8;
    z-index: 1;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .background:focus,
  .background:focus-visible {
    border: 4px solid #ffed24;
  }
</style>

```
</details>


```html:App.svelte
<script>
  import Popup from './Popup.svelte'
  const content = 'Hoozah!'
</script>

<iframe data-why>
  <Popup {content}>Open</Popup>
</iframe>
```

ポップアップが表示され、動作も確認できました！
コンポーネントにpropsもきちんと渡せています。

![popup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/55e06514-5ef2-6956-4cbc-6c16226f45e9.gif)

### HTMLソースを変更する

`whyframe`でレンダリング時に使用するHTMLソースを指定して変更することができます。
別のHTMLを作成して、HTMLソースを変更してみます。

<details><summary>other.htmlのコード</summary>

```html:other.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Whyframe - Other</title>
    <style>
      *,
      *::before,
      *::after {
        box-sizing: border-box;
      }

      html,
      body {
        font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
        width: 100%;
        height: 100%;
        padding: 0;
        margin: 0;
      }

      body {
        background-color: #2b2b2b;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%239e9e9e' fill-opacity='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
        background-position: 2px 2px;
        padding: 0.5rem;
        color: #efefef;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import { createApp } from "whyframe:app";
      createApp(document.getElementById("app"));
    </script>
  </body>
</html>


```
</details>

```html:App.svelte
<script>
  import Popup from './Popup.svelte'
  const content = 'Hoozah!'
</script>

<iframe data-why>
  <p>Default HTML source</p>
  <Popup {content}>Open</Popup>
</iframe>

<iframe data-why src="/other.html">
  <p>Custom HTML source</p>
  <Popup {content}>Open</Popup>
</iframe>
```

使用するHTMLソースを変更できました！ 
![スクリーンショット 2022-12-06 15.08.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/dd5bf871-e862-6f24-7486-3dd85980bcf1.png)

デフォルトのHTMLソースとして使いたい場合は設定を以下のように`vite.config.js`を変更すれば大丈夫です。

```diff_javascript:vite.config.js
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { whyframe } from "@whyframe/core";
import { whyframeSvelte } from "@whyframe/svelte";
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
+   whyframe({ defaultSrc: "/other.html" }),
    whyframeSvelte(),
    svelte(),
  ],
});
```

### ソースコードを抽出する
「このソースコードを書いたらこのUIが実現できるよ！」というようなことを示したい場合に、ソースコードを抽出して表示できたら便利です。
`<iframe>`に`data-why-show-source`を追加して、`getWhyframeSource()`を使えば取得できます。

```html:App.svelte
<script>
  import Popup from "./Popup.svelte";
  import { getWhyframeSource } from "@whyframe/core/utils";
  import { onMount } from "svelte";

  const content = "Hoozah!";

  let iframe = null;
  let source = "";

  onMount(() => {
    source = getWhyframeSource(iframe);
  });
</script>

<iframe bind:this={iframe} data-why data-why-show-source>
  <Popup {content}>Open</Popup>
</iframe>

<p>Source:</p>
<pre>{source}</pre>
```

`<iframe>`内のソースコードが取得できました！ これを使えば、ドキュメントも簡単に作れそうです。

![スクリーンショット 2022-12-06 15.32.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/cd714b6e-aa34-71ba-642a-1a382b1a4f41.png)

### コンポーネント化して使い回す
複数箇所で使いたい場合、コンポーネント化して便利に使いまわしたいことがあると思います。
`whyframe`の場合、ただコンポーネント化するだけではうまく動作せず、少し設定が必要なので説明します。

まず、コンポーネントを用意します。

```html:Story.svelte
<script>
  export let title = 'iframe'
  export let src = undefined
</script>

<p>{title}</p>
<iframe data-why {title} {src}>
  <slot />
</iframe>
```

コンポーネント化したコンポーネント名を以下のように`vite.config.js`に追加します。

```diff_javascript:vite.config.js
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { whyframe } from "@whyframe/core";
import { whyframeSvelte } from "@whyframe/svelte";
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
+   whyframe({ name: 'Story' }),
    whyframeSvelte(),
    svelte(),
  ],
});
```

`Story.svelte`を`App.svelte`で呼び出して使用してみます。

```html:App.svelte
<script>
  import Popup from "./Popup.svelte";
  import Story from "./Story.svelte";
  const content = "Hoozah!";
</script>

<Story>
  <p>This is a Story component</p>
  <Popup {content}>Open</Popup>
</Story>
```

無事にレンダリングされました！

![スクリーンショット 2022-12-06 15.52.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217487/4939a686-fbb0-f0eb-18aa-e00cb16781f6.png)


# 最後に
今回はiframe内にマークアップをレンダリングするときに使うライブラリの`whyframe`を紹介してみました！
ニッチなライブラリにはなりますが、知っていると使えるタイミングが意外とあるのでは？と思ったので是非、使ってみてください！