前回からのつづき
前回、新しく作成する Web Conponents 「拡張tableタグ」に盛り込みたい機能や、それを使ったテストプログラムの説明を行いました。
今回から、Web Components 「拡張tableタグ」の構築を実際に進めていきたいと思います。
Web Componentsとしての大枠の作成
まず、Web Components としての大枠部分を作成します。タグ名は、「konkon-table」とします。
class ExpandTable extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
}
}
customElements.define("konkon-table", ExpandTable);
👉 ExpandTable クラスを定義し、「konkon-table」タグを登録します。
table 構成の定義
UI部品の table 構成を constructor 内の shadow DOM に定義します。
:
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
this.shadow.innerHTML = `
<style>
:host {
display: block;
background-color: white;
}
table {
border-collapse: separate;
border-spacing: 0;
border: 1px solid;
}
thead th {
position: sticky;
top: 0;
z-index: 1;
border: 1px solid;
background-color: skyblue;
font-weight: bold;
height: 2rem;
}
tbody td {
border: 1px solid;
background-color: white;
height: 2rem;
}
tfoot td {
position: sticky;
bottom: 0;
z-index: 1;
border: 1px solid;
background-color: lightgray;
font-weight: bold;
height: 2rem;
}
.id { width: 20rem; }
.hizuke { width: 30rem; }
.hinmoku { width: 50rem; }
.kingaku { width: 30rem; }
.dummy-row td {
background-color: lemonchiffon;
}
</style>
<table>
<thead></thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
`;
this.table = this.shadowRoot.querySelector("table");
this.thead = this.shadowRoot.querySelector("thead");
this.tbody = this.shadowRoot.querySelector("tbody");
this.tfoot = this.shadowRoot.querySelector("tfoot");
}
:
👉 同時に、テーブルの style も定義します。
※ 定義後に、this.shadowRoot.querySelector で、要素を取得しているのは、メソッド内で操作する為です。
テーブルイメージ
| ID | 日付 | 品目 | 金額 |
|---|---|---|---|
| 1 | 2026/5/1 | ノート | 2000 |
| 2 | 2026/5/2 | えんぴつ | 1200 |
| 3 | 2026/5/2 | 消しゴム | 500 |
| 合計 | 3700 |
fields 属性の読込
connectedCallback イベント内で、「konkon-table」タグの定義情報の fields 属性を読み込みます。
:
connectedCallback() {
const fieldsAttr = this.getAttribute("fields");
this.fields = fieldsAttr ? JSON.parse(fieldsAttr) : [];
}
:
👉 fields 属性は、JSON 文字列として受け渡されるので、パースが必要です。
※ constructor では属性がまだ反映されていない為、属性の取得は、このタイミングで行います。
「拡張tableタグ」は、使用側のHTMLで下記のように定義されることを想定します。
例:
<konkon-table id="kk-table" fields='[
{"name": "id", "class": "id", "type": "number", "calc": "合計", "content": "ID"},
{"name": "hizuke", "class": "hizuke", "type": "date", "calc": "", "content": "日付"},
{"name": "hinmoku", "class": "hinmoku", "type": "string", "calc": "", "content": "品目"},
{"name": "kingaku", "class": "kingaku", "type": "number", "calc": "sum", "content": "金額"}
]'>
</konkon-table>
👉 fields属性に JSON 形式で、項目毎の定義を記述します。
項目毎のプロパティは、下記のとおりとします。
- name : th タグにつけるID
- class : th タグにつける CSS クラス
- type : td タグの値のタイプ
- number : 数字
- date : 日付
- string : 文字
- calc : フッターの td タグに出力する値
- 文字列 : そのまま出力
- sum : 列の合計を計算して出力
- content : th タグに出力する項目名
テーブルのヘッダー部作成メソッドの追加
fields 属性で定義された項目の内容に従い、ヘッダー部を作成するメソッドを定義します。
:
setHeader() {
this.thead.textContent = "";
this.tbody.textContent = "";
this.tfoot.textContent = "";
const tr = document.createElement("tr");
this.fields.forEach(field => {
const th = document.createElement("th");
th.classList.add(field.class);
th.textContent = field.content;
tr.appendChild(th);
});
this.thead.appendChild(tr);
}
:
👉 メソッドが呼び出されたタイミングで、ヘッダー部、ボディ部、フッター部を初期化しています。
「拡張tableタグ」定義時の fields 属性に定義された項目に従い、タイトルを作成します。
テーブルへの行追加メソッドの追加
パラメータとして受け渡されたデータをボディ部に行として、追加するメソッドを定義します。
:
setData(data) {
const tr = document.createElement("tr");
this.fields.forEach(field => {
const td = document.createElement("td");
td.textContent = (field.type === "date"
? data[field.name].toLocaleDateString()
: data[field.name]);
tr.appendChild(td);
});
this.tbody.appendChild(tr);
}
:
👉 field情報を参照し、該当のデータが日付の場合は、文字列に変換して出力します。
パラメータの data オブジェクトの形式は下記の通りです。各項目の値はプロパティとして定義します。プロパティ名は、fields 属性の定義の name と合わせるものとします。
例:
{
id: 1,
hizuke: new Date("2026-05-01"),
hinmoku: "ノート",
kingaku: 2000
}
テーブルのフッター部の作成メソッドの追加
合計など、テーブルのフッター部を作成するメソッドを定義します。
:
setFooter() {
const trec = document.createElement('tr');
this.fields.forEach((field, columnIndex) => {
const tdata = document.createElement('td');
if (field.type == "number") {
switch (field.calc) {
case "sum" :
const tds = this.shadowRoot.querySelectorAll(`tbody tr td:nth-child(${columnIndex + 1})`);
let sum = 0;
tds.forEach(td => {
const value = Number(td.textContent.trim());
if (!isNaN(value)) sum += value;
});
tdata.textContent = sum;
break;
default :
tdata.textContent = field.calc;
break;
}
} else {
tdata.textContent = field.calc;
}
trec.appendChild(tdata);
});
this.tfoot.appendChild(trec);
// レコードが少ない場合のフッター位置固定化の為、ダミーレコード挿入
const oldDummy = this.shadowRoot.querySelector(".dummy-row");
if (oldDummy) oldDummy.remove();
const tds = this.shadowRoot.querySelectorAll(`tbody tr td:nth-child(1)`);
const tdsHeight = tds.length > 0 ? tds[0].offsetHeight : 0;
if (tdsHeight * tds.length + this.thead.offsetHeight + this.tfoot.offsetHeight < this.offsetHeight) {
this.style.overflow = "hidden";
const tr = document.createElement("tr");
tr.classList.add("dummy-row");
const td = document.createElement("td");
td.colSpan = 99; // 何列でも対応
td.style.height = (this.offsetHeight - this.table.offsetHeight < 0 ? 0 : this.offsetHeight - this.table.offsetHeight) + "px";
tr.appendChild(td);
this.tbody.appendChild(tr);
} else {
this.style.overflow = "auto";
}
}
:
👉 フッター部作成時、fields 属性に、”SUM”と定義されている項目に関しては、該当列の合計を計算して出力しています。
※ フッター作成後、行数が少ない場合に、フッターの位置が上方にズレることを防ぐ為、ダミーの行を生成し、高さを計算して設定しています。又、行数が表示サイズを超える場合は、スクロールバーを表示するようにしています。
テーブルの再作成メソッドの追加
複数行のデータをオブジェクトの配列として渡し、ヘッダー部、フッター部も含め、テーブル全てを作り直すメソッドを定義します。
:
resetDatas(datas) {
this.setHeader();
datas.forEach(data => this.setData(data));
this.setFooter();
}
:
👉 内部では、いままで作成したメソッドを順次呼び出して実現しています。
パラメータの datas オブジェクトは、data オブジェクトの配列とします。
例:
[
{
id: 1,
hizuke: new Date("2026-05-01"),
hinmoku: "ノート",
kingaku: 2000
},
{
id: 2,
hizuke: new Date("2026-05-02"),
hinmoku: "えんぴつ",
kingaku: 1200
}
:
]
使用側の対応
「拡張tableタグ」を使用するテストプログラムの対応箇所を示します。
HTMLの対応箇所
<konkon-table id="kk-table" fields='[
{"name": "id", "class": "id", "type": "number", "calc": "合計", "content": "ID"},
{"name": "hizuke", "class": "hizuke", "type": "date", "calc": "", "content": "日付"},
{"name": "hinmoku", "class": "hinmoku", "type": "string", "calc": "", "content": "品目"},
{"name": "kingaku", "class": "kingaku", "type": "number", "calc": "sum", "content": "金額"}
]'>
</konkon-table>
👉 「拡張tableタグ」を表示したい場所に konkon-table タグを記述します。
CSSの対応箇所
#kk-table {
height: 12rem;
}
👉 「拡張tableタグ」の高さを設定します。今回は ID を使用しています。
※ この定義を行った場合、「拡張tableタグ」の高さは固定され、自動スクロールとなります。逆に指定しない場合、行数に合わせ、高さが可変となります。
JavaScriptの対応箇所
:
const table = document.getElementById("kk-table");
:
table.setHeader();
:
table.setData(data);
:
table.setFooter();
:
table.resetDatas(datas);
:
👉 「拡張tableタグ」のElementを取得し、必要なタイミングで、必要なメソッドを呼び出し、テーブルを描画します。
今後の展開
- 属性変更を監視する attributeChangedCallback の活用
→
<konkon-table fields="...">を後から書き換えても自動で再描画できるようにする。
- slot を使ったカスタムテンプレート対応
→ ユーザーが
<th>や<td>のテンプレートを差し込めるようにする。
- CSS Shadow Parts の導入
→ 外側から一部のスタイルだけ上書き可能にする。(プロの UI コンポーネントでよく使う)
- イベント発火(CustomEvent)
→ 行クリックやセル編集などを外側に通知できるようにする。
まとめ
👉 Web Components 化により、
- コードがすっきりと見やすくなる。
- 使う側との役割分担が明確になる。
- UI部品のコードの独立性が高まる。
- 独自UI部品の可能性が広がる。