calc-table.ts
ts
import {
type Component,
type State,
asInteger,
component,
insertOrRemoveElement,
on,
setProperty,
setText,
state,
} from '../../../'
import { FormSpinbuttonProps } from '../form-spinbutton/form-spinbutton'
export type CalcTableProps = {
columns: number
rows: number
}
export default component(
'calc-table',
{
columns: asInteger(),
rows: asInteger(),
},
(el, { all, first }) => {
const colHeads = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const rowTemplate =
el.querySelector<HTMLTemplateElement>('.calc-table-row')
const colheadTemplate = el.querySelector<HTMLTemplateElement>(
'.calc-table-colhead',
)
const cellTemplate =
el.querySelector<HTMLTemplateElement>('.calc-table-cell')
if (!rowTemplate || !colheadTemplate || !cellTemplate)
throw new Error('Missing template elements')
const colSums = new Map<string, State<number>>()
for (let i = 0; i < el.columns; i++) {
colSums.set(colHeads[i], state(0))
}
const calcColumnSum = (rowKey: string): number => {
return Array.from(
el.querySelectorAll<HTMLInputElement>(
`tbody input[data-key="${rowKey}"]`,
),
)
.map(input =>
Number.isFinite(input.valueAsNumber)
? input.valueAsNumber
: 0,
)
.reduce((acc, val) => acc + val, 0)
}
return [
/* Control number of rows / columns */
setProperty(
'rows',
() =>
el.querySelector<Component<FormSpinbuttonProps>>(
'.rows form-spinbutton',
)?.value,
),
setProperty(
'columns',
() =>
el.querySelector<Component<FormSpinbuttonProps>>(
'.columns form-spinbutton',
)?.value,
),
/* Create rows */
first(
'tbody',
insertOrRemoveElement(
target => el.rows - target.querySelectorAll('tr').length,
{
position: 'beforeend',
create: parent => {
const row = document.importNode(
rowTemplate.content,
true,
).firstElementChild
if (!(row instanceof HTMLTableRowElement))
throw new Error(
`Expected <tr> as root in table row template, got ${row}`,
)
const rowKey = String(
parent.querySelectorAll('tr').length + 1,
)
row.dataset['key'] = rowKey
row.querySelector('slot')?.replaceWith(
document.createTextNode(rowKey),
)
return row
},
resolve: () => {
for (const [colKey, colSum] of colSums) {
colSum.set(calcColumnSum(colKey))
}
},
},
),
),
/* Create column headers */
first(
'thead tr',
insertOrRemoveElement(
target =>
el.columns - (target.querySelectorAll('th').length - 1),
{
position: 'beforeend',
create: parent => {
const cell = document.importNode(
colheadTemplate.content,
true,
).firstElementChild
if (!(cell instanceof HTMLTableCellElement))
throw new Error(
`Expected <th> as root in column header template, got ${cell}`,
)
const colKey =
colHeads[
parent.querySelectorAll('th').length - 1
]
colSums.set(colKey, state(0))
cell.querySelector('slot')?.replaceWith(
document.createTextNode(colKey),
)
return cell
},
},
),
),
/* Create input cells for each row */
all(
'tbody tr',
insertOrRemoveElement(
target => el.columns - target.querySelectorAll('td').length,
{
position: 'beforeend',
create: (parent: HTMLElement) => {
const cell = document.importNode(
cellTemplate.content,
true,
).firstElementChild
if (!(cell instanceof HTMLTableCellElement))
throw new Error(
`Expected <td> as root in cell template, got ${cell}`,
)
const rowKey = parent.dataset['key']
const colKey =
colHeads[parent.querySelectorAll('td').length]
const input = cell.querySelector('input')
if (!input)
throw new Error(
'No input found in cell template',
)
input.dataset['key'] = colKey
cell.querySelector('slot')?.replaceWith(
document.createTextNode(`${colKey}${rowKey}`),
)
return cell
},
},
),
),
/* Create empty cells for column sums */
first(
'tfoot tr',
insertOrRemoveElement(
target => el.columns - target.querySelectorAll('td').length,
{
position: 'beforeend',
create: parent => {
const cell = document.createElement('td')
const colKey =
colHeads[parent.querySelectorAll('td').length]
cell.dataset['key'] = colKey
return cell
},
},
),
),
/* Update column values when cells change */
all(
'tbody input',
on('change', e => {
const colKey = (e.target as HTMLInputElement)?.dataset[
'key'
]
colSums.get(colKey!)?.set(calcColumnSum(colKey!))
}),
),
/* Update sums for each column */
all(
'tfoot td',
setText(target =>
String(colSums.get(target.dataset['key']!)!.get()),
),
),
]
},
)
declare global {
interface HTMLElementTagNameMap {
'calc-table': Component<CalcTableProps>
}
}