前回からのつづき
前回、Web Conponents 「拡張tableタグ」の作り方を説明しました。
以降では、実際に作成した「拡張tableタグ」のJavaScript、テストプログラムの HTML, CSS, JavaScript の全コードを掲載します。
「拡張tableタグ」のJavaScript
//
// テーブル拡張クラス用JavaScriptコード
//
class ExpandTable extends HTMLElement {
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");
}
connectedCallback() {
const fieldsAttr = this.getAttribute("fields");
this.fields = fieldsAttr ? JSON.parse(fieldsAttr) : [];
}
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);
}
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);
}
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";
}
}
resetDatas(datas) {
this.setHeader();
datas.forEach(data => this.setData(data));
this.setFooter();
}
}
customElements.define("konkon-table", ExpandTable);
テストプログラムの HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Session Storage Test</title>
<link rel="stylesheet" href="./style.css">
<script type="text/javascript" src="./table_class.js"></script>
<script type="text/javascript" src="./script.js"></script>
</head>
<body>
<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>
<fieldset class="field">
<legend>[ 検索項目 ]</legend>
<div>
<label>
<input type="radio" id="fid" name="field" value="id" checked>ID
</label>
<label>
<input type="radio" id="fhizuke" name="field" value="hizuke">日付
</label>
<label>
<input type="radio" id="fhinmoku" name="field" value="hinmoku">品目
</label>
<label>
<input type="radio" id="fkingaku" name="field" value="kingaku">金額
</label>
</div>
</fieldset>
<fieldset class="condition">
<legend>[ 検索条件 ]</legend>
<div>
<input type="number" class="value" id="input-value" disabled><br>
<input type="number" class="bound" id="input-lower-value" disabled> ~ <input type="number" class="bound" id="input-upper-value" disabled><br>
<label><input type="checkbox" id="input-lower-threshold" checked disabled> 閾値含む </label>
<label><input type="checkbox" id="input-upper-threshold" checked disabled> 閾値含む</label>
</div>
<div class="search-button">
<button id="btn-only" disabled>一致検索</button>
<button id="btn-prefix-match" disabled>前方一致検索</button><br>
<button id="btn-lower" disabled>下限検索</button>
<button id="btn-upper" disabled>上限検索</button>
<button id="btn-bound" disabled>範囲検索</button>
</div>
</fieldset>
<p>
<button id="btn-all" disabled>全検索</button>
</p>
<p>
<div id="popup-status"></div>
</p>
</body>
</html>
テストプログラムの CSS
body {
padding: 1rem;
background-color: rgb(253, 191, 76);
text-align: center;
overflow: hidden;
}
#kk-table {
height: 12rem;
}
.condition {
display: flex;
justify-content: center;
gap: 1rem;
}
.search-button {
text-align: left;
}
.value {
width: 19.5rem;
}
.bound {
width: 8rem;
}
#popup-status {
font-size: 1.25rem;
font-weight: bold;
color: red;
margin-bottom: 1rem;
}
テストプログラムの JavaScript
//
// IndexedDBの動作確認用JavaScriptコード
//
window.addEventListener("load", function() {
// 画面要素取得
const table = document.getElementById("kk-table");
const input_fields = document.getElementsByName("field");
const input_value = document.getElementById("input-value");
const input_lower_value = document.getElementById("input-lower-value");
const input_upper_value = document.getElementById("input-upper-value");
const input_lower_threshold = document.getElementById("input-lower-threshold");
const input_upper_threshold = document.getElementById("input-upper-threshold");
const btn_only = document.getElementById("btn-only");
const btn_prefix_match = document.getElementById("btn-prefix-match");
const btn_lower = document.getElementById("btn-lower");
const btn_upper = document.getElementById("btn-upper");
const btn_bound = document.getElementById("btn-bound");
const btn_all = document.getElementById("btn-all");
const status = document.getElementById("popup-status");
// 固定値定義
const MAX_COUNT = 10;
const datas = [
{ id: 1, hizuke: new Date("2026-05-01"), hinmoku: "ノート", kingaku: 2000 },
{ id: 2, hizuke: new Date("2026-05-02"), hinmoku: "えんぴつ", kingaku: 1200 },
{ id: 3, hizuke: new Date("2026-05-02"), hinmoku: "消しゴム", kingaku: 500 },
{ id: 4, hizuke: new Date("2026-05-03"), hinmoku: "ボールペン", kingaku: 1500 },
{ id: 5, hizuke: new Date("2026-05-03"), hinmoku: "シャーペン", kingaku: 100 },
{ id: 6, hizuke: new Date("2026-05-04"), hinmoku: "定規", kingaku: 900 },
{ id: 7, hizuke: new Date("2026-05-04"), hinmoku: "コンパス", kingaku: 2500 },
{ id: 8, hizuke: new Date("2026-05-05"), hinmoku: "ノートA4", kingaku: 1000 },
{ id: 9, hizuke: new Date("2026-05-06"), hinmoku: "ノートB5横", kingaku: 6000 },
{ id: 10, hizuke: new Date("2026-05-07"), hinmoku: "ノートB5縦", kingaku: 2000 }
];
// 変数定義
let db = null; // IndexedDBのIDBDatabase(DB接続)オブジェクト
let threshold_open = false; // 閾値フラグ false:含まない/true:含む
// 検索項目に従い、入力欄の種別変更
input_fields.forEach(function (field) {
field.addEventListener('click', function(event) {
input_value.value = "";
input_lower_value.value = "";
input_upper_value.value = "";
btn_only.disabled = true;
btn_prefix_match.disabled = true;
btn_lower.disabled = true;
btn_upper.disabled = true;
btn_bound.disabled = true;
switch (field.value) {
case "id" :
input_value.type = "number";
input_lower_value.type = "number";
input_upper_value.type = "number";
break;
case "hizuke" :
input_value.type = "date";
input_lower_value.type = "date";
input_upper_value.type = "date";
break;
case "hinmoku" :
input_value.type = "text";
input_lower_value.type = "text";
input_upper_value.type = "text";
break;
case "kingaku" :
input_value.type = "number";
input_lower_value.type = "number";
input_upper_value.type = "number";
break;
default :
input_value.type = "text";
input_lower_value.type = "text";
input_upper_value.type = "text";
break;
}
});
});
// 一致検索、前方一致検索の入力欄の変更を監視して、入力があれば検索ボタンを有効化
['keyup', 'change'].forEach(function (eventType) {
input_value.addEventListener(eventType, function(event) {
input_lower_value.value = "";
input_upper_value.value = "";
input_lower_threshold.disabled =true;
input_upper_threshold.disabled =true;
btn_lower.disabled = true;
btn_upper.disabled = true;
btn_bound.disabled = true;
if (this.value) {
btn_only.disabled = false;
if (input_value.type == "text") btn_prefix_match.disabled = false;
} else {
btn_only.disabled = true;
btn_prefix_match.disabled = true;
}
status.textContent = "";
});
});
// 下限検索の入力欄の変更を監視して、入力があれば検索ボタンを有効化
['keyup', 'change'].forEach(function (eventType) {
input_lower_value.addEventListener(eventType, function(event) {
input_value.value = "";
btn_only.disabled = true;
btn_prefix_match.disabled = true;
if (this.value) {
input_lower_threshold.disabled =false;
btn_lower.disabled = false;
if (input_upper_value.value) btn_bound.disabled = false;
} else {
input_lower_threshold.disabled =true;
btn_lower.disabled = true;
btn_bound.disabled = true;
}
status.textContent = "";
});
});
// 下限検索、上限検索、範囲検索の入力欄の変更を監視して、入力があれば検索ボタンを有効化
['keyup', 'change'].forEach(function (eventType) {
input_upper_value.addEventListener(eventType, function(event) {
input_value.value = "";
btn_only.disabled = true;
btn_prefix_match.disabled = true;
if (this.value) {
input_upper_threshold.disabled =false;
btn_upper.disabled = false;
if (input_lower_value.value) btn_bound.disabled = false;
} else {
input_upper_threshold.disabled =true;
btn_upper.disabled = true;
btn_bound.disabled = true;
}
status.textContent = "";
});
});
// 一致検索、前方一致検索ボタン押下時に、IndexedDBから値を取得し、テーブルに表示
[btn_only, btn_prefix_match].forEach(function (button) {
button.addEventListener('click', function(event) {
const tx = db.transaction('sales', 'readonly');
const store = tx.objectStore('sales');
table.setHeader();
const results = []
function query(value) {
switch (button.id) {
case "btn-only" :
return IDBKeyRange.only(value);
case "btn-prefix-match" :
return IDBKeyRange.bound(value, value+"\uffff");
default :
return IDBKeyRange.only(value);
}
}
let request;
input_fields.forEach((input_field) => {
if (input_field.checked) {
switch (input_field.value) {
case "id" :
request = store.openCursor(query(Number(input_value.value)));
break;
case "hizuke" :
request = store.index(input_field.value + "Index").openCursor(query(new Date(input_value.value)));
break;
case "hinmoku" :
request = store.index(input_field.value + "Index").openCursor(query(input_value.value));
break;
case "kingaku" :
request = store.index(input_field.value + "Index").openCursor(query(Number(input_value.value)));
break;
default :
request = store.openCursor(query(0));
break;
}
}
});
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
table.setFooter();
if (!results.length) status.textContent = "指定されたデータがありません。";
return;
}
results.push(cursor.value)
table.setData(cursor.value);
status.textContent = "完了"
if (results.length < MAX_COUNT) {
cursor.continue();
return;
}
table.setFooter();
}
request.onerror = () => {
status.textContent = "エラー発生"
}
});
});
// 下限検索、上限検索、範囲検索ボタン押下時に、IndexedDBから値を取得し、テーブルに表示
[btn_lower, btn_upper, btn_bound].forEach(function (button) {
button.addEventListener('click', function(event) {
const tx = db.transaction('sales', 'readonly');
const store = tx.objectStore('sales');
table.setHeader();
const results = []
function query(lowerValue, upperValue, lowerOpen, upperOpen) {
switch (button.id) {
case "btn-lower" :
return IDBKeyRange.lowerBound(lowerValue, lowerOpen);
case "btn-upper" :
return IDBKeyRange.upperBound(upperValue, upperOpen);
case "btn-bound" :
if (lowerValue == upperValue && lowerOpen != upperOpen) {
return IDBKeyRange.only(0);
} else if (lowerValue > upperValue) {
return IDBKeyRange.only(0);
} else {
return IDBKeyRange.bound(lowerValue, upperValue, lowerOpen, upperOpen);
}
default :
return IDBKeyRange.only(0);
}
}
let request;
input_fields.forEach((input_field) => {
if (input_field.checked) {
const lower_open = (input_lower_threshold.checked ? false : true);
const upper_open = (input_upper_threshold.checked ? false : true);
switch (input_field.value) {
case "id" :
request = store.openCursor(query(Number(input_lower_value.value), Number(input_upper_value.value), lower_open, upper_open));
break;
case "hizuke" :
request = store.index(input_field.value + "Index").openCursor(query(new Date(input_lower_value.value), new Date(input_upper_value.value), lower_open, upper_open));
break;
case "hinmoku" :
request = store.index(input_field.value + "Index").openCursor(query(input_lower_value.value, input_upper_value.value, lower_open, upper_open));
break;
case "kingaku" :
request = store.index(input_field.value + "Index").openCursor(query(Number(input_lower_value.value), Number(input_upper_value.value), lower_open, upper_open));
break;
default :
request = store.openCursor(query(0, 0, 0, 0));
break;
}
}
});
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
table.setFooter();
if (!results.length) status.textContent = "指定されたデータがありません。";
return;
}
results.push(cursor.value)
table.setData(cursor.value);
status.textContent = "完了"
if (results.length < MAX_COUNT) {
cursor.continue();
return;
}
table.setFooter();
}
request.onerror = () => {
status.textContent = "エラー発生"
}
});
});
// 全検索ボタン押下時に、カスタムイベント発生
btn_all.addEventListener('click', function(event) {
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'sales' }
}));
});
// カスタムイベント発生時、IndexedDBから全データ取得し、テーブルを再表示
document.addEventListener('reloadTable', (e) => {
const tx = db.transaction(e.detail.storename, 'readonly');
const store = tx.objectStore(e.detail.storename);
table.setHeader();
const results = []
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
table.setFooter();
return;
}
results.push(cursor.value)
table.setData(cursor.value);
if (results.length < MAX_COUNT) {
cursor.continue();
return;
}
table.setFooter();
}
});
// 初期処理
status.textContent = "データベースオープン中";
const request = indexedDB.open('SalesDatabase', 1);
request.onupgradeneeded = (event) => {
db = event.target.result;
if (!db.objectStoreNames.contains("sales")) {
const store = db.createObjectStore("sales", { keyPath: "id" });
store.createIndex("hizukeIndex", "hizuke");
store.createIndex("hinmokuIndex", "hinmoku");
store.createIndex("kingakuIndex", "kingaku");
datas.forEach(data => {
store.add(data);
});
}
};
request.onsuccess = (event) => {
db = event.target.result;
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'sales' }
}));
input_value.disabled=false;
input_lower_value.disabled=false;
input_upper_value.disabled=false;
btn_all.disabled=false;
status.textContent = "";
};
});