Lit の難しさ
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ WebComponents 由来のところが多め
◆ 状態を二重に持ったりクラスを使うことになったりイベントを使うことになったり
◆ 状態を二重に持ったりクラスを使うことになったりイベントを使うことになったり
React も不満が溜まってきてるので Lit3 が出てから久々に Lit を使っています
以前は lit-html をメインにして lit-element の方はほとんど使ってませんでした
ですが 最近というか lit パッケージにまとまるようになってからは lit-element のほうがメインです
lit-html だけでも使うことはできるものの あまり使われない扱いになってます
そういうこともあって lit-element を使うようにしてました
しかし Lit は Lit で難しさがあって React のほうが良かったかもと思うところもあります
Lit いうよりは WebComponents によるものが大きいのですけど
DOM の都合に影響される部分が大きく HTMLElement を継承しないといけないのでクラスベースにもなります
例えば 属性とプロパティがあります
期待されるのは相互に変換されることですし 初期値は属性から受け取るケースもあります
Lit はある程度やってくれるのですが WebComponents ならではの面倒なところです
また属性はグローバルなものがあります
属性やプロパティは自由なものが使えてほしいですが id や title みたいな属性を作ると特別な意味を持ってしまいます
将来的な追加を考えると data-* みたいに 「-」 を入れたほうがいいのかもしれません
でもそれをすると名前が長くなって扱いづらいです
render 関数に引数はありません
React なら引数で受け取る一時的なものでも状態として扱うことになります
WebComponents の場合は標準の DOM みたいなものになるので 仕方ないですね
React みたいなものを使わず DOM 操作のみの場合は 使う側が状態を保持せず DOM の要素側でデータを持ってるわけですし
ただ使う側も Lit になると 親と子で二重に状態を持つことになります
同期させるのが面倒です
子側で持ってるなら 親で管理しなくてもいいと思うのですが 更新したいときだけ更新させようとすると render で対処できないです
render を使うなら今の状態を常に子コンポーネントに渡さないといけません
子コンポーネントの状態を〇〇にしたいというときは渡すものがありますが それ以外のときに何を渡せばいいかわからないです
例えば表示状態をプロパティで持つ子コンポーネントがあって 表示状態はいくつかあります
子コンポーネントの内部の処理で表示状態は色々変わりますが 親コンポーネントとしては表示か非表示かの 2 択しか興味がないです
表示中にどう表示されてるかの変化を監視して親コンポーネントのプロパティを更新するようなことはしたくないです
でも表示をデフォルトの表示モードとして render を行うと別の箇所の再レンダリングで表示モードがリセットされます
現在の値を子コンポーネントから取得すればできなくはないですが 直接 DOM 操作することになります
それをするなら更新を手続き型的にやるほうがやりたいことに近いです
要素を参照するために ref を使ったり querySelector を使ったりが必要です
Lit の利点が減るので 親側も Lit ならできる限りプロパティ渡しにしたいです
特定の処理の後だけ動かないなど見つけづらいバグの元になります
開閉状態を扱うコンポーネントがあるとします
閉じるボタンを押したとき 子コンポーネント側でプロパティを閉じてる状態に更新してイベントを起こします
親側はイベントをリッスンして 子コンポーネントの状態を閉じてるに更新します
これが漏れると コンポーネントは閉じているのに親は開いてる状態として扱ってしまいます
再度開こうとしても すでに開いてる扱いなので 子コンポーネントのプロパティは更新されず開けないです
live ディレクティブを使えば強制的に更新できます
しかし この場合は関係ないところの更新で再レンダリングされたときに閉じたコンポーネントが再度開いてしまいます
これを考えると状態を複数箇所で持たない React の考え方のほうが分かりづらいバグが出にくいと思います
しかし WebComponents は標準のタグのように使えるコンポーネントを作るためのものなので仕方ないところではあります
自分で作るコンポーネントでも無効な値なら更新をせず維持したり修正可能なら修正したりします
最大が 100 までのところに 120 を渡したら 100 に修正したり from と to を渡して from のほうが大きい場合は from と to を入れ替えたりです
React ならそれは関数内の変数でしかなく公開されないですが Lit の場合はプロパティ自身を更新するので扱いが違います
上に書いたように親が子に渡したつもりの値と 子コンポーネントが管理してる値が違うと更新が意図しないことになることがあるので live が必要になります
その場合は 再レンダリングのたびに不正な値なので修正を繰り返すことになります
修正時に変更イベントとして親に伝えて親のプロパティを更新するのもありですが 全部のプロパティでそれをやると管理する親側の更新処理も増えてコードが複雑化します
Lit で直接 HTML 要素を扱うときにもある問題といえばそうですが あまりスッキリとしないところです
React みたいにコンポーネントの実体が DOM に存在しないものと比べるとコストが高いです
React ほど小さくコンポーネント化するのは向いてないです
なので共通化して使い回したいところだけかなと考えてました
基本は 1 ページが 1 コンポーネントでできててもいいくらいで 標準タグにあって良さそうな機能はコンポーネントとして分けて使うような感じです
Lit は React に比べて更新がとても高速なので パフォーマンス面では別にこれでも全然問題はないです
ただ コンポーネントが大きすぎて見づらいです
Lit を使ってるプロジェクトはどうしてるんだろうと Github などを覗いてると コンポーネントだけでなくテンプレート関数としてもモジュールに切り出してるようでした
簡単なものだとこういう感じです
状態を持たなくていいところなら こういうのもありですね
Lit だと そういうものがないですが クラスなのでインスタンスが存在します
Lit が管理するプロパティに変更があったことはわかるので 変更があったときだけ 自分で適当なプロパティに代入して計算結果を保持できます
計算元のプロパティが更新されていたら再計算して 変わってなければ前回計算したプロパティの値を使います
一応 useMemo に近いものに guard ディレクティブがありますが あくまでディレクティブです
${} の埋め込み箇所でしか使えません
テンプレート外で計算処理の結果を保持するのには使えないです
ディレクティブの中で計算することもできますが 同じ値を複数箇所で使いたい場合には向いていません
メモもプロパティで行うとプロパティが増えて管理しづらくなります
単純に上から下の関数内の処理と違うので読みづらいです
また Lit でわかるのは元のプロパティの変化のみです
計算した結果が更新されたかどうかは自分で別に管理しないとわかりません
useMemo の結果を次の useMemo に使うようなことをやりづらいです
例えば こういうコンポーネントを作るとします
data が更新されると data から list を作ります
list を filter_text でフィルタした結果を画面に表示します
filter_text は直接編集せず 入力したテキストが editing_filter_text に反映され 更新ボタンが押されると filter_text に反映します
data から list の変換や list のフィルタ処理を毎回したくないのでメモしたいです
willUpdate で list や filterd_list という Lit 管理外のプロパティを作ってます
2 つめの if では厳密には data じゃなくて list の変更を条件にしたいですが changed を使うのなら data にしないといけなく直感的じゃないです
自分でセッターをつけたいときに扱いづらいです
アップデートのスケジュールやデータの保存等は Lit 側に任せたいので super の getter/setter にしたいのですが これはエラーです
エラーと言っても Lit の開発モードのみ出してくれる親切機能です
Lit の初期化時に getOwnPropertyDescriptor で既存のプロパティ定義を取得してから getter/setter を置き換えています
なので直接 getter/setter をつけていても まず呼び出されるのは Lit のものです
Lit の getter/setter が内部で getOwnPropertyDescriptor で取得した getter/setter を呼び出します
上のコードだと foo プロパティへの代入時に
Lit のセッター → Example クラスに設定したセッター → super のセッター
という順で呼び出されます
このときの super は単純に自身に代入するものになります
代入されるとプロトタイプの getter/setter は無効になります
こういう問題があるのでセッターが必要な場合は自分で値の保存が必要です
更新のスケジュールは同期的な更新なら Lit がしてくれるので requestUpdate の呼び出しは不要です
例えば入力値が不正ならセットせずスキップしたり セットはするけど有効な値になるよう修正したりということがあります
修正なら更新直前に willUpdate でもいいですが 更新するかどうかの判断後なので不要でも更新がスキップされません
またセッターはプロパティごとですが willUpdate は 1 つだけです
あるプロパティを更新したら他のプロパティも更新するような依存関係があるとき willUpdate の中に if 文でセッターごとの処理を書くのはあまり見やすくないです
他には非同期で更新したいときがあります
セット時に非同期処理を行い結果が得られたら プロパティを更新します
非同期処理の待機中に画面をロード中にせず 現状の表示をキープしたい場合は 先にセットしてしまうと困るので独自のセッターが必要です
このケースでは非同期処理後に Lit のプロパティの data を更新していますが ここで Lit 管理のプロパティを更新しない場合は 再レンダリングがスケジュールされないので自分で requestUpdate が必要になります
value プロパティを外部から受け取って 内部で value を元に変換した値をプロパティとして保持しているとかがあります
外部からのアクセスのために内部的に更新があると value も更新しますが その更新で内部のプロパティまで更新されると困ります
例えばこういうのです
value が "1234,5678" みたいなものを想定しています
カンマ区切りの数値が 2 つで それぞれは 4 桁で足りない場合は 0 埋めされます
入力途中でも value に反映するのですが その反映で willUpdate の value が更新されたときの処理が行われます
このせいで入力途中でも 0 埋めされて使いづらくなります
willUpdate の処理は外部からの更新のときだけでいいのに内部の更新でも行われるのが原因です
これをやろうとすると外部公開用プロパティ (value) は実体をもたない getter/setter にするくらいしか良い方法がないです
ただそうすると Lit の機能で属性と連動させられなくなったり不便なところも出てきます
こういう点では外部からの状態の変更は DOM 的な考えでメソッド呼び出しのほうがいいのですが Lit の宣言的な方法と相性が悪いです
Lit でも同じことが可能ですが プロパティが二重になるのであまりプロパティで渡すものは増やしたくないです
また そのコンポーネントを使うのが Lit とは限らないので 標準の DOM に合わせてイベントとしたいです
その場合どこで dispatch すべきなんでしょうか
React だと props で受け取るものは基本ローカルに state として持たないので イベント時に親に通知して親で更新して再レンダリングしてもらいます
しかし Lit だと自分でもプロパティとして状態を持っています
標準の DOM らしさを考えると 親が自身のプロパティを更新するかどうかで動きを変えるのは少し違うと思います
チェックボックスのチェック状態は親が更新しなくても自動で変わります
となると更新してからの dispatch です
Lit では DOM の更新後に処理できる updated メソッドがあります
ここでイベントを起こすのがキレイに思います
ただこれだと問題があって 更新されたプロパティはわかりますが どうやって更新されたのかがわかりません
例えば change イベントを起こしたいとき
ユーザーが input に入力するなどで対応するプロパティが更新されたのか
コンポーネント内部の処理でプロパティが更新されたのか
コンポーネント外からプロパティが更新されたのか
どれなのかわからないので一律イベントを起こすことになります
でも起こしたいのはユーザーが入力した場合のみのはずです
親コンポーネントがプロパティを書き換えたのにそれでイベントが起きると無限ループになる可能性があります
実際 input.value は JavaScript から書き換えてもイベントは起こりません
となると input 要素のイベントハンドラー内になりそうです
DOM 更新前にイベントを起こすのはどうなのかなと思ったりもしましたが キャンセルを考えると こっちのほうがいいかもです
preventDefault みたいなメソッドを用意しておいて 呼び出されたらプロパティの更新をスキップすることで キャンセルできます
DOM 的にはこれでいいのですが こういう独自のメソッドを用意するのは面倒です
preventDefault を使うとブラウザ内部の動作のキャンセルと混ざります
別名でメソッドを用意するか 同名でメソッドの上書きになります
上書きならこんな感じです
自分でイベントを新規に作る場合はブラウザのデフォルト挙動とか考えなくてもいいので そのまま preventDefault を使って イベントの defaultPrevented プロパティを見てキャンセルするか判断するでも良さそうです
キャンセル関係では イベントだけ起こして自身のプロパティは更新せず 変更するなら親からプロパティを書き換えてもらう React のほうが楽ですね
ただ WebComponents として考えると Lit のように親で状態を管理するものに限らず DOM の要素単体として使うこともあるので親で状態管理することを強要する API はどうなのかなと思うのでこれのほうが良さそうです
まあそれ以前にキャンセルがいるのかというところも疑問ですけど
標準でもキャンセルできないイベントもありますし もとに戻すような特殊なことがしたいなら チェックボックスの change イベントのように 変わるたびに親で元に戻すでいいと思います
基本はコンストラクタです
ただ fetch などの非同期処理の場合はどうなのでしょう
用途による部分が多いと思います
画面に表示せず機能を使いたいならコンストラクタですし 画面に表示する以外で使わないなら connectedCallback でいいです
例えば a タグのダウンロード機能はドキュメントに接続されていなくても createElement で要素を作って click メソッドを実行するだけで使えます
それに対して execCommand や iframe の印刷とかはドキュメントに接続されてないと動作しなかったはずです
こういうものは表示しないように hidden 属性をつけてから一時的に body に追加しメソッドを呼び出します
実行後に body から削除します
こういう手順が面倒なので 単体で使う機能ならコンストラクタでやったほうがいいです
connectedCallback にすると 一旦切断してから再接続すると毎回実行されます
ソートで入れ替えるなどでも実行されるのでそれを望まないものなら Lit の firstUpdated のほうが適切です
DOM の更新後ですが 最初の 1 回だけ実行してくれます
機能だけ使うことがあるので コンストラクタで初期化するような場合でも ロード済みかどうかを知りたいので loaded みたいなプロパティがある方が便利かもしれません
画面表示がなくても成り立つ機能ならモジュールに切り出して 機能だけ使いたいならコンポーネントを通さず モジュールを直接使える方がいいかもしれませんけど
同じ感じで Lit でも固定値のときに ${} にせず "" にしたら属性部分が Lit の Part として扱われなくて 期待どおりに動いてくれませんでした
こう書くと 「.value」 は Lit ではなにもせずそのまま HTML として解釈されます
「.value」 という属性が追加されるだけです
value をプロパティとして代入する動きにはなりません
変数を使えるので動的に切り替えができます
しかし Lit ではタグ名の部分は Lit の Part として扱われないので変数を使うことはできません
html 関数での処理前に事前にテンプレートに埋め込む方法で実現できるものの 別のモジュールを使ったりで少し特殊なものになります
これについては長めになったのと一応公式なやり方が用意されてるものなので別記事に分けました
Lit で動的にタグを作る
以前は lit-html をメインにして lit-element の方はほとんど使ってませんでした
ですが 最近というか lit パッケージにまとまるようになってからは lit-element のほうがメインです
lit-html だけでも使うことはできるものの あまり使われない扱いになってます
そういうこともあって lit-element を使うようにしてました
しかし Lit は Lit で難しさがあって React のほうが良かったかもと思うところもあります
Lit いうよりは WebComponents によるものが大きいのですけど
WebComponents
一番の特徴は WebComponents を使うことですDOM の都合に影響される部分が大きく HTMLElement を継承しないといけないのでクラスベースにもなります
例えば 属性とプロパティがあります
期待されるのは相互に変換されることですし 初期値は属性から受け取るケースもあります
Lit はある程度やってくれるのですが WebComponents ならではの面倒なところです
また属性はグローバルなものがあります
属性やプロパティは自由なものが使えてほしいですが id や title みたいな属性を作ると特別な意味を持ってしまいます
将来的な追加を考えると data-* みたいに 「-」 を入れたほうがいいのかもしれません
でもそれをすると名前が長くなって扱いづらいです
状態
React なら props で受け取ったものが引数として参照できますが Lit の場合はプロパティとしてすべて保持されますrender 関数に引数はありません
React なら引数で受け取る一時的なものでも状態として扱うことになります
WebComponents の場合は標準の DOM みたいなものになるので 仕方ないですね
React みたいなものを使わず DOM 操作のみの場合は 使う側が状態を保持せず DOM の要素側でデータを持ってるわけですし
ただ使う側も Lit になると 親と子で二重に状態を持つことになります
同期させるのが面倒です
子側で持ってるなら 親で管理しなくてもいいと思うのですが 更新したいときだけ更新させようとすると render で対処できないです
render を使うなら今の状態を常に子コンポーネントに渡さないといけません
子コンポーネントの状態を〇〇にしたいというときは渡すものがありますが それ以外のときに何を渡せばいいかわからないです
例えば表示状態をプロパティで持つ子コンポーネントがあって 表示状態はいくつかあります
子コンポーネントの内部の処理で表示状態は色々変わりますが 親コンポーネントとしては表示か非表示かの 2 択しか興味がないです
表示中にどう表示されてるかの変化を監視して親コンポーネントのプロパティを更新するようなことはしたくないです
でも表示をデフォルトの表示モードとして render を行うと別の箇所の再レンダリングで表示モードがリセットされます
現在の値を子コンポーネントから取得すればできなくはないですが 直接 DOM 操作することになります
それをするなら更新を手続き型的にやるほうがやりたいことに近いです
...
render() {
return html`
<foo-bar></foo-bar>
`
}
updated(changed) {
if (condition) {
const foo_bar = this.renderRoot.querySelector("foo-bar")
foo_bar.size = "large"
}
}
...
要素を参照するために ref を使ったり querySelector を使ったりが必要です
Lit の利点が減るので 親側も Lit ならできる限りプロパティ渡しにしたいです
二重管理の問題
上の例の問題と近いですが 親で管理することにしても 更新漏れがありえます特定の処理の後だけ動かないなど見つけづらいバグの元になります
開閉状態を扱うコンポーネントがあるとします
閉じるボタンを押したとき 子コンポーネント側でプロパティを閉じてる状態に更新してイベントを起こします
親側はイベントをリッスンして 子コンポーネントの状態を閉じてるに更新します
これが漏れると コンポーネントは閉じているのに親は開いてる状態として扱ってしまいます
再度開こうとしても すでに開いてる扱いなので 子コンポーネントのプロパティは更新されず開けないです
live ディレクティブを使えば強制的に更新できます
しかし この場合は関係ないところの更新で再レンダリングされたときに閉じたコンポーネントが再度開いてしまいます
これを考えると状態を複数箇所で持たない React の考え方のほうが分かりづらいバグが出にくいと思います
しかし WebComponents は標準のタグのように使えるコンポーネントを作るためのものなので仕方ないところではあります
値の自動修正
標準の HTML 要素でも無効な値を渡したりすると自動修正されます自分で作るコンポーネントでも無効な値なら更新をせず維持したり修正可能なら修正したりします
最大が 100 までのところに 120 を渡したら 100 に修正したり from と to を渡して from のほうが大きい場合は from と to を入れ替えたりです
React ならそれは関数内の変数でしかなく公開されないですが Lit の場合はプロパティ自身を更新するので扱いが違います
上に書いたように親が子に渡したつもりの値と 子コンポーネントが管理してる値が違うと更新が意図しないことになることがあるので live が必要になります
その場合は 再レンダリングのたびに不正な値なので修正を繰り返すことになります
修正時に変更イベントとして親に伝えて親のプロパティを更新するのもありですが 全部のプロパティでそれをやると管理する親側の更新処理も増えてコードが複雑化します
Lit で直接 HTML 要素を扱うときにもある問題といえばそうですが あまりスッキリとしないところです
テンプレート関数
WebComponents だと DOM の実体があるので Lit の処理外でも内部的に色々なことが行われますReact みたいにコンポーネントの実体が DOM に存在しないものと比べるとコストが高いです
React ほど小さくコンポーネント化するのは向いてないです
なので共通化して使い回したいところだけかなと考えてました
基本は 1 ページが 1 コンポーネントでできててもいいくらいで 標準タグにあって良さそうな機能はコンポーネントとして分けて使うような感じです
Lit は React に比べて更新がとても高速なので パフォーマンス面では別にこれでも全然問題はないです
ただ コンポーネントが大きすぎて見づらいです
Lit を使ってるプロジェクトはどうしてるんだろうと Github などを覗いてると コンポーネントだけでなくテンプレート関数としてもモジュールに切り出してるようでした
簡単なものだとこういう感じです
const article = (article) => html`
<div>
<h1>${article.title}</h1>
<p>${article.body}</p>
</div>
`
状態を持たなくていいところなら こういうのもありですね
メモ
React だと useMemo をつかって重たい処理の結果をメモできますLit だと そういうものがないですが クラスなのでインスタンスが存在します
Lit が管理するプロパティに変更があったことはわかるので 変更があったときだけ 自分で適当なプロパティに代入して計算結果を保持できます
計算元のプロパティが更新されていたら再計算して 変わってなければ前回計算したプロパティの値を使います
一応 useMemo に近いものに guard ディレクティブがありますが あくまでディレクティブです
${} の埋め込み箇所でしか使えません
テンプレート外で計算処理の結果を保持するのには使えないです
ディレクティブの中で計算することもできますが 同じ値を複数箇所で使いたい場合には向いていません
メモもプロパティで行うとプロパティが増えて管理しづらくなります
単純に上から下の関数内の処理と違うので読みづらいです
また Lit でわかるのは元のプロパティの変化のみです
計算した結果が更新されたかどうかは自分で別に管理しないとわかりません
useMemo の結果を次の useMemo に使うようなことをやりづらいです
例えば こういうコンポーネントを作るとします
data が更新されると data から list を作ります
list を filter_text でフィルタした結果を画面に表示します
filter_text は直接編集せず 入力したテキストが editing_filter_text に反映され 更新ボタンが押されると filter_text に反映します
data から list の変換や list のフィルタ処理を毎回したくないのでメモしたいです
class Example extends LitElement {
static properties = {
editing_filter_text: {},
filter_text: {},
data: {},
}
onClickUpdateButton() {
this.filter_text = this.editing_filter_text
}
willUpdate(changed) {
if (changed.has("data")) {
this.list = toList(this.data)
}
if (changed.has("data") || changed.has("filter_text")) {
this.filtered_list = this.list.filter(item => item.text.includes(this.filter_text))
}
}
render() {
return html`
...
<input
.value=${this.editing_filter_text}
@input=${event => this.editing_filter_text = event.target.value}
>
<button @click=${this.onClickUpdateButton}>Update</button>
...
<div>
${this.filterd_list.map(item => html`
<div><span>${item.id}</span><span>${item.text}</span></div>
`)}
</div>
...
`
}
}
willUpdate で list や filterd_list という Lit 管理外のプロパティを作ってます
2 つめの if では厳密には data じゃなくて list の変更を条件にしたいですが changed を使うのなら data にしないといけなく直感的じゃないです
セッター
Lit が管理するプロパティは自動で getter/setter が作られます自分でセッターをつけたいときに扱いづらいです
アップデートのスケジュールやデータの保存等は Lit 側に任せたいので super の getter/setter にしたいのですが これはエラーです
エラーと言っても Lit の開発モードのみ出してくれる親切機能です
class Example extends LitElement {
static properties = {
foo: {},
}
get foo() {
return super.foo
}
set foo(value) {
super.foo = value
}
}
Lit の初期化時に getOwnPropertyDescriptor で既存のプロパティ定義を取得してから getter/setter を置き換えています
なので直接 getter/setter をつけていても まず呼び出されるのは Lit のものです
Lit の getter/setter が内部で getOwnPropertyDescriptor で取得した getter/setter を呼び出します
上のコードだと foo プロパティへの代入時に
Lit のセッター → Example クラスに設定したセッター → super のセッター
という順で呼び出されます
このときの super は単純に自身に代入するものになります
代入されるとプロトタイプの getter/setter は無効になります
こういう問題があるのでセッターが必要な場合は自分で値の保存が必要です
更新のスケジュールは同期的な更新なら Lit がしてくれるので requestUpdate の呼び出しは不要です
セッターが欲しいとき
セッターって結構つけたいことが多いので 値の保存だけといっても面倒です例えば入力値が不正ならセットせずスキップしたり セットはするけど有効な値になるよう修正したりということがあります
修正なら更新直前に willUpdate でもいいですが 更新するかどうかの判断後なので不要でも更新がスキップされません
またセッターはプロパティごとですが willUpdate は 1 つだけです
あるプロパティを更新したら他のプロパティも更新するような依存関係があるとき willUpdate の中に if 文でセッターごとの処理を書くのはあまり見やすくないです
他には非同期で更新したいときがあります
セット時に非同期処理を行い結果が得られたら プロパティを更新します
非同期処理の待機中に画面をロード中にせず 現状の表示をキープしたい場合は 先にセットしてしまうと困るので独自のセッターが必要です
get value() {
return this._value
}
set value(v) {
asyncSomething(v).then(data => {
this._value = v
this.data = data
})
}
このケースでは非同期処理後に Lit のプロパティの data を更新していますが ここで Lit 管理のプロパティを更新しない場合は 再レンダリングがスケジュールされないので自分で requestUpdate が必要になります
外部からの更新のときだけ処理したい
また セッターに関連して外部からの更新のときだけ追加で処理をしたいことがありますvalue プロパティを外部から受け取って 内部で value を元に変換した値をプロパティとして保持しているとかがあります
外部からのアクセスのために内部的に更新があると value も更新しますが その更新で内部のプロパティまで更新されると困ります
例えばこういうのです
...
willUpdate(changed) {
if (changed.has("value")) {
const [one, two] = this.value.split(",").map(x => x.padStart(4, "0").slice(0, 4))
this._one = one
this._two = two
}
}
onChangeOne(event) {
this._one = event.target.value
this.value = [this._one, this._two].map(x => x.padStart(4, "0").slice(0, 4)).join(",")
}
...
value が "1234,5678" みたいなものを想定しています
カンマ区切りの数値が 2 つで それぞれは 4 桁で足りない場合は 0 埋めされます
入力途中でも value に反映するのですが その反映で willUpdate の value が更新されたときの処理が行われます
このせいで入力途中でも 0 埋めされて使いづらくなります
willUpdate の処理は外部からの更新のときだけでいいのに内部の更新でも行われるのが原因です
これをやろうとすると外部公開用プロパティ (value) は実体をもたない getter/setter にするくらいしか良い方法がないです
ただそうすると Lit の機能で属性と連動させられなくなったり不便なところも出てきます
こういう点では外部からの状態の変更は DOM 的な考えでメソッド呼び出しのほうがいいのですが Lit の宣言的な方法と相性が悪いです
イベント
親に更新があったことを伝える方法ですが React なら子コンポーネントに props として関数を渡してこれを呼び出してもらいますLit でも同じことが可能ですが プロパティが二重になるのであまりプロパティで渡すものは増やしたくないです
また そのコンポーネントを使うのが Lit とは限らないので 標準の DOM に合わせてイベントとしたいです
その場合どこで dispatch すべきなんでしょうか
React だと props で受け取るものは基本ローカルに state として持たないので イベント時に親に通知して親で更新して再レンダリングしてもらいます
しかし Lit だと自分でもプロパティとして状態を持っています
標準の DOM らしさを考えると 親が自身のプロパティを更新するかどうかで動きを変えるのは少し違うと思います
チェックボックスのチェック状態は親が更新しなくても自動で変わります
となると更新してからの dispatch です
Lit では DOM の更新後に処理できる updated メソッドがあります
ここでイベントを起こすのがキレイに思います
...
updated(changed) {
if (changed.has("value")) {
this.dispatchEvent(new Event("changed"))
}
}
...
ただこれだと問題があって 更新されたプロパティはわかりますが どうやって更新されたのかがわかりません
例えば change イベントを起こしたいとき
ユーザーが input に入力するなどで対応するプロパティが更新されたのか
コンポーネント内部の処理でプロパティが更新されたのか
コンポーネント外からプロパティが更新されたのか
どれなのかわからないので一律イベントを起こすことになります
でも起こしたいのはユーザーが入力した場合のみのはずです
親コンポーネントがプロパティを書き換えたのにそれでイベントが起きると無限ループになる可能性があります
実際 input.value は JavaScript から書き換えてもイベントは起こりません
となると input 要素のイベントハンドラー内になりそうです
DOM 更新前にイベントを起こすのはどうなのかなと思ったりもしましたが キャンセルを考えると こっちのほうがいいかもです
preventDefault みたいなメソッドを用意しておいて 呼び出されたらプロパティの更新をスキップすることで キャンセルできます
DOM 的にはこれでいいのですが こういう独自のメソッドを用意するのは面倒です
preventDefault を使うとブラウザ内部の動作のキャンセルと混ざります
別名でメソッドを用意するか 同名でメソッドの上書きになります
上書きならこんな感じです
...
onValueChange(eve) {
const value = eve.target.value
if (value === this.value) return
const event = new CustomEvent("will-change", { detail: value })
let cancel = false
event.preventDefault = () => {
cancel = true
}
this.dispatchEvent(event)
if (cancel) {
this.requestUpdate()
} else {
this.value = value
}
}
...
自分でイベントを新規に作る場合はブラウザのデフォルト挙動とか考えなくてもいいので そのまま preventDefault を使って イベントの defaultPrevented プロパティを見てキャンセルするか判断するでも良さそうです
キャンセル関係では イベントだけ起こして自身のプロパティは更新せず 変更するなら親からプロパティを書き換えてもらう React のほうが楽ですね
ただ WebComponents として考えると Lit のように親で状態を管理するものに限らず DOM の要素単体として使うこともあるので親で状態管理することを強要する API はどうなのかなと思うのでこれのほうが良さそうです
まあそれ以前にキャンセルがいるのかというところも疑問ですけど
標準でもキャンセルできないイベントもありますし もとに戻すような特殊なことがしたいなら チェックボックスの change イベントのように 変わるたびに親で元に戻すでいいと思います
非同期初期化
これは Lit というより WebComponents ならではの部分ですが 初期化をどこでするかです基本はコンストラクタです
ただ fetch などの非同期処理の場合はどうなのでしょう
用途による部分が多いと思います
画面に表示せず機能を使いたいならコンストラクタですし 画面に表示する以外で使わないなら connectedCallback でいいです
例えば a タグのダウンロード機能はドキュメントに接続されていなくても createElement で要素を作って click メソッドを実行するだけで使えます
それに対して execCommand や iframe の印刷とかはドキュメントに接続されてないと動作しなかったはずです
こういうものは表示しないように hidden 属性をつけてから一時的に body に追加しメソッドを呼び出します
実行後に body から削除します
こういう手順が面倒なので 単体で使う機能ならコンストラクタでやったほうがいいです
connectedCallback にすると 一旦切断してから再接続すると毎回実行されます
ソートで入れ替えるなどでも実行されるのでそれを望まないものなら Lit の firstUpdated のほうが適切です
DOM の更新後ですが 最初の 1 回だけ実行してくれます
機能だけ使うことがあるので コンストラクタで初期化するような場合でも ロード済みかどうかを知りたいので loaded みたいなプロパティがある方が便利かもしれません
...
constructor() {
super()
this.message = null
this.loaded = new Promise((resolve, reject) => {
asyncSomething().then(
message => {
this.message = message
resolve()
},
error => {
reject(error)
}
)
})
}
...
画面表示がなくても成り立つ機能ならモジュールに切り出して 機能だけ使いたいならコンポーネントを通さず モジュールを直接使える方がいいかもしれませんけど
プレフィックス付き属性には ${} が必須
難しいというかハマりがちな罠ですが React の感覚だと props が静的な文字列なら {} にせず "" でいいです同じ感じで Lit でも固定値のときに ${} にせず "" にしたら属性部分が Lit の Part として扱われなくて 期待どおりに動いてくれませんでした
html`
<foo-bar .value="a"></foo-bar>
`
こう書くと 「.value」 は Lit ではなにもせずそのまま HTML として解釈されます
「.value」 という属性が追加されるだけです
value をプロパティとして代入する動きにはなりません
動的なタグ
React の JSX では HTML のタグ名に当たる部分は変数としての関数です変数を使えるので動的に切り替えができます
しかし Lit ではタグ名の部分は Lit の Part として扱われないので変数を使うことはできません
html 関数での処理前に事前にテンプレートに埋め込む方法で実現できるものの 別のモジュールを使ったりで少し特殊なものになります
これについては長めになったのと一応公式なやり方が用意されてるものなので別記事に分けました
Lit で動的にタグを作る