◆ Custom Element を作るときに属性を対応させるのが面倒
  ◆ プロパティと対応付けで型を変換したり
  ◆ setter 処理があると属性の監視を設定したり
◆ 考えてみると属性って基本使わない
  ◆ HTML として書くときに初期値として設定するくらい
◆ コンストラクタで属性をプロパティ変換して 以降はプロパティのみにする

毎回のように思うことですが Custom Element を作るときに属性を対応させるのが面倒です
プロパティと対応付けないといけませんし 文字列型と任意型なので変換も必要です
さらに書き換えられたときに追加の処理がある場合は面倒が増えます
プロパティの方は簡単ですが 属性の方は変更を監視する設定が必要です
結局 中途半端な対応のままなことも少なくないです

これをどうにかしたいなーと考えていたのですが よく考えればプロパティと対応してる属性って基本使わないんですよね
プロパティアクセスでいいのに getAttribute や setAttribute をつかうことはほぼありません
IE 対応で IE のバグ避けのために使うこともありましたが WebComponents 機能は IE だと動かないので無視でいいです
a タグの href はプロパティだと現在の URL を使った解決済みの完全なもので 属性だと指定した生のデータなので稀に使い分けることはあります
自分で作る場合にこんな特殊なものは基本無いですし 使い分けはまずないです

ただ 唯一属性が必要なところがあって それは初期値です
HTML としてタグを書くときに class だったり hidden だったりを書くことは多いです
すでにある要素を取得した場合ならともかく 作成時ならわざわざインスタンスを作ってからプロパティを更新というやり方は基本しません

document.body.innerHTML = `
<a href="/foo">aa</a>
`



const a = document.createElement("a")
a.href = "/foo"
document.body.append(a)

なら基本上の方でしょう
それに最初から HTML ファイルに書いてあるケースだってありえます

初期値だけ属性を使う

初期値のみであれば constructor の処理で取得してプロパティに変換してしまえば あとは属性はないものとして考えられます
フォームパーツであれば 属性を初期値として reset 処理のために残していたりしますが それ以外だと残っていてプロパティと異なれば混乱のもとなのでプロパティ変換のタイミングで消しておきます

customElements.define(
"a-b",
class extends HTMLElement {
constructor() {
super()

this._foo = this.getAttribute("foo")
this.removeAttribute("foo")
}

get foo() {
return this._foo
}

set foo(value) {
this._foo = value
}
}
)

これを定義して a-b 要素を作ります

document.body.innerHTML = `
<a-b foo="x" bar="y"></a-b>
`

foo 属性はプロパティ化して消えるので devtools で見るとこうなります

<a-b bar="y"></a-b>

DOM ツリーの表示で確認できないのは不便かもですが 属性が多くなってくると見づらくなってあまり見ないですし プロパティ化できてない 名前のミスなどに気づけますしこれはこれで良さそうです

もう少し楽にする

ただこれでも getter/setter 定義や属性取得を書いてクラスが見づらくなるデメリットがあります
これを楽にするための関数を作りました

const attrToProp = (instance, option = instance.constructor.attr_to_prop) => {
const store = {}

for (const [key, val] of Object.entries(option)) {
const { fromAttr, setBefore, setAfter } = val ?? {}
const attr = instance.getAttribute(key)
store[key] = fromAttr ? fromAttr(attr) : attr
instance.removeAttribute(key)

Object.defineProperty(instance, key, {
get() {
return store[key]
},
set(value) {
const old = store[key]
if (setBefore) {
value = setBefore(value, old)
}
store[key] = value
if (setAfter) {
setAfter(value, old)
}
},
})
}

return store
}

これを こんな感じで使います

customElements.define(
"a-b",
class extends HTMLElement {
constructor() {
super()
attrToProp(this)
}

static attr_to_prop = {
foo: {
fromAttr(x) {
return ~~x
},
},
bar: null,
baz: {
setBefore(value, old) {
if (value.length > 10) return old
else return value + "!"
},
setAfter(value) {
console.log(value)
},
},
}
}
)

static プロパティの attr_to_prop という名前で設定をまとめて書いておいて attrToProp 関数にインスタンスを渡します
static プロパティを使わず第二引数に直接渡すこともできます

attr_to_prop は属性と対応づけるプロパティ名を key にしてその設定を value に書いたオブジェクトです
設定は fromAttr, setBefore, setAfter の 3 種類で全部関数です
どれも省略可能で 全部を省略するなら value を null にしても良いです

fromAttr は属性からプロパティに変換するときの関数です
属性はすべて文字列なので数値型にしたいとかにつかいます
指定されていないとそのままの文字列でプロパティになります

getter/setter も自動で作るので setBefore と setAfter には setter の処理を書きます
setBefore はセット前の処理で return した値でセットする値を置き換えできます
第二引数に現在の値が渡されるので 更新しない場合は現在の値を返せば良いです
setAfter はセット後です
こっちは return した値は無視されます

document.body.innerHTML = `
<a-b foo="9" bar="9" baz="9"></a-b>
`
const ab = document.querySelector("a-b")

console.log(ab)
// <a-b></a-b>

ab.foo
// 9
ab.bar
// "9"
ab.baz
// "9"

ab.baz = "1234567890abcdef"
// "9"
ab.baz = "A"
// "A!"

もっと簡単で良かったかも

ここまで作ってから思ったのですが属性と対応付けないなら getter/setter はいらなかった気がします
setter で追加処理がいるなら別に作ればいいだけでこの機能に含めなくても良さそうです

foo 属性を数値型としてプロパティにするならこれだけでも十分です

customElements.define(
"a-b",
class extends HTMLElement {
foo = ~~this.getAttribute("foo")
}
)

すごくシンプルになりました
属性の削除も必要なら getAttribute の代わりに属性を削除して取得する関数を用意してそれを呼び出せば良いです