◆ 差分がなくても PropertyPart を更新する
◆ directive を作って PropertyPart のインスタンスを直接操作する
◆ PropertyPart を継承して差分チェックをせずに常に更新する Part を作る
  ◆ その Part をテンプレートから生成するために TemplateProcessor も作る必要あり
  ◆ サンプルでは . の代わりに ! から始まる属性は常に更新するプロパティ定義にした

前の記事では input の値を更新するためにプリミティブ値でもオブジェクトとして扱うようにしました
ですがあんまり良い方法ではないです
boolean 値は対応できませんでしたし

楽に済ませたかったのですが 簡単にできる方法だと限界があるので今回はちゃんとした方法を使います

方法

プロパティの更新処理を実行するには PropertyPart (実装は継承元の AttributePart) の setValue の if 内の処理を実行すればよいです
ですがこの if 文の条件に入るには非プリミティブ値か前回のキャッシュと値が異なる必要があります
その他オプションで制御できないので 普通の方法ではここの処理を変更できません

ライブラリのソースコードをいじらないなら方法は 2 つ
directive を使う方法と自分で Part を定義する方法です

directive

簡単さでは directive のほうが上です
directive は ${} の中に directive 関数で作った値を入れます
その値を元に directive は Part を更新します
自作 directive も作れるので 自分で directive を作れば Part を直接操作できます

Part の操作では そのまま setValue を呼んでも強制することはできないので setValue の if 内の処理を直接実行してしまうのが簡単です
コード量も少ないのでコピペしてしまえばおっけいです
将来的に setValue の処理が追加されることも考えると part.value の前回の値を変えてしまって必ず前回と値が異なる状況にしてしまうのも手かもしれません

こういう感じです

	const d = directive(value => part => {
part.value = {}
part.setValue(value)
})

render(html`<input .value=${d(text)}>`, document.body)

通常のと比較用

比較用サンプルです
テキストボックスとセレクトボックスとチェックボックスがあって 入力時に特殊な制御をします
テキストボックスは数字のみ入力できるようにします
セレクトボックスは C を選ぶと強制的に A に変更するようにします
チェックボックスは常にチェック状態になるようにします
それらの下には 内部で保持している実際の値を表示します

上の normal の方は普通にプロパティ設定してるので 下に出ている実際の値と画面上の値で違いが出ます
下の extend は directive 機能を使って毎回 DOM プロパティを更新してるので input と下に出ている内部の値が一致しています

<div id="normal"></div>
<div id="extend"></div>

<script type="module">
import { html, render, directive } from "https://unpkg.com/lit-html?module"

const init = (host, tpl) => {
const values = {
text: "",
select: "1",
checked: true,

onChangeText(eve) {
// only number textbox
values.text = eve.target.value.replace(/[^0-9]/g, "")
rerender()
},
onChangeSelect(eve) {
// select A when C is selected
values.select = eve.target.value.replace("3", "1")
rerender()
},
onChangeChecked(eve) {
// always true
rerender()
},
}

const rerender = () => render(tpl(values), host)
rerender()
}

init(normal, values => html`
<div>
<input type="text" .value=${values.text} @input=${values.onChangeText}>
<select .value=${values.select} @change=${values.onChangeSelect}>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>
<input type="checkbox" .checked=${values.checked} @change=${values.onChangeChecked}>
</div>
<div>${values.text},${values.select},${values.checked}</div>
`)

const d = directive(value => part => {
part.value = {}
part.setValue(value)
})

init(extend, values => html`
<div>
<input type="text" .value=${d(values.text)} @input=${values.onChangeText}>
<select .value=${d(values.select)} @change=${values.onChangeSelect}>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>
<input type="checkbox" .checked=${d(values.checked)} @change=${values.onChangeChecked}>
</div>
<div>${values.text},${values.select},${values.checked}</div>
`)
</script>

独自 Part

lit-html では Part 自体を自分で作ることができます
PropertyPart の setValue では値をチェックして差分がないと更新されませんでしたが 自分で差分関係なく常に更新する版の Part を作れば directive を使わなくても毎回更新できます
ほとんどは PropertyPart のままで setValue の中身だけ変えればいいので PropertyPart を継承して作ります
それと PropertyPart のインスタンスを作る PropertyCommitter もあるのでこっちも同じように継承した版を用意します

自分で作った Part を使うには TemplateProcessor も作る必要があります
属性名が @ から始まったらイベントリスナで . から始まったらプロパティといったことを判断して Part を返す処理を行う部分です
全ての PropertyPart を置き換えてしまうのは無駄な更新を抑える機能が使えなくなってもったいないです
なので 新しい属性ルールを作って それを常に更新する版の PropertyPart にします
「.」 の代わりに 「!」 から始まったら 常に更新する版のプロパティとします
そこ以外は defaultTemplateProcessor の処理のままです

この独自のテンプレートを作れる html タグ関数を export する JavaScript ファイルはこうなりました

import {
isDirective,
noChange,
TemplateResult,
AttributeCommitter,
BooleanAttributePart,
EventPart,
NodePart,
PropertyPart,
PropertyCommitter,
} from "https://unpkg.com/lit-html?module"

class AUPropertyCommitter extends PropertyCommitter {
_createPart() {
return new AUPropertyPart(this)
}
}

class AUPropertyPart extends PropertyPart {
setValue(value) {
if (value !== noChange) {
this.value = value
if (!isDirective(value)) {
this.committer.dirty = true
}
}
}
}

const template_processor = {
handleAttributeExpressions(element, name, strings, options) {
const prefix = name[0]
if (prefix === "!") {
const committer = new AUPropertyCommitter(element, name.slice(1), strings)
return committer.parts
}
if (prefix === ".") {
const committer = new PropertyCommitter(element, name.slice(1), strings)
return committer.parts
}
if (prefix === "@") {
return [new EventPart(element, name.slice(1), options.eventContext)]
}
if (prefix === "?") {
return [new BooleanAttributePart(element, name.slice(1), strings)]
}
const committer = new AttributeCommitter(element, name, strings)
return committer.parts
},

handleTextExpression(options) {
return new NodePart(options)
},
}

const html = (strings, ...values) => new TemplateResult(strings, values, "html", template_processor)
export default html

これを使うサンプルです

サンプルの動作は directive 用のと一緒です
extend の方のテンプレートが変わってます

<div id="normal"></div>
<div id="extend"></div>

<script type="module">
import { render } from "https://unpkg.com/lit-html?module"
import html from "./lit-html-extend.js"

const init = (host, tpl) => {
const values = {
text: "",
select: "1",
checked: true,

onChangeText(eve) {
// only number textbox
values.text = eve.target.value.replace(/[^0-9]/g, "")
rerender()
},
onChangeSelect(eve) {
// select A when C is selected
values.select = eve.target.value.replace("3", "1")
rerender()
},
onChangeChecked(eve) {
// always true
rerender()
},
}

const rerender = () => render(tpl(values), host)
rerender()
}

init(normal, values => html`
<div>
<input type="text" .value=${values.text} @input=${values.onChangeText}>
<select .value=${values.select} @change=${values.onChangeSelect}>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>
<input type="checkbox" .checked=${values.checked} @change=${values.onChangeChecked}>
</div>
<div>${values.text},${values.select},${values.checked}</div>
`)

init(extend, values => html`
<div>
<input type="text" !value=${values.text} @input=${values.onChangeText}>
<select !value=${values.select} @change=${values.onChangeSelect}>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>
<input type="checkbox" !checked=${values.checked} @change=${values.onChangeChecked}>
</div>
<div>${values.text},${values.select},${values.checked}</div>
`)
</script>