背景
SycamoreというRustとWebAssemblyでWebアプリを作るライブラリがあります。
Node.jsでいうReactやVueみたいなものです(言い方が雑で怒られそうかもしれない……)
このSycamoreでは、純粋に生のHTMLを扱う場合はdangerously_set_inner_html
という属性を使うだけで十分です。
例えば
view!{
div(
dangerously_set_inner_html=inner_html,
)
}
↑このようにするだけで生のHTMLをそのまま出力することができます。
一方で、この方法では、この例のdiv
のように、何かその生のHTMLの親にあたる要素も一緒に作らなければならないという不自由がありました。
やること
今回は、簡単そうな状況に限定して、ちょうど1つだけの要素を表現する生のHTMLを、親の要素で包まずに返すcomponentを作ることに挑戦します。
結論
こんな感じでcomponentを作りました。
#[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を追加するコード例を示します。
[package]
name="sycamore-raw-html"
version="0.0.0"
edition="2021"
[dependencies.sycamore]
version="^0.9.0-beta.2"
<!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>
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
});
}
次のコマンドでサーバーを立ち上げてブラウザの開発者ツールで確認します。
trunk serve
上記画像を見ると、意図したとおり生のHTMLとして入力したspan
が親の要素として追加したdiv
で囲まれていることがわかります。
本記事で提案するコンポーネントを使う場合
では次に、以下に本記事で提案するコンポーネントを使った場合の最小限の構成で生のHTMLを追加するコード例を示します。
今度は上記コード例のうち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
});
}
先程と同様にサーバーを立ち上げて開発者ツールで確認します
上記画像を見ると、今度は意図したとおり生のHTMLとして入力したspan
がbody
の直下に追加されていることがわかります。
応用について
このコンポーネントの特長は、生のHTMLを追加の親要素で囲むことなく、そのまま出力できる点です。これは、例えばCSSの要件で親要素で囲むことが適切でない場合に役立ちます。具体例としては、display: flex;
の都合で親要素を追加できない場合や、 >
で直接の子要素を指すセレクタを使いたい場合などが考えられます。このような場合に、本記事で提案するコンポーネントが有用であると考えられます。
心配なところ
今回はinner_html
の中は一番祖先の要素はちょうど1つだけになる場合を扱いました。
しかし、一般には一番祖先の要素が複数ある場合や1つもない場合も考えられますが、そのような場合にもこの方法で機能するかという心配をしています。
そのような場合があれば別途チェックしようと思います。