Sycamoreで生のHTMLを親の要素で包まずに返すcomponentを作った


## 背景

SycamoreというRustとWebAssemblyでWebアプリを作るライブラリがあります。

https://sycamore-rs.netlify.app/

Node.jsでいうReactやVueみたいなものです（言い方が雑で怒られそうかもしれない……）

このSycamoreでは、純粋に生のHTMLを扱う場合は`dangerously_set_inner_html`という属性を使うだけで十分です。

例えば

```rust
view!{
    div(
        dangerously_set_inner_html=inner_html,
    )
}
```

↑このようにするだけで生のHTMLをそのまま出力することができます。

一方で、この方法では、この例の`div`のように、何かその生のHTMLの親にあたる要素も一緒に作らなければならないという不自由がありました。

## やること

今回は、簡単そうな状況に限定して、***ちょうど１つだけの要素***を表現する生のHTMLを、親の要素で包まずに返すcomponentを作ることに挑戦します。

## 結論

こんな感じでcomponentを作りました。

```rust
#[component(inline_props)]
pub fn RawHtml<G:Html>(
	inner_html:String,
)->View<G>{
	let container:View<G>=view!{
		div(
			dangerously_set_inner_html=inner_html.clone(),
		)
	};
	let node=container
		.as_node()
		.unwrap()
		.first_child()
		.unwrap();
	View::new_node(node)
}
```

## 説明

まず順に説明します。

### `dangerously_set_inner_html`

これは特別な属性で、Vueでいうところの`v-html`と同じような働きをします。

### `.first_child`

これで先程`div`の中に入れた`inner_html`を`GenericNode`を実装する型にして取り出します。

### `View::new_node`

これで先程取り出した`GenericNode`な型になった`inner_html`を改めて`View<G>`の型にします。

## 実演

では具体的な最小の構成で上記コンポーネントを使う場合とそうでない場合を比較したいと思います

### 本記事で提案するコンポーネントを使わない場合

まず、以下に本記事で提案するコンポーネントを使わない場合の最小限の構成で生のHTMLを追加するコード例を示します。

```toml:Cargo.toml
[package]
name="sycamore-raw-html"
version="0.0.0"
edition="2021"

[dependencies.sycamore]
version="^0.9.0-beta.2"
```

```html:index.html
<!doctype html>
<html>
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<link data-trunk rel="rust" href="./Cargo.toml" />
	</head>
</html>
```

```rust:src/main.rs
use sycamore::prelude::*;

#[component]
fn App<G:Html>()->View<G>{
	view!{
		div(
			dangerously_set_inner_html=r#"<span style="color:red;">raw html</span>"#,
		)
	}
}

fn main(){
	sycamore::render(||view!{
		App
	});
}
```

次のコマンドでサーバーを立ち上げてブラウザの開発者ツールで確認します。

```bash:ターミナルで実行
trunk serve
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782801/15103a4e-3a82-80f5-6749-da21f35c1697.png)

上記画像を見ると、意図したとおり生のHTMLとして入力した`span`が親の要素として追加した`div`で囲まれていることがわかります。

### 本記事で提案するコンポーネントを使う場合

では次に、以下に本記事で提案するコンポーネントを使った場合の最小限の構成で生のHTMLを追加するコード例を示します。

今度は上記コード例のうち`src/main.rs`のみ変更します。以下に全文を示します。

```rust:src/main.rs
use sycamore::prelude::*;

#[component(inline_props)]
pub fn RawHtml<G:Html>(
	inner_html:String,
)->View<G>{
	let container:View<G>=view!{
		div(
			dangerously_set_inner_html=inner_html.clone(),
		)
	};
	let node=container
		.as_node()
		.unwrap()
		.first_child()
		.unwrap();
	View::new_node(node)
}

#[component]
fn App<G:Html>()->View<G>{
	view!{
		RawHtml(
			inner_html=r#"<span style="color:red;">raw html</span>"#.to_string(),
		)
	}
}

fn main(){
	sycamore::render(||view!{
		App
	});
}
```

先程と同様にサーバーを立ち上げて開発者ツールで確認します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782801/18496d9c-0973-b473-d1e2-ee07378f201c.png)

上記画像を見ると、今度は意図したとおり生のHTMLとして入力した`span`が`body`の直下に追加されていることがわかります。

## 応用について

このコンポーネントの特長は、生のHTMLを追加の親要素で囲むことなく、そのまま出力できる点です。これは、例えばCSSの要件で親要素で囲むことが適切でない場合に役立ちます。具体例としては、`display: flex;`の都合で親要素を追加できない場合や、 `>` で直接の子要素を指すセレクタを使いたい場合などが考えられます。このような場合に、本記事で提案するコンポーネントが有用であると考えられます。

## 心配なところ

今回は`inner_html`の中は一番祖先の要素はちょうど１つだけになる場合を扱いました。
しかし、一般には一番祖先の要素が複数ある場合や１つもない場合も考えられますが、そのような場合にもこの方法で機能するかという心配をしています。
そのような場合があれば別途チェックしようと思います。