前回からのつづき

前回、新しく作成する 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部品の可能性が広がる。