◆ イベント起きたときに指定要素のメソッド呼びだしするのが難しい
◆ hyperHTML に更新中に行うなら プロパティに引数を入れて setter 経由で更新くらい
◆ プロパティが同じだと更新されないので同じ引数で 2 連続実行できない
◆ メソッド実行後にまたイベント起こしてメソッド呼び出し用のプロパティリセットすれば一応できる
◆ なんか無理矢理感もあってどうにかしたい方法

これまで

hyperHTML や lit-html など値を元に必要箇所のみ書き換えるツールはすごく便利です
イベントごとに自分で一つ一つ書き換えていたら漏れが出てきますが イベントは問わずデータが変更されたらその時のデータを元に可変部分だけの差分だけ書き換えるので常に値に基づいた値になります

ただ完全にこれだけに頼りづらい部分もあります
すでに他の記事でもいくつか書きましたが だいたいは解消して たぶんこれが今の所思いつく最後の問題です

これまでにあったものは まずパフォーマンスです
全部比較して差分を探すので DOM の要素が何千とあると重そうというものです
でも実際のところは遅いのはレンダリングで DOM 自体を書き換えたり参照するのは高速です
なので差分探す程度なら数千要素くらいどうということはないです
innerHTML を 1 から作っても 1 秒未満ですし 1000 件超えるようならちょっとした変更でも場所によってはレンダリングで数秒かかるのでそっちに比べると気にする必要もないくらいです

次が 全部状態管理したくない場合や DOM を直接操作したい場合です
フォーム系などで常に変更を同期するのが面倒な場合や ドラッグドロップで並び替えるなど既存ライブラリが DOM を直接操作する場合とか replaceWith で新規コンポーネントに置き換えたいときなどです
ライブラリが内部で参照を持っていたりするので管理外のところでの書き換えは正常に動作しなくなったりエラーになったりします
しかし 管理してる要素自身を変更しなければ問題ありません
hyperHTML のテンプレートではカスタム要素を作るだけにしておけば内側は管理されません
コンポーネントの中はそのコンポーネントが独自に管理できます
外部からの状態の更新は hyperHTML で設定されるプロパティの setter で設定されたデータに応じて更新ができます

メソッドを呼び出したい

で 今回のものですが メソッド呼び出ししたいことがあります

イベントがあったときに一回だけメソッドを呼び出したいということはよくあります
グローバルな alert や print ならイベントリスナの中で 状態を更新して再描画を始める前にそこで実行してしまえます
データに変更がないならそこでのメソッド呼び出しだけで再描画も不要です

ですがコンポーネントのメソッドとなると shadow DOM が使われていて深い階層にあると要素を見つけるのも一苦労です
できれば hyperHTML などの子要素更新の過程で済ませたいです

とりあえず setter が呼び出されるとでセットされる値でメソッドを呼び出すものを作ってみました

<!doctype html>
<script src="https://cdn.jsdelivr.net/npm/hyperhtml@2.13.0/index.js"></script>

<body>
<script>
customElements.define("x-elem1", class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
this.onClick = this.onClick.bind(this)
}

connectedCallback() {
this.render()
}

render(data = {}) {
hyperHTML.bind(this.shadowRoot)`
<input type="text"><button onclick="${this.onClick}">btn</button>
<span>${data.count}</span>
<x-elem2 data="${data.exec}"></x-elem2>
`
}

set data(value) {
console.log("eve")
this.render(value)
}

onClick() {
const msg = this.shadowRoot.querySelector("input").value
this.dispatchEvent(new CustomEvent("exec", { detail: msg, bubbles: true }))
}
})

customElements.define("x-elem2", class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" }).innerHTML = `
<style>
:host {
position: relative;
overflow: hidden;
display: block;
width: 200px;
height: 30px;
background: lemonchiffon;
}

div{
position: absolute;
top: 0;
height: 30px;
font-size: 18px;
line-height: 30px;
color: red;
}
</style>
`
}

set data(value) {
value && this.run(value)
}

run(msg) {
const div = document.createElement("div")
div.textContent = msg
this.shadowRoot.append(div)
const player = div.animate([{ marginLeft: 0 }, { marginLeft: "100%" }], 1000)
player.onfinish = eve => {
div.remove()
}
}
})

!function () {
let data = { count: 0, exec: null }

function render() {
hyperHTML.bind(document.body)`
<x-elem1 data="${{ ...data }}"></x-elem1>
`
}

document.body.addEventListener("exec", eve => {
data.exec = eve.detail
data.count++
render()
})

render()
}()
</script>
</body>

x-elem の中には input と x-elem2 があって x-elem2 では run メソッドに文字列を渡すとその文字がアニメーションで流れます
x-elem で input に入力してボタンを押すとデータが更新されて x-elem2 の setter が呼び出されます
setter では run メソッドを呼び出すので 入力したテキストが x-elem2 の中を流れます

Demo

問題点

一見良さそうなのですが この手のライブラリは余計な更新を避けるために変更がないときはなにもしません
基本的には良いことなのですが今回みたいなプロパティの更新をメソッド呼び出しに関連付けてしまうと相性がよくないです

今回の場合だと 同じメッセージを 2 回流そうとしても流れません

プロパティをもとにメソッド呼び出しをする以上 これを解決するには実行後に値をクリアするしか無いと思います
とは言っても hyperHTML で子要素を更新している最中にデータを更新したくありません
hyperHTML はあくまで view 部分でデータをもとにその状態の DOM に更新するという処理だけにしたいです

それに更新しようにも元のデータはグローバルじゃないので渡された文字列しか参照できませんし 双方向バインディングではなくて親から子への一方向の setter 経由の通知なので親に変更を伝えることもできません
できるのは 通常のクリックなどイベントが起きたときにそれを伝える仕組みだけです
今回のものでは DOM 経由でイベントを起こして親要素でイベントを受け取るようにしています

メソッド実行後に これを呼び出してクリアすることはできます
ただ それだと残りの要素を hyperHTML で更新するより先に次のイベントによる変更で再更新が始まります
その更新が終わったあとに 続きの最初のイベントでの残った部分の更新が起きます
イベント的には メッセージボタン押す ▶ メソッド実行したのでクリア という流れなのに一部は先に起きた方のイベントの状態で更新されてしまいます

分かりづらいのでコードでいうと

set data(value){
if(value){
this.run(value)
this.dispatchEvent(new Event("method-done"))
// ↑の処理で method-done 後の再更新が実行される
// 終わってからここから残りが実行されるけど ここよりあとに更新される要素は
// method-done イベントで更新されたあとに再度古いイベントでの更新になる
}
}

という感じです
更新元のデータが参照型でどっちが先に起きても常に同じ値になるなら問題はないですが あんまり気持ちの良い方法でもないです

ということでイベントを起こすのを非同期にして メッセージ更新での変更が完全に全体に届いたあとにイベントが起きるようにします

set data(value){
if(value){
this.run(value)
Promise.resolve().then(() => this.dispatchEvent(new Event("method-done")))
}
}

一応これで大丈夫とは思うのですが プロパティでメソッド呼び出しを管理して 呼び出し直後に呼び出しましたって言うイベントを受けてプロパティをクリアってやり方がなんかいまいち良い方法とは思えないのですよね
呼び出しを行うイベントと呼び出し終わったという 2 回もイベントが起きますし

もっといい方法はないのでしょうか