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 を使うかでしょうか