以下の内容はhttps://let.blog.jp/tag/lit-elementより取得しました。


Lit で CSS-in-JS したい
Lit の 3.0 が正式リリースされたので久々に Lit を使っています
React で CSS-in-JS ライブラリを使っていると Lit でスタイルを適用したいときに少し不便です
一応 styleMap という directive があるのですが 単純に style 属性を設定するものです
最近ブラウザで使えるようになったネストするセレクタは書けません

やりたいものはこういうのです

customElements.define("foo-bar", class extends LitElement {
render() {
return html`
<div
class=${style`
border: 2px solid #aaa;
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input>
</div>
`
}
})

div の class のところでスタイルを書いて 自動生成されるクラス名が class に渡されて設定されるというものです
だいたい Emotion です
ただ そこまで複雑なことはしなくてもいいです
ネストはブラウザ側の機能で実現できるので 単純にクラス名を作って設定するだけでいいです
それなら自分で簡単にできるかなと試してました

LitElement のクラスでは styles という静的プロパティを用意しておくことで 自動でそのスタイルをコンポーネントに適用してくれる機能があります
しかし こういう動的に更新するものには使えないです
なので自分で adoptedStyleSheets を追加します
コンストラクタのタイミングでは ShadowRoot を作ってくれないので createRenderRoot をオーバーライドして ここで追加します
あとは style 関数が呼び出されたタイミングで中身を更新します

customElements.define("foo-bar", class extends LitElement {
constructor() {
super()
this._cssss = new CSSStyleSheet()
}
createRenderRoot() {
const root = super.createRenderRoot()
root.adoptedStyleSheets.push(this._cssss)
return root
}
style(template, ...values) {
const class_name = "c"
this._cssss.replaceSync(`.${class_name} {${String.raw(template, ...values)}}`)
return class_name
}
render() {
return html`
<div
class=${this.style`
border: 2px solid #aaa;
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input>
</div>
`
}
})

これで動くようになりました
ただこれだと最後に呼び出された style 関数の分しかスタイルを保持していません
replaceSync の代わりに insertRules にすれば追加できますが 増えていく一方です

同じ場所の更新ならルールを更新できるといいのですけど
テンプレートリテラルの機能を使ってみようかとも思いました
しかし あくまでソースコード上の同じ場所ということしかわかりません
CSSStyleSheet が CustomElement ごとなので 別インスタンスと混ざることはないですが map で繰り返すようなケースは対応できません

Lit の directive が使えるかと思いましたが directive は lit-html 側の機能なので LitElement と統合されていないです
CustomElement の参照は得られず 対応する Part の更新だけに焦点が当てられています
また Part が消えた場合の処理を記述する方法も用意されていません

いい方法がみつからないので とりあえず全部更新する方法にしました
update をオーバーライドすれば render の前後に処理を入れられるので 更新時に使ったルールだけを残し 他は削除します
スタイルの更新のための処理が増えてきたので 間にクラスを設けることにしました

class StylitElement extends LitElement {
constructor() {
super()
this._cssss = new CSSStyleSheet()
this._style_num = 0
this._style_used_rules = new Set()
}
createRenderRoot() {
const root = super.createRenderRoot()
root.adoptedStyleSheets.push(this._cssss)
return root
}
style(template, ...values) {
const class_name = "c" + (this._style_num++).toString(36)
const index = this._cssss.insertRule(`.${class_name} {${String.raw(template, ...values)}}`)
this._style_used_rules.add(this._cssss.cssRules.item(index))
return class_name
}
update() {
this._style_num = 0
this._style_used_rules.clear()
super.update()
const rules = [...this._cssss.cssRules]
for (const rule of rules.toReversed()) {
if (!this._style_used_rules.has(rule)) {
const index = rules.indexOf(rule)
this._cssss.deleteRule(index)
}
}
}
}

このクラスを使います
複数の style を使って map の中でも使うようにしています
また再レンダリングを兼ねて input の入力を全体で同期するようにしました

customElements.define("foo-bar", class extends StylitElement {
static properties = {
name: {},
}
constructor() {
super()
this.name = ""
}
render() {
return html`
<div class=${this.style`display: flex; flex-flow: column; gap: 10px;`}>
<div
class=${this.style`
border: 2px solid #aaa;
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input .value=${this.name} @input=${event => this.name = event.target.value}>
</div>
<div
class=${this.style`
border: 2px solid #f8d;
padding: 20px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input .value=${this.name} @input=${event => this.name = event.target.value}>
</div>
${["#f00", "#0f0", "#00f"].map(color => {
return html`
<div
class=${this.style`
border: 2px solid ${color};
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input .value=${this.name} @input=${event => this.name = event.target.value}>
</div>
`
})}
</div>
`
}
})

とりあえず動くようになりましたが render の呼び出しのたびに style で作るルールは全部新規に作り 古いのは消すという処理です
あんまりパフォーマンス的に優れてそうにないです
でもこれ以上がんばるならもう Emotion 使ったほうがいいような気もしてきます
ただ Emotion にしても同じルールが存在するかを確認して差分更新をするのでそこまで改善されるのかは疑問です

簡単にできそうで効率良くなるかもってところだと やっぱりルールの更新方法でしょうか
差分更新はがんばらないにしても もう少しなんとかできるかもと思います
CSSStyleSheet の削除って index 指定でしか消せず ひとつひとつ index を取得してから消すのって効率悪そうです
ルールの全置き換えなら 最初から新規の CSSStyleSheet を作って それと置き換えてしまう方がよいかもしれないです
それか 配列でルールの文字列を保持して最後に replaceSync を使うかでしょうか
lit-html のリポジトリが lit に移されてる
以前の URL (https://github.com/Polymer/lit-html) にアクセスすると https://github.com/lit/lit に飛ばされる
Polymer プロジェクトの一部だったのが lit という独自のプロジェクトに分かれたみたい

リポジトリは monorepo になって lit リポジトリの中に lit-html や lit-element が入ってる
こっちで管理されるのは時期メジャーアップデートで lit-html 2.0 と lit-element 3.0 らしい
他にも SSR やスターターキットなどのパッケージもある

lit-html と lit-element はそれぞれパッケージに分かれてるけど lit というパッケージもできてこれだけで lit-html と lit-element の両方が使えるみたい

公式サイトも新しくなってる
https://lit.dev/

公式サイト内のドキュメントと同じ内容かもだけど リポジトリの Wiki には lit2.0 の説明やアップグレードガイドが用意されてる
https://github.com/lit/lit/wiki/About-Lit-2.0
lit-element でリスナはコンポーネント内メソッドの方がいい?
子コンポーネントから親コンポーネントのメソッドを呼び出すために関数をプロパティで渡す時
子コンポーネント側で変換やチェック処理がいらないなら 親から受け取ったのをそのままリスナに設定してる
こういう感じ

import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"

customElements.define("elem-1", class extends LitElement {
onClickButton = () => {
console.log("CLICKED")
}

render() {
return html`
<elem-2 .onClickButton=${this.onClickButton}></elem-2>
`
}
})

customElements.define("elem-2", class extends LitElement {
static properties = {
onClickButton: { type: Function },
}

render() {
return html`
<button @click=${this.onClickButton}>button</button>
`
}
})

document.body.append(document.createElement("elem-1"))

最初に受け取ったときや変更されたときに render 処理が必要だから properties で定義が必要

リスナ用の受け取る関数の変更で rerender をさせたくないので リスナにはコンポーネント内のメソッドを設定するといいのかも
受け取る関数は properties 定義してないただのプロパティとして受け取る

import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"

customElements.define("elem-1", class extends LitElement {
onClickButton = () => {
console.log("CLICKED")
}

render() {
return html`
<elem-2 .onClickButton=${this.onClickButton}></elem-2>
`
}
})

customElements.define("elem-2", class extends LitElement {
_onClickButton = (eve) => {
this.onClickButton(eve)
}

render() {
return html`
<button @click=${this._onClickButton}>button</button>
`
}
})

document.body.append(document.createElement("elem-1"))

properties に定義いらないし rerender を減らせる
だけど lit-element だと React の関数コンポーネントと違って毎回リスナ関数を作り直さない
親側で動的に関数を作ったりしてない限りは変更がないので 初回以外に rerender が発生しないことが多そう

個人的には properties は減らしたいけど どっちもどっちなのかも



以上の内容はhttps://let.blog.jp/tag/lit-elementより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14