◆ input イベントで入力したものを削除すると
◆ テンプレートに埋め込むデータは前回のものと一緒になる
◆ 差分なしとみなされて input は更新されず消されるべき文字が画面に残る

input で oninput ハンドラで入力を制限することってありますよね
半角英数字しか許可しないところではひらがなとかが入力されるごとに削除して画面上にはでなくしたり
コピペしても使える文字だけが残ったり

そんなことを hyperhtml と lit-html でやろうとするとちょっと困ったことになります

hyperhtml

数字だけ打てる input を 2 種類作りました
(1) は普通にやった場合で (2) は (1) の問題に対処した場合です

import { bind } from "https://unpkg.com/hyperhtml@2.30.3/esm/index.js?module"

let text1 = ""
let text2 = ""

const render = () =>
bind(document.body)`
<p>number only</p>
(1)<input oninput=${onInput1} value=${text1}>
(2)<input oninput=${onInput2} value=${text2}>
`

const onInput1 = eve => {
text1 = eve.target.value.replace(/[^\d]/g, "")
render()
}

const onInput2 = eve => {
text2 = eve.target.value
render()
text2 = text2.replace(/[^\d]/g, "")
render()
}

render()

(1) では普通に input イベントで修正してからテンプレートに埋め込む変数にセットしてます
hyperhtml では無駄な処理をへらすためにテンプレートの各埋め込み場所で 前回の値を保持していて異なる場合のみ DOM の更新を行います
input に数字以外が入力されると replace で消してからセットされます

"123abc456" と順番に打つと abc も打ててしまい 4 を入力したタイミングで 1234 となります
a を入力した時点では内部的には 123a ではなく 123 になっていますが それだと前回と同じ値なので変更なしとみなされ input の value は更新されません
その結果 次に差分が出る 4 の入力まで画面上は入力できない文字が見えていることになります

内部的にはちゃんと abc は消えていますが 画面と違うのって結構問題です
123abc まで入力してユーザが送信しようとした場合 画面では 123abc なのに 123 で保存されます
チェックしようとするなら わざわざ input を取得して内部的に持っている値と同じかチェックすることになります
hyperhtml や lit-html では input のデータを変数上にすでに持っているのがメリットなのに 直接 DOM 操作するときのように送信時に要素を取得してデータを確認するのではそのメリットを活かせてません

それに対処したのが (2) です
変化があると判断させるために禁止文字を消す前に実際に入力された内容で render を実行します
これ自体では何も起きず hyperhtml が管理する内部の値を更新するだけです
その後にデータを修正してもう一度 render を実行すると差分ありとみなされて DOM が更新されます

lit-html

lit-html でも同じく差分の有無をチェックする仕組みがあるので全く同じでした
対処方法も一緒です

import { html, render } from "https://unpkg.com/lit-html@1.1.2/lit-html.js?module"

let text1 = ""
let text2 = ""

const update = () =>
render(
html`
<p>number only</p>
(1)<input @input=${onInput1} .value=${text1}>
(2)<input @input=${onInput2} .value=${text2}>
`,
document.body
)

const onInput1 = eve => {
text1 = eve.target.value.replace(/[^\d]/g, "")
update()
}

const onInput2 = eve => {
text2 = eve.target.value
update()
text2 = text2.replace(/[^\d]/g, "")
update()
}

update()

lit-element

lit-element になるとさらに対処が複雑になります

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

customElements.define("my-element", class extends LitElement {
static get properties() {
return {
text: { type: String },
}
}

constructor() {
super()
this.text = ""
}

onInput(eve) {
this.text = eve.target.value
Promise.resolve().then(() => {
this.text = eve.target.value.replace(/[^\d]/g, "")
})
}

render() {
return html`
<input @input=${this.onInput} .value=${this.text}>
<p>${this.text}</p>
`
}
})

document.body.append(document.createElement("my-element"))

lit-element の場合は 定義したプロパティを更新することで 自動で render が実行されます
ただし 非同期で遅延して実行されます
なんどもプロパティを更新しても そのイベントの処理が全部終わってから DOM 更新に入るので無駄がないのですが その分 lit-html のような方法は使えません
そこで Promise.resolve().then(...) を使って 2 度目の更新も次のマイクロタスクにして非同期実行にします
this.text がセットされたところで DOM 更新を行うタスクがセットされるので そのあとで Promise.resolve.then(...) でセットしているので 2 度目の更新がスケジュールされるのは 1 度目の実際の更新の後になります


色々不便なところもありますがとりあえず画面と一致しない問題は回避できました
input の入力制限なんてありがちなものだと思うので もうちょっと良い方法が提供されてもいいと思うのですけどねー
テンプレートリテラルの埋め込み時に差分確認無視して絶対更新してくれるオプションがあるといいのに