基本的なWebComponentsの作り方
Web Componentsとは
ロジック(Javascript)と見た目(HTML、css)をひとまとめにした独自のHTMLタグを作ることができます
ReactやVue.jsのコンポーネントのようなものですが、特定のフレームワークに依存せず広く使用できることがメリットです
現在(2023/10/14)の主要なブラウザはサポートされています
まずはHelloWorld
まずはプログラミングのお約束のHello Worldです
画面にHello Worldと表示するだけのWeb Components(以後Hello Worldコンポーネントと呼ぶ)です
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WebComponentsテスト</title>
</head>
<body>
<hello-world></hello-world>
<script>
class HelloWorld extends HTMLElement{
connectedCallback() {
this.innerText = 'Hello World';
}
}
customElements.define( 'hello-world', HelloWorld );
</script>
</body>
</html>
見たことのない<hello-world></hello-world>
というHTMLタグがありますが、このように独自のタグを作成することができます
では、具体的な作り方ですが、まずHTMLElementを継承したクラス(HelloWorld)を定義します
定義したクラスをcustomElements.define
でタグとして登録します
今回はわかりやすくするためhello-world
タグとして登録しましたが、クラス名と全く違うタグを設定することもできます
例えばcustomElements.define('test', HelloWorld)
とすると、<test></test>
というタグで同様に画面にHello Worldと表示されます
connectedCallback()でなにをしているの?
connectedCallbackはDOMにHello Worldコンポーネントが追加されるタイミングで実行されます
初期化処理や内部のDOMを生成するコードを記述します
connectedCallback() {
this.innerText = 'Hello World';
}
今回は簡単にinnerTextにHello Worldを設定しているのみです
通常のJavascriptでDOMを操作する場合とそう大きな違いはありません
子要素に別のエレメントを追加する場合はdocument.createElement()
でエレメントを作成し、appendChild
します
金額入力コンポーネントを作成してみる
ごく基本的なWeb Componentsの作成方法を説明しましたが、画面に固定の文字を表示するだけでは実用性がありません
より実用的な例として金額入力コンポーネントを作成します
必要な機能は以下の通りとします
- 数字と数値に関するもの記号のみ入力可能
- 3桁ごとにカンマを表示する
- valueは数値とする
- 別のライブラリやフレームワークなどの影響を受けないようにする
具体的なコードは以下のようになります
class CurrencyInput extends HTMLElement{
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
const input = document.createElement('input');
input.part = 'input';
input.addEventListener('input', e => {
if(e.target.value !== '') {
const numberValue = Number(e.target.value.replace(/,/g, ''));
if (!isNaN(numberValue)) {
input.value = numberValue.toLocaleString();
this.value = numberValue;
} else {
input.value = input.value.replace(/[^0-9.-]/g, '');
this.value = 0;
}
} else {
this.value = 0;
}
this.dispatchEvent(new Event('input'));
});
shadow.appendChild(input); // <-- Shadow Domにする場合
// this.appendChild(input); // <-- Shadow Domにしない場合
}
}
customElements.define( 'currency-input', CurrencyInput );
コンポーネント初期化
内部にinputタグを一つだけ持つシンプルな構造とします
初期化処理はシンプルなものでdocument.createElement
でinputのエレメントを作成し、イベントなどを設定しています
inputイベントで入力された値を判定し、表示用のテキストとしてinput.value
を書き換えています
その後、コンポーネントの外部にinputイベントを発火しています
ここでポイントとなるのがinput.value
とthis.value
の2つ変数が存在している点です
input.value
はコンポーネント内部で使用されるだけで、外部からは取得できません
一方、this.value
は外部から取得することができます
例えば以下のようにすると外部から値を取得することができます
<currency-input></currency-input>
<script>
const value = document.querySelector('currency-input').value;
</script>
Web Componentsも既存のHTMLタグと同様にdocument.querySelector
で取得することができます
コンポーネントを外部から切り離す
Web Componentsを決まった用途でしか使用しない場合、あまり考慮する必要はないかもしれませんが、複数プロジェクトなどで使いまわす場合、別のライブラリやcssに影響を受けるのは好ましくありません
そこで、コンポーネント内部を完全に別のものとして切り離すことができます
これをShadow Domといいます
コンポーネント内部をShadow Domにするにはconst shadow = this.attachShadow({mode: 'open'});
とすればいいです
ここで作成したshadow
に対して別途作成したエレメントをappendChild
していくことになります
逆にShadow Domにしたくない場合はthis.appendChild
とすれば外部と繋がったDomになります
Shadow Domとした場合、下記のコードはエラーとなります
<currency-input></currency-input>
<script>
// Shadow Domの場合currency-input内部のinputに直接アクセスできない
const value = document.querySelector('currency-input input').value;
</script>
Shadow Domのデメリット
Shadow Domにすれば、他のライブラリなどの影響を受けないため、予期せぬ原因で動作不良を起こすことがありません
一方、デメリットも存在します
Javascriptだけではなく、cssも切り離されているという点です
例えば、bootstrapを使用する場合を考えます
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WebComponentsテスト</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<custom-button></custom-button>
<button class="btn btn-primary">primary</button>
<script>
class CustomButton extends HTMLElement{
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
const button = document.createElement('button');
button.className = 'btn btn-primary';
button.innerText = 'primary';
shadow.appendChild(button);
}
}
customElements.define( 'custom-button', CustomButton);
</script>
</body>
</html>
2つのbuttonの見た目が違うことがわかります
custom-buttonのbuttonにはbootstrapの影響が及んでいません
Shadow Dom内部に外部からcssを適用する方法
Shadow Domは外部と切り離されていますが、cssに関しては外部から適用する方法があります
まずコンポーネント側の準備としてcssを適用させたいエレメントにpartというプロパティを指定します
const input = document.createElement('input');
input.part = 'inner-input'; // 任意の名前を指定できる
その後、疑似要素partを使用してcssを適用します
カッコ内はpartで指定した名前を指定します
currency-input::part(inner-input){
text-align: right;
}
これでnumeric-input内部のinputにcssを適用させることができます
注意点としてはpartが指定されていなければ、cssは適用できないという点です
今回の成果物
最後に全コードを記載しておきます
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WebComponentsテスト</title>
<style>
currency-input::part(inner-input){
text-align: right;
}
</style>
</head>
<body>
<currency-input></currency-input>
<button type="button" id="check" class="btn">value check</button>
<script>
class CurrencyInput extends HTMLElement{
connectedCallback() {
const input = document.createElement('input');
input.part = 'inner-input';
input.addEventListener('input', e => {
if(e.target.value !== '') {
const numberValue = Number(e.target.value.replace(/,/g, ''));
if (!isNaN(numberValue)) {
input.value = numberValue.toLocaleString();
this.value = numberValue;
} else {
input.value = input.value.replace(/[^0-9.-]/g, '');
this.value = 0;
}
} else {
this.value = 0;
}
this.dispatchEvent(new Event('input'));
});
// Shadow Domにする場合
const shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(input);
// Shadow Domにしない場合
//this.appendChild(input);
}
}
customElements.define( 'currency-input', CurrencyInput );
document.getElementById('check').addEventListener('click', e => {
console.log(document.querySelector('currency-input').value)
});
</script>
</body>
</html>
Discussion