vueless 1.4.12-beta.7 → 1.4.12-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composables/useUI.ts +19 -1
- package/package.json +1 -1
- package/ui.data-table/UTable.vue +70 -5
- package/ui.data-table/UTableRow.vue +4 -3
package/composables/useUI.ts
CHANGED
|
@@ -228,6 +228,16 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
228
228
|
const keysAttrs: KeysAttrs<T> = {};
|
|
229
229
|
const attrsRefs: Record<string, Ref<KeyAttrs>> = {};
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Structural signature per key. The watcher below fires on any config/prop/class
|
|
233
|
+
* change and would otherwise re-mint every key's attrs object — even keys whose
|
|
234
|
+
* classes are identical — invalidating downstream reactive readers (e.g. every
|
|
235
|
+
* body row of a table on an unrelated sticky toggle). We skip the `.value` write
|
|
236
|
+
* when the new object is structurally equal to the last one, so the ref keeps its
|
|
237
|
+
* identity and dependents are not re-rendered.
|
|
238
|
+
*/
|
|
239
|
+
const attrsSignatures: Record<string, string> = {};
|
|
240
|
+
|
|
231
241
|
for (const key in config.value) {
|
|
232
242
|
if (isSystemKey(key)) continue;
|
|
233
243
|
|
|
@@ -266,12 +276,20 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
|
|
|
266
276
|
/* Delete value key to prevent v-model overwrite. */
|
|
267
277
|
delete commonAttrs.value;
|
|
268
278
|
|
|
269
|
-
|
|
279
|
+
const nextValue: KeyAttrs = {
|
|
270
280
|
...commonAttrs,
|
|
271
281
|
class: cx([...data.extendsClasses, classes, commonAttrs.class]),
|
|
272
282
|
config: data.mergedNestedConfig,
|
|
273
283
|
...data.mergedDefaults,
|
|
274
284
|
};
|
|
285
|
+
|
|
286
|
+
/* Keep the previous ref identity when nothing changed — see attrsSignatures. */
|
|
287
|
+
const signature = JSON.stringify(nextValue);
|
|
288
|
+
|
|
289
|
+
if (attrsSignatures[key] === signature) continue;
|
|
290
|
+
|
|
291
|
+
attrsSignatures[key] = signature;
|
|
292
|
+
attrsRefs[key].value = nextValue;
|
|
275
293
|
}
|
|
276
294
|
},
|
|
277
295
|
{ immediate: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueless",
|
|
3
|
-
"version": "1.4.12-beta.
|
|
3
|
+
"version": "1.4.12-beta.9",
|
|
4
4
|
"description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
|
|
5
5
|
"author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
|
|
6
6
|
"homepage": "https://vueless.com",
|
package/ui.data-table/UTable.vue
CHANGED
|
@@ -1064,7 +1064,7 @@ const {
|
|
|
1064
1064
|
skeletonCheckboxAttrs,
|
|
1065
1065
|
} = useUI<Config>(defaultConfig, mutatedProps);
|
|
1066
1066
|
|
|
1067
|
-
/* Plain object — inner refs are already reactive. */
|
|
1067
|
+
/* Plain object — inner refs are already reactive and identity-stable (see useUI). */
|
|
1068
1068
|
const tableRowAttrs = {
|
|
1069
1069
|
bodyCellContentAttrs,
|
|
1070
1070
|
bodyCellCheckboxAttrs,
|
|
@@ -1111,8 +1111,40 @@ function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
|
|
|
1111
1111
|
]);
|
|
1112
1112
|
}
|
|
1113
1113
|
|
|
1114
|
+
/**
|
|
1115
|
+
* Per-row VNode memo cache. Toggling one checkbox invalidates the `selectedRowIds`
|
|
1116
|
+
* computed, which re-runs the body render function. Without memoization every row's
|
|
1117
|
+
* VNode would be rebuilt and diffed on each toggle (200+ rows → visible lag). We cache
|
|
1118
|
+
* each row's VNode keyed by row id and only rebuild it when an input that actually
|
|
1119
|
+
* affects that row changes — so an unrelated row keeps the same VNode reference and
|
|
1120
|
+
* Vue skips it entirely during patch.
|
|
1121
|
+
*/
|
|
1122
|
+
const rowVNodeCache = new Map<RowId, { signature: string; row: FlatRow; vnode: VNode }>();
|
|
1123
|
+
|
|
1124
|
+
function getRowSignature(row: FlatRow, rowIndex: number): string {
|
|
1125
|
+
return [
|
|
1126
|
+
rowIndex,
|
|
1127
|
+
Number(isRowSelected(row)),
|
|
1128
|
+
Number(expandedRowsSet.value.has(row.id)),
|
|
1129
|
+
Number(isRowVisible(row)),
|
|
1130
|
+
props.selectable ? 1 : 0,
|
|
1131
|
+
props.search || "",
|
|
1132
|
+
getRowActiveSearchMatchColumn(row) || "",
|
|
1133
|
+
[...(getRowSearchMatchColumns(row) || [])].join(","),
|
|
1134
|
+
].join("|");
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1114
1137
|
function renderTableRow(row: FlatRow, rowIndex: number): VNode {
|
|
1115
|
-
|
|
1138
|
+
const signature = getRowSignature(row, rowIndex);
|
|
1139
|
+
const cached = rowVNodeCache.get(row.id);
|
|
1140
|
+
|
|
1141
|
+
// `row` identity guards against stale data: `flatTableRows` yields fresh row
|
|
1142
|
+
// objects whenever `props.rows` changes, so a new reference means new cell data.
|
|
1143
|
+
if (cached && cached.row === row && cached.signature === signature) {
|
|
1144
|
+
return cached.vnode;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const vnode = h(
|
|
1116
1148
|
UTableRow,
|
|
1117
1149
|
{
|
|
1118
1150
|
key: row.id,
|
|
@@ -1138,13 +1170,48 @@ function renderTableRow(row: FlatRow, rowIndex: number): VNode {
|
|
|
1138
1170
|
} as unknown as UTableRowProps,
|
|
1139
1171
|
slots,
|
|
1140
1172
|
);
|
|
1173
|
+
|
|
1174
|
+
rowVNodeCache.set(row.id, { signature, row, vnode });
|
|
1175
|
+
|
|
1176
|
+
return vnode;
|
|
1141
1177
|
}
|
|
1142
1178
|
|
|
1179
|
+
/* Column layout / config changes affect every row — invalidate the whole cache. */
|
|
1180
|
+
watch(
|
|
1181
|
+
[
|
|
1182
|
+
normalizedColumns,
|
|
1183
|
+
config,
|
|
1184
|
+
columnPositions,
|
|
1185
|
+
() => props.textEllipsis,
|
|
1186
|
+
() => props.emptyCellLabel,
|
|
1187
|
+
],
|
|
1188
|
+
() => rowVNodeCache.clear(),
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
/* Drop cache entries for rows that no longer exist (filters, pagination reset). */
|
|
1192
|
+
watch(flatTableRows, (rows) => {
|
|
1193
|
+
const liveIds = new Set(rows.map((row) => row.id));
|
|
1194
|
+
|
|
1195
|
+
for (const id of rowVNodeCache.keys()) {
|
|
1196
|
+
if (!liveIds.has(id)) rowVNodeCache.delete(id);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1143
1200
|
function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
1144
1201
|
return [renderDateDividerRow(row, rowIndex), renderTableRow(row, rowIndex)].filter(
|
|
1145
1202
|
Boolean,
|
|
1146
1203
|
) as VNode[];
|
|
1147
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Stable functional component for the body rows. Defined once so its type
|
|
1208
|
+
* identity never changes across parent re-renders — a previous inline `:is`
|
|
1209
|
+
* arrow created a new type on every render, forcing Vue to unmount and rebuild
|
|
1210
|
+
* the entire tbody (e.g. on a sticky-header toggle). Reading `renderedRows`
|
|
1211
|
+
* through the closure keeps it reactive while row keys drive reconciliation.
|
|
1212
|
+
*/
|
|
1213
|
+
const BodyRows = () =>
|
|
1214
|
+
renderedRows.value.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat();
|
|
1148
1215
|
</script>
|
|
1149
1216
|
|
|
1150
1217
|
<template>
|
|
@@ -1362,9 +1429,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1362
1429
|
/>
|
|
1363
1430
|
</tr>
|
|
1364
1431
|
|
|
1365
|
-
<component
|
|
1366
|
-
:is="() => renderedRows.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat()"
|
|
1367
|
-
/>
|
|
1432
|
+
<component :is="BodyRows" />
|
|
1368
1433
|
|
|
1369
1434
|
<tr v-if="props.virtualScroll && virtualScroll.bottomSpacerHeight.value > 0">
|
|
1370
1435
|
<td
|
|
@@ -400,6 +400,7 @@ function renderNestedFirstCell(value: Cell, key: string, cellIndex: number): VNo
|
|
|
400
400
|
|
|
401
401
|
function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
402
402
|
const keyStr = String(key);
|
|
403
|
+
const column = props.columns.find((col) => col.key === key) ?? props.columns[index];
|
|
403
404
|
|
|
404
405
|
const nestedCellNode = isNestedFirstCell(index)
|
|
405
406
|
? renderNestedFirstCell(value, key, index)
|
|
@@ -414,12 +415,12 @@ function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
|
414
415
|
...props.attrs.bodyCellBaseAttrs.value,
|
|
415
416
|
class: cx([
|
|
416
417
|
props.attrs.bodyCellBaseAttrs.value.class,
|
|
417
|
-
|
|
418
|
+
column.tdClass,
|
|
418
419
|
getCellClasses(props.row, keyStr),
|
|
419
|
-
getStickyColumnClass(
|
|
420
|
+
getStickyColumnClass(column),
|
|
420
421
|
getSearchMatchCellClass(keyStr),
|
|
421
422
|
]),
|
|
422
|
-
style: getStickyColumnStyle(
|
|
423
|
+
style: getStickyColumnStyle(column),
|
|
423
424
|
"data-cell-key": key,
|
|
424
425
|
"data-test": getDataTest(`${key}-cell`),
|
|
425
426
|
},
|