Created
November 28, 2024 15:56
-
-
Save skysan87/2c4dcb513031f8c43a5b62332e9a87be to your computer and use it in GitHub Desktop.
動的にテーブルヘッダーを作成
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>動的にテーブルヘッダーを作成</title> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"Vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" | |
} | |
} | |
</script> | |
</head> | |
<!-- | |
なお、rowspanと行の組み合わせ次第では、Excelの結合を表現できない。 | |
例:1-2行目の結合と2-3行目の結合が被るとき | |
--> | |
<body> | |
<div id="app"> | |
<table> | |
<caption>見本</caption> | |
<thead> | |
<tr> | |
<th rowspan="4" style="background-color:aqua;">1</th> | |
<th colspan="3" rowspan="2" style="background-color: green;">2</th> | |
<th rowspan="4" style="background-color:orange;">3</th> | |
<th colspan="5" style="background-color:red;">4</th> | |
</tr> | |
<tr> | |
<th colspan="4" style="background-color: violet;">4-1</th> | |
<th rowspan="3" style="background-color: pink;">4-2</th> | |
</tr> | |
<tr> | |
<th rowspan="2" style="background-color: greenyellow;">2-1</th> | |
<th colspan="2">2-2</th> | |
<th rowspan="2">4-1-1</th> | |
<th rowspan="2">4-1-2</th> | |
<th colspan="2">4-1-3</th> | |
</tr> | |
<tr> | |
<th>2-2-1</th> | |
<th>2-2-2</th> | |
<th>4-1-3-1</th> | |
<th>4-1-3-2</th> | |
</tr> | |
</thead> | |
<tbody> | |
</tbody> | |
</table> | |
<div style="padding: 10px;"></div> | |
<table> | |
<caption>headerを動的に生成 見本</caption> | |
<thead> | |
<tr v-for="(row,index) in converted" :key="index"> | |
<th v-for="(col, i) in row" :key="i" :rowspan="col.rowspan" :colspan="col.colspan"> | |
{{ col.title }} | |
</th> | |
</tr> | |
</thead> | |
<tbody> | |
</tbody> | |
</table> | |
<table> | |
<caption>headerを動的に生成 動作確認</caption> | |
<thead> | |
<tr v-for="(row,index) in map" :key="index"> | |
<th v-for="(col, i) in row" :key="i" :rowspan="col.rowspan" :colspan="col.colspan"> | |
{{ col.title }} | |
</th> | |
</tr> | |
</thead> | |
<tbody> | |
</tbody> | |
</table> | |
</div> | |
</body> | |
<!-- vue.js --> | |
<script type="module"> | |
import { createApp, ref, watch } from 'Vue' | |
createApp({ | |
setup () { | |
// 変換前の定義 | |
const data = [ | |
{ | |
title: '1' | |
}, | |
{ | |
title: '2', | |
rowspan: 2, | |
child: [ | |
{ | |
title: '2-1' | |
}, | |
{ | |
title: '2-2', | |
child: [ | |
{ title: '2-2-1' }, | |
{ title: '2-2-2' }, | |
] | |
} | |
] | |
}, | |
{ | |
title: '3', | |
}, | |
{ | |
title: '4', | |
child: [ | |
{ | |
title: '4-1', | |
child: [ | |
{ title: '4-1-1' }, | |
{ title: '4-1-2' }, | |
{ | |
title: '4-1-3', | |
child: [ | |
{ title: '4-1-3-1' }, | |
{ title: '4-1-3-2' } | |
] | |
}, | |
] | |
}, | |
{ | |
title: '4-2' | |
}, | |
] | |
} | |
] | |
// 変換後の期待値 | |
const converted = [ | |
[ | |
{ title: '1', rowspan: 4, colspan: 1 }, | |
{ title: '2', rowspan: 2, colspan: 3 }, | |
{ title: '3', rowspan: 4, colspan: 1 }, | |
{ title: '4', rowspan: 1, colspan: 5 } | |
], | |
[ | |
{ title: '4-1', rowspan: 1, colspan: 4 }, | |
{ title: '4-2', rowspan: 3, colspan: 1 } | |
], | |
[ | |
{ title: '2-1', rowspan: 2, colspan: 1 }, | |
{ title: '2-2', rowspan: 1, colspan: 2 }, | |
{ title: '4-1-1', rowspan: 2, colspan: 1 }, | |
{ title: '4-1-2', rowspan: 2, colspan: 1 }, | |
{ title: '4-1-3', rowspan: 1, colspan: 2 }, | |
], | |
[ | |
{ title: '2-2-1', rowspan: 1, colspan: 1 }, | |
{ title: '2-2-2', rowspan: 1, colspan: 1 }, | |
{ title: '4-1-3-1', rowspan: 1, colspan: 1 }, | |
{ title: '4-1-3-2', rowspan: 1, colspan: 1 }, | |
] | |
] | |
// dataのネストの深さを調べる(最深部->rowspan) | |
const getNestLevel = (obj) => { | |
const getMaxDepth = (d) => { | |
let maxDepth = 1 | |
// TODO: check 最深部のカウント | |
if (d.rowspan) { | |
maxDepth = d.rowspan | |
} | |
if (d.child) { | |
d.child.forEach(child => { | |
const depth = 1 + getMaxDepth(child) | |
maxDepth = Math.max(maxDepth, depth) | |
}) | |
} | |
return maxDepth | |
} | |
const results = obj.map(d => getMaxDepth(d)) | |
return Math.max(...results) | |
} | |
// 全子要素(childプロパティ)の数をカウント | |
const countChildren = (obj) => { | |
let count = 0 | |
obj.child.forEach(child => { | |
if (child.child) { | |
count += countChildren(child) | |
} else { | |
count++ | |
} | |
}) | |
return count | |
} | |
// rowspanとcolspanの計算 | |
const calcRowspanAndColspan = (_data) => { | |
// jsonのネストの最深部のレベル | |
const deepestLevel = getNestLevel(_data) | |
const rows = [[]] | |
const calc = (d, level) => { | |
if (!rows[level]) { | |
rows[level] = [] | |
} | |
const hasChild = d.child | |
let offset = 0 | |
// rowspan: 縦方向のセル結合(子要素あり:1,子要素なし:ネストの最深-ネストの現在の深さ) | |
let rowspan | |
if (d.rowspan) { | |
rowspan = d.rowspan | |
offset = d.rowspan - 1 | |
} else if (hasChild) { | |
rowspan = 1 | |
} else { | |
rowspan = deepestLevel - level | |
} | |
// colspan: 横方向のセル結合(子要素あり:全個要素の数,子要素なし:1) | |
const colspan = hasChild ? countChildren(d) : 1 | |
// jsonのネストの深さ=TR要素のindexに変換 | |
rows[level].push({ title: d.title, rowspan, colspan }) | |
if (hasChild) { | |
d.child.forEach((child, i) => calc(child, level + offset + 1)) | |
} | |
} | |
_data.forEach((d, i) => calc(d, 0)) | |
return rows | |
} | |
const map = calcRowspanAndColspan(data) | |
// 実際に列となる項目を取得する | |
const getColumns = (_data) => { | |
const columns = [] | |
const loop = (d) => { | |
if (d.child) { | |
d.child.forEach(c => loop(c)) | |
} else { | |
columns.push(d.title) | |
} | |
} | |
_data.forEach(d => loop(d)) | |
return columns | |
} | |
return { | |
converted, | |
map | |
} | |
} | |
}).mount('#app') | |
</script> | |
<style> | |
table { | |
border-collapse: collapse; | |
} | |
th, | |
td { | |
border: 1px solid black; | |
padding: 10px; | |
text-align: center; | |
} | |
</style> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment