◆ ShadowDOM がないとコンポーネント内が別空間にならない
  ◆ class 名が被ってスタイルが影響受けたり サブコンポーネントの中までクエリセレクタで取得できたり
◆ CSS は parcel の css modules が便利
◆ JavaScript も css modules に任せると JavaScript の都合で css ファイルまで変更必要になる
  ◆ ▶ JavaScript でも似たことをする機能を作った

ShadowDOM は便利ですし WebComponents を使うときは基本使うかと思います
ただ 全部に同じスタイルタグを入れないとダメでムダが多かったりもしますし 外から見れないというのは対応してないライブラリがツリー全体をパースしないといけないようなケースには使えません
なので ShadowDOM をつかわないというケースもときどきあるのですが ShadowDOM がないとコンポーネントの内側も全部見えてしまいます

困るのは CSS と JavaScript のセレクタ関係です
CSS では特定コンポーネントだけを対象にしたいのに 内側にあるコンポーネントの内側も対象になります

例えば

x-elem .foo {
margin: 10px;
}

とすれば x-elem タグの内側という条件をつけれるので x-elem 内に限定はできます
しかし x-elem の中に y-elem があった場合に y-elem の中の .foo まで影響します

少し面倒ですが クラス名をそのコンポーネント専用のクラス名にして他コンポーネントと重複しないようにすれば セレクタの最後のスタイルをあてる対象をクラス名で指定しているなら大丈夫です

x-elem .x-elem--foo {
margin: 10px;
}

という感じです
クラス名が長くなりますし 冗長感があってなんか嫌です


JavaScript の場合はスクリプトなのでセレクタにマッチしてもカスタム要素の内側は無視するという処理を入れることはできます

const matches = document.body.querySelectorAll("x-elem .foo")
const elem = [...matches].find(e => {
let x = e.parentElement
while(x){
if(x.tagName === "X-ELEM") return true
if(x.tagName.includes("-")) return false
x = x.parentElement
}
})

x-elem 以外の - がつく要素は別のカスタムエレメントなので 親をたどって x-elem より先にそれが出たらマッチしないものとします
1 つほしいなら querySelectorAll の結果から最初にマッチしたもので 複数欲しいならマッチするものだけにフィルタした結果になります
ちょっと面倒ですがなんとかなりはします

css modules

parcel を使っていて知ったのですが css modules を使えば CSS も楽に解決できそうです

JavaScript では

import css from "./x-elem.css"

という風に CSS ファイルをインポートします
css modules が有効だと CSS ファイルの id とクラス名部分が自動で他と重複しない文字列に置き換えられて import ではその変換情報を取得できます
css 変数の中身はこういう感じになります

{
active: "_active_x03jk_380",
body: "_body_x03jk_121",
clear: "_clear_x03jk_492",
container: "_container_x03jk_333",
delete: "_delete_x03jk_306",
footer: "_footer_x03jk_118",
main: "_main_x03jk_109",
top: "_top_x03jk_16",
}

使うときは

this.innerHTML = `
<div class="${css.body}"></div>
<footer class="${css.footer}">
<input type="button" class="${css.delete}" value="delete">
</footer>
`

this.addEventListener("click", eve => {
this.classList.add(css.active)
})

みたいに文字列リテラルでクラス名を入れる代わりにインポートした css オブジェクトのプロパティを設定します
バンドルされた CSS ファイルの方には 「_active_x03jk_380」 みたいな変換済みの名前で定義されているのでこうすることでスタイルが適用できます

body や active みたいなよくある名前にしても CSS ファイル上では複雑な重複しない名前に変換されているのでそのコンポーネント専用のクラス名として扱えます
別コンポーネントに body や active を書いてもバンドルされた CSS ファイル上では別の名前です

JavaScript に使うと

便利なので JavaScript のセレクタでも使ったりしたくなります

this.querySelector(`.${css.top}`).innerHTML = "aaa"

ですが CSS ファイルなのでスタイル定義がないものはインポートした css オブジェクトに含まれません
例えば top に特に必要なスタイルがなくて CSS ファイルに .top を書いてないと css.top は undefined です
他にも undefined があるとセレクタでは最初の undefined を取得するので目的のものを取得できません

JavaScript で使うために

.top, .clear, .content, .footer, .save, .cancel, .submit, .main {}

のように JavaScript でしか使わないクラス名を CSS ファイルに書いていくのもなんか変な話です
JavaScript の処理の都合で CSS ファイルの編集が必要になるわけですしね

現時点で JavaScript 専用だろうとコンポーネント内に存在するクラス名ならスタイルが空でもすべて CSS ファイルにあってもおかしくないといえばそうなのかもですが個人的には CSS と JavaScript は分けたいです

JavaScript 用のクラスリストを作る

そういうわけで 似たような仕組みを JavaScript 側でも作って JavaScript のセレクタとして扱うものはこっちで作ったクラス名にします
CSS ファイルからインポートしたものは HTML を作るときの埋め込みと classList のメソッドの引数のみに使います

class BaseElement extends HTMLElement {
constructor() {
super()

const ctor = this.constructor
if (!ctor.names) {
const obj = {}
const elem_key = btoa(this.tagName + this.constructor.name)
.replace(/[\+\/\=]/g, x => ({ "+": "-", "/": "_", "=": "" }[x]))
.slice(0, 8)
for (const x of ctor.key_names) {
obj[x] = "__" + x + "__" + elem_key
}
Object.freeze(obj)
ctor.names = obj
}
this.names = ctor.names
}

static get key_names() {
return []
}
}

customElements.define(
"x-elems",
class extends BaseElement {
connectedCallback() {
this.innerHTML = `
<div id="${this.names.id}" class="${this.names.container}">
<div class="${this.names.xxx_host}">
<input class="${this.names.xxx}">
</div>
<div class="${this.names.yyy_host}">
<input class="${this.names.yyy}">
</div>
<div class="${this.names.zzz_host}">
<input class="${this.names.zzz}">
</div>
</div>
`
}

static get key_names() {
return ["id", "container", "xxx_host", "xxx", "yyy_host", "yyy", "zzz_host", "zzz"]
}
}
)

document.body.innerHTML = `<x-elems></x-elems><hr><x-elems></x-elems>`

BaseElement はこの機能を使うための親クラスです
これを継承したクラスで static な getter の key_names を作ります
返す値はクラス名の文字列の配列です

コードにあるように this.names.xxx のようにするとクラス名を取得できます
クラス名は css modules に少し似せていて元のクラス名とランダムな文字列の組み合わせです
ただ毎回同じになったほうがデバッグ時に扱いやすいので 見た目はランダムな文字列ですが実際はクラス名とタグ名を btoa して前から 8 文字取り出したものです
長めかつ前半が重複する名前だと別のコンポーネントでも一致してしまう可能性はあるのでもう少しちゃんとしたものにするなら hash 関数通すとかしたほうが良いと思います

JavaScript 内のコンポーネント内で定義してるクラス名なのでコンポーネントの JavaScript ファイルで完結するのがいいところです
セレクタとして使うなら

this.querySelector(`.${this.names.button}`)

という感じです

ただ少し長くなって面倒です
コンポーネント内の機能なのでコンポーネントのメソッドとして

qs(strs, ...values){
const names = values.map(e => this.names[e])
const selector = strs.reduce((a, e, i) => a + names[i - 1] + e)
return this.querySelector(selector)
}

classQuery(cond){
if(typeof cond === "string"){
return this.querySelector(`.${this.names[cond]}`)
}else if(cond instanceof Object){
const entries = Object.entries(cond).map(([key, value]) => [key, this.querySelector(`.${this.names[value]}`)])
const result = {}
for(const [key, value] of entries) result[key] = value
return result
}
}

こういうのを作っておけば

this.qs`#${"form"} .${"button"}`

this.classQuery({date: "date", time: "time", name: "name", submit: "submit"})

という感じで便利な使い方もできます