はいさい!ちゅらデータのオースティンやいびーん!
概要
Web Componentsの作成を助けてくれるLitでShadow DOMではなく、Light DOMにレンダーする部品を作る方法を紹介します!
背景
LitElementで部品を作ると、デフォルトでShadow DOMを追加し、そのShadow DOMにHTMLエレメント、CSSのスタイルなどをレンダーしていきます。
しかし、Shadow DOMではなく、Light DOMにレンダーしたい場合があります。
- グローバルCSSを適応したい時
- ログインフォームでブラウザのキーマネージャーと相性良く付き合いたい時(Chrome、SafariはShadow DOMに対してパスワードの自動入力・保存ができないのです)
- Light DOMの<form>に入る入力部品を作りたい時
こういう時に以下の方法でLight DOMにレンダーしていただけます!
コード
LitElementは、customElement.defineでブラウザに登録された時に、内蔵化されているLitElement.createRenderRootを実行して、部品のHTMLをどこに入れるかを定めます。
この関数を以下のように上書きすると、this.shadowRootではなく、thisをRenderRootに指定することができます。
import { LitElement, html } from "lit";
export default class LitComponent extends LitElement {
protected createRenderRoot() {
return this; // Light DOMを使うように
// thisがLight DOMの<lit-component>のエレメントに当たります
}
#handleSubmit: EventListener = (event) => {
event.preventDefault();
};
render() {
return html`
<h1>Lit Form</h1>
<form @submit=${this.#handleSubmit}>
<label for="name">名前</label>
<input type="text" id="name" name="name" />
</form>
`;
}
}
これで、この部品はLight DOMにレンダーされるようになります!
実際にブラウザで見てみよう。
Webpackなどのバンドラーのエントリーポイントで上記のLitComponentを読み込んで、customElements.defineでブラウザに登録します。
import LitComponent from "./lit-component"
customElements.define("lit-component", LitComponent);
また、HTMLのテンプレートに<lit-element>を入れます。また、Light DOMの<style>タグでh1の文字の色を赤にしています。
<lit-element>内の<h1>が赤くなっていれば、Light DOMにレンダーできていることがわかります。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<style>
h1 {
color: red;
}
</style>
<lit-component></lit-component>
</body>
</html>
そして、InspectorでHTMLを見てみると、
Shadow DOMはなく、Light DOMにレンダーされていることが確認できます。
注意点: LitElementのstylesが使えなくなる!
このように、Light DOMにレンダーすると、通常のLitElementのCSSを指定するstatic styles = css``が使えなくなります。
通常は、以下のように書きます。
import { LitElement, html, css } from "lit";
export default class LitComponent extends LitElement {
#handleSubmit: EventListener = (event) => {
event.preventDefault();
};
static styles = css`
input {
height: 40px;
}
`;
render() {
return html`
<h1>Lit Form</h1>
<form @submit=${this.#handleSubmit}>
<label for="name">名前</label>
<input type="text" id="name" name="name" />
</form>
`;
}
}
しかし、上記のLight DOMをRenderRootに指定する方法だと、static stylesが反映されません。
なぜなら、実は、LitElement.createRenderRootでは、デフォルトで以下のようなコードを実行しているからです。
import { LitElement, html, css, adoptStyles } from "lit";
...
protected createRenderRoot() {
this.attachShadow({ mode: "open" });
adoptStyles(this.shadowRoot!, [LitComponent.styles])
return this.shadowRoot;
}
このcreateRenderRootを上書きすると、lit.adoptStylesがなくなってしまいます。もっと残念な知らせですが、このadoptStylesにthisだけを渡すことができないようです。
これで、以下のような工夫が必要になります。
import { LitElement, html, css } from "lit";
export default class LitComponent extends LitElement {
protected createRenderRoot() {
return this; // Light DOMを使うように
// thisがLight DOMの<lit-component>のエレメントに当たります
}
#handleSubmit: EventListener = (event) => {
event.preventDefault();
};
render() {
const styles = css`
input {
height: 40px;
}
`;
return html`
<style>${styles}</style>
<h1>Lit Form</h1>
<form @submit=${this.#handleSubmit}>
<label for="name">名前</label>
<input type="text" id="name" name="name" />
</form>
`;
}
}
まとめ
これで、LitElementをShadow DOMからLight DOMにレンダーするように指定する方法を紹介してきましたが、いかがでしょうか?
次の記事では、Shadow DOMとLight DOMの両方にレンダーする方法を紹介します!