I'm working with Solid.js and have a table with rows and cells. I'm using signals to manage the selected row and cell indices, but I need to update a ref (reference to the selected cell) based on those signals in order to manipulate the table's scroll when the content overflows.
The issue is that I don't want to use querySelector, as I don't consider it a good practice, and I also want the reference to update according to the state of the signals.
Here is a simplified version of my implementation:
import { batch, Component, createEffect, createSelector, createSignal, For, JSX } from "solid-js";
export const DataGrid: Component = () => {
const rows = Array.from({ length: 10 });
const cells = Array.from({ length: 10 });
const [selectedRowIndex, setSelectedRowIndex] = createSignal(0);
const [selectedCellIndex, setSelectedCellIndex] = createSignal(0);
const isSelectedRow = createSelector(selectedRowIndex);
const isSelectedCell = createSelector(selectedCellIndex);
const keysToPrevent = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
const onKeyDown: JSX.EventHandler<HTMLDivElement, KeyboardEvent> = (event) => {
const { key, ctrlKey } = event;
if (keysToPrevent.includes(key)) {
event.preventDefault();
}
const rowLength = rows.length;
const cellLength = cells.length;
switch (key) {
case "ArrowUp":
if (ctrlKey) {
setSelectedRowIndex(0);
} else if (selectedRowIndex() > 0) {
setSelectedRowIndex((v) => v - 1);
}
break;
case "ArrowDown":
if (ctrlKey) {
setSelectedRowIndex(rowLength - 1);
} else if (selectedRowIndex() < rowLength - 1) {
setSelectedRowIndex((v) => v + 1);
}
break;
case "ArrowLeft":
if (ctrlKey) {
setSelectedCellIndex(0);
} else if (selectedCellIndex() > 0) {
setSelectedCellIndex((v) => v - 1);
}
break;
case "ArrowRight":
if (ctrlKey) {
setSelectedCellIndex(cellLength - 1);
} else if (selectedCellIndex() < cellLength - 1) {
setSelectedCellIndex((v) => v + 1);
}
break;
}
};
const onMouseDown: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {
const rowIndex = parseInt(event.currentTarget.dataset.rowIndex!);
const colIndex = parseInt(event.currentTarget.dataset.colIndex!);
batch(() => {
setSelectedRowIndex(rowIndex);
setSelectedCellIndex(colIndex);
});
};
let tableRef!: HTMLDivElement;
let selectedCellRef!: HTMLDivElement;
createEffect(() => {
selectedRowIndex();
selectedCellIndex();
/* I need to update scrollTop and scrollLeft of the table
when the selected cell changes. */
});
return (
<div class="table" tabIndex={0} onKeyDown={onKeyDown} ref={tableRef}>
<For each={rows}>
{(_, rowIndex) => (
<div class="row" classList={{ selected: isSelectedRow(rowIndex()) }}>
<For each={cells}>
{(_, cellIndex) => (
<div
class="cell"
classList={{ selected: isSelectedRow(rowIndex()) && isSelectedCell(cellIndex()) }}
data-row-index={rowIndex()}
data-col-index={cellIndex()}
onMouseDown={onMouseDown}
>
row {rowIndex()} - cell {cellIndex()}
</div>
)}
</For>
</div>
)}
</For>
</div>
);
};
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--cell-height: 27px;
--cell-border-color: #caddf7;
}
html {
font-size: 12px;
font-family: "Tahoma";
}
a,
input,
button,
select,
textarea {
font-size: inherit;
font-family: inherit;
}
body {
padding: 20px;
}
.table {
width: 400px;
height: 400px;
outline: none;
overflow: auto;
border: 1px solid #8ea6c3;
user-select: none;
}
.row {
display: flex;
width: fit-content;
}
.cell {
width: 100px;
flex-shrink: 0;
height: var(--cell-height);
line-height: var(--cell-height);
padding-inline: 4px;
border-right: 1px solid var(--cell-border-color);
border-bottom: 1px solid var(--cell-border-color);
&.selected {
box-shadow: inset 0 0 0 1px blue;
}
}