vueless 1.3.9-beta.13 → 1.3.9-beta.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.3.9-beta.13",
3
+ "version": "1.3.9-beta.15",
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",
@@ -57,7 +57,7 @@
57
57
  "@vue/eslint-config-typescript": "^14.6.0",
58
58
  "@vue/test-utils": "^2.4.6",
59
59
  "@vue/tsconfig": "^0.7.0",
60
- "@vueless/storybook": "^1.4.8",
60
+ "@vueless/storybook": "^1.4.9",
61
61
  "eslint": "^9.32.0",
62
62
  "eslint-plugin-storybook": "^10.0.2",
63
63
  "eslint-plugin-vue": "^10.3.0",
@@ -4,33 +4,36 @@ import {
4
4
  ref,
5
5
  computed,
6
6
  watch,
7
+ watchEffect,
7
8
  useSlots,
8
9
  nextTick,
9
10
  onMounted,
10
11
  onBeforeUnmount,
11
12
  useTemplateRef,
13
+ h,
12
14
  } from "vue";
13
15
  import { isEqual } from "lodash-es";
14
16
 
15
- import UEmpty from "../ui.container-empty/UEmpty.vue";
16
- import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
17
- import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
18
- import UTableRow from "./UTableRow.vue";
19
- import UDivider from "../ui.container-divider/UDivider.vue";
20
-
21
17
  import { useUI } from "../composables/useUI";
22
18
  import { useVirtualScroll } from "../composables/useVirtualScroll";
19
+ import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
23
20
  import { getDefaults, cx, getMergedConfig } from "../utils/ui";
24
21
  import { hasSlotContent } from "../utils/helper";
25
- import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
26
-
27
- import defaultConfig from "./config";
28
- import { normalizeColumns, mapRowColumns, getFlatRows, getRowChildrenIds } from "./utilTable";
29
22
 
30
23
  import { PX_IN_REM } from "../constants";
31
24
  import { COMPONENT_NAME } from "./constants";
32
25
 
33
- import type { ComputedRef } from "vue";
26
+ import defaultConfig from "./config";
27
+ import { normalizeColumns, getFlatRows, getRowChildrenIds } from "./utilTable";
28
+ import { StickySide } from "./types";
29
+
30
+ import UEmpty from "../ui.container-empty/UEmpty.vue";
31
+ import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
32
+ import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
33
+ import UDivider from "../ui.container-divider/UDivider.vue";
34
+ import UTableRow from "./UTableRow.vue";
35
+
36
+ import type { ComputedRef, VNode } from "vue";
34
37
  import type { Config as UDividerConfig } from "../ui.container-divider/types";
35
38
  import type {
36
39
  Cell,
@@ -43,8 +46,8 @@ import type {
43
46
  FlatRow,
44
47
  ColumnObject,
45
48
  SearchMatch,
49
+ UTableRowProps,
46
50
  } from "./types";
47
- import { StickySide } from "./types";
48
51
 
49
52
  defineOptions({ inheritAttrs: false });
50
53
 
@@ -133,6 +136,8 @@ const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>
133
136
  const localSelectedRows = shallowRef<Row[]>([]);
134
137
  const localExpandedRows = shallowRef<RowId[]>([]);
135
138
 
139
+ const expandedRowsSet = computed(() => new Set(localExpandedRows.value));
140
+
136
141
  const sortedRows: ComputedRef<FlatRow[]> = computed(() => {
137
142
  const headerKeys = props.columns.map((column) =>
138
143
  typeof column === "object" ? column.key : column,
@@ -206,9 +211,9 @@ const tableRowWidthStyle = computed(() => ({ width: `${tableWidth.value / PX_IN_
206
211
  const flatTableRows = computed(() => getFlatRows(props.rows));
207
212
 
208
213
  const visibleFlatRows = computed(() => {
209
- return flatTableRows.value.filter(
210
- (row) => !row.parentRowId || localExpandedRows.value.includes(row.parentRowId),
211
- );
214
+ const expanded = expandedRowsSet.value;
215
+
216
+ return flatTableRows.value.filter((row) => !row.parentRowId || expanded.has(row.parentRowId));
212
217
  });
213
218
 
214
219
  const virtualScroll = useVirtualScroll({
@@ -219,11 +224,25 @@ const virtualScroll = useVirtualScroll({
219
224
  });
220
225
 
221
226
  const renderedRows = computed(() => {
222
- return props.virtualScroll
223
- ? visibleFlatRows.value.slice(virtualScroll.startIndex.value, virtualScroll.endIndex.value)
224
- : visibleFlatRows.value;
227
+ if (props.virtualScroll) {
228
+ // For virtual scroll, we still need to slice based on visible rows
229
+ // but we need to map back to the actual rows from flatTableRows
230
+ const visibleSlice = visibleFlatRows.value.slice(
231
+ virtualScroll.startIndex.value,
232
+ virtualScroll.endIndex.value,
233
+ );
234
+ const visibleIds = new Set(visibleSlice.map((row) => row.id));
235
+
236
+ return flatTableRows.value.filter((row) => visibleIds.has(row.id));
237
+ }
238
+
239
+ return flatTableRows.value;
225
240
  });
226
241
 
242
+ function isRowVisible(row: FlatRow): boolean {
243
+ return !row.parentRowId || expandedRowsSet.value.has(row.parentRowId);
244
+ }
245
+
227
246
  const searchMatches = computed<SearchMatch[]>(() => {
228
247
  const query = props.search?.toLowerCase();
229
248
 
@@ -310,48 +329,155 @@ const totalSearchMatches = computed(() => {
310
329
  return count;
311
330
  });
312
331
 
313
- watch(totalSearchMatches, (count) => {
314
- emit("search", count);
332
+ const selectedRowIds = computed(() => {
333
+ return new Set(localSelectedRows.value.map((row) => row.id));
315
334
  });
316
335
 
317
- watch(activeMatch, (match) => {
318
- if (!match) return;
336
+ const isSelectedAllRows = computed(() => {
337
+ return localSelectedRows.value.length === flatTableRows.value.length;
338
+ });
319
339
 
320
- if (props.virtualScroll) {
321
- const rowIndex = visibleFlatRows.value.findIndex((row) => row.id === match.rowId);
340
+ // Conditional watchers - only create listeners when their associated features are enabled
341
+ // Using watchEffect to dynamically create/destroy watchers based on feature state
342
+
343
+ // Search-related watchers
344
+ // Note: totalSearchMatches watcher always runs to emit events (even when search is cleared)
345
+ watch(totalSearchMatches, (count) => emit("search", count), { flush: "post" });
346
+
347
+ // activeMatch watcher - only created when search is active
348
+ let stopActiveMatchWatch: (() => void) | null = null;
349
+
350
+ watchEffect(() => {
351
+ if (props.search) {
352
+ // Create watcher if it doesn't exist
353
+ if (!stopActiveMatchWatch) {
354
+ stopActiveMatchWatch = watch(
355
+ activeMatch,
356
+ (match) => {
357
+ if (!match) return;
358
+
359
+ if (props.virtualScroll) {
360
+ const rowIndex = visibleFlatRows.value.findIndex((row) => row.id === match.rowId);
361
+
362
+ if (rowIndex === -1) return;
363
+
364
+ virtualScroll.scrollToIndex(rowIndex);
365
+ } else {
366
+ scrollToRow(match.rowId);
367
+ }
368
+ },
369
+ { flush: "post" },
370
+ );
371
+ }
372
+ } else {
373
+ // Cleanup watcher if it exists
374
+ if (stopActiveMatchWatch) {
375
+ stopActiveMatchWatch();
376
+ stopActiveMatchWatch = null;
377
+ }
378
+ }
379
+ });
380
+
381
+ // Selection-related watchers (only created when selectable is enabled)
382
+ let stopLocalSelectedRowsWatch: (() => void) | null = null;
383
+ let stopSelectedRowsWatch: (() => void) | null = null;
384
+ let stopSelectAllWatch: (() => void) | null = null;
385
+
386
+ watchEffect(() => {
387
+ if (props.selectable) {
388
+ // Create watchers if they don't exist
389
+ if (!stopLocalSelectedRowsWatch) {
390
+ stopLocalSelectedRowsWatch = watch(localSelectedRows, onChangeLocalSelectedRows);
391
+ }
322
392
 
323
- if (rowIndex === -1) return;
393
+ if (!stopSelectedRowsWatch) {
394
+ stopSelectedRowsWatch = watch(() => props.selectedRows, onChangeSelectedRows, {
395
+ immediate: true,
396
+ });
397
+ }
324
398
 
325
- virtualScroll.scrollToIndex(rowIndex);
399
+ if (!stopSelectAllWatch) {
400
+ stopSelectAllWatch = watch(selectAll, onChangeSelectAll);
401
+ }
326
402
  } else {
327
- scrollToRow(match.rowId);
403
+ // Cleanup watchers if they exist
404
+ if (stopLocalSelectedRowsWatch) {
405
+ stopLocalSelectedRowsWatch();
406
+ stopLocalSelectedRowsWatch = null;
407
+ }
408
+
409
+ if (stopSelectedRowsWatch) {
410
+ stopSelectedRowsWatch();
411
+ stopSelectedRowsWatch = null;
412
+ }
413
+
414
+ if (stopSelectAllWatch) {
415
+ stopSelectAllWatch();
416
+ stopSelectAllWatch = null;
417
+ }
328
418
  }
329
419
  });
330
420
 
331
- const selectedRowIds = computed(() => {
332
- return new Set(localSelectedRows.value.map((row) => row.id));
333
- });
421
+ // Expansion watcher (always register as it's a core feature)
422
+ watch(() => props.expandedRows, onChangeExpandedRows, { immediate: true });
334
423
 
335
- const isSelectedAllRows = computed(() => {
336
- return localSelectedRows.value.length === flatTableRows.value.length;
424
+ // Sticky header watcher (only created when stickyHeader is enabled)
425
+ let stopHeaderStickyWatch: (() => void) | null = null;
426
+
427
+ watchEffect(() => {
428
+ if (props.stickyHeader) {
429
+ // Create watcher if it doesn't exist
430
+ if (!stopHeaderStickyWatch) {
431
+ stopHeaderStickyWatch = watch(isHeaderSticky, setHeaderCellWidth);
432
+ }
433
+ } else {
434
+ // Cleanup watcher if it exists
435
+ if (stopHeaderStickyWatch) {
436
+ stopHeaderStickyWatch();
437
+ stopHeaderStickyWatch = null;
438
+ }
439
+ }
337
440
  });
338
441
 
339
- watch(localSelectedRows, onChangeLocalSelectedRows);
340
- watch(() => props.selectedRows, onChangeSelectedRows, { immediate: true });
341
- watch(() => props.expandedRows, onChangeExpandedRows, { immediate: true });
342
- watch(selectAll, onChangeSelectAll);
343
- watch(isHeaderSticky, setHeaderCellWidth);
344
- watch(isFooterSticky, (newValue) =>
345
- newValue ? nextTick(setFooterCellWidth) : setFooterCellWidth(null),
346
- );
442
+ // Sticky footer watcher (only created when stickyFooter is enabled)
443
+ let stopFooterStickyWatch: (() => void) | null = null;
444
+
445
+ watchEffect(() => {
446
+ if (props.stickyFooter) {
447
+ // Create watcher if it doesn't exist
448
+ if (!stopFooterStickyWatch) {
449
+ stopFooterStickyWatch = watch(isFooterSticky, (newValue) =>
450
+ newValue ? nextTick(setFooterCellWidth) : setFooterCellWidth(null),
451
+ );
452
+ }
453
+ } else {
454
+ // Cleanup watcher if it exists
455
+ if (stopFooterStickyWatch) {
456
+ stopFooterStickyWatch();
457
+ stopFooterStickyWatch = null;
458
+ }
459
+ }
460
+ });
347
461
 
348
462
  let resizeObserver: ResizeObserver | null = null;
349
463
  let scrollRafId: number | null = null;
464
+ let resizeDebounceId: ReturnType<typeof setTimeout> | null = null;
465
+
466
+ function scheduleWindowResize(tableRectWidth: number, tableRectHeight: number) {
467
+ if (resizeDebounceId) clearTimeout(resizeDebounceId);
468
+
469
+ resizeDebounceId = setTimeout(() => {
470
+ resizeDebounceId = null;
471
+ onWindowResize();
472
+
473
+ tableHeight.value = tableRectHeight;
474
+ tableWidth.value = tableRectWidth;
475
+ }, 300);
476
+ }
350
477
 
351
478
  onMounted(async () => {
352
479
  document.addEventListener("keyup", onKeyupEsc);
353
480
  document.addEventListener("scroll", onScroll, { passive: true });
354
- window.addEventListener("resize", onWindowResize);
355
481
 
356
482
  await nextTick();
357
483
  updateHeaderOffsetTop();
@@ -363,9 +489,7 @@ onMounted(async () => {
363
489
 
364
490
  if (!entry) return;
365
491
 
366
- tableHeight.value = entry.contentRect.height;
367
- tableWidth.value = entry.contentRect.width;
368
- calculateStickyColumnPositions();
492
+ scheduleWindowResize(entry.contentRect.width, entry.contentRect.height);
369
493
  });
370
494
 
371
495
  resizeObserver.observe(tableWrapperRef.value);
@@ -375,12 +499,15 @@ onMounted(async () => {
375
499
  onBeforeUnmount(() => {
376
500
  document.removeEventListener("keyup", onKeyupEsc);
377
501
  document.removeEventListener("scroll", onScroll);
378
- window.removeEventListener("resize", onWindowResize);
379
502
  resizeObserver?.disconnect();
380
503
 
381
504
  if (scrollRafId !== null) {
382
505
  cancelAnimationFrame(scrollRafId);
383
506
  }
507
+
508
+ if (resizeDebounceId !== null) {
509
+ clearTimeout(resizeDebounceId);
510
+ }
384
511
  });
385
512
 
386
513
  function onChangeSelectedRows() {
@@ -396,8 +523,6 @@ function onChangeExpandedRows() {
396
523
  }
397
524
 
398
525
  function onWindowResize() {
399
- tableWidth.value = tableWrapperRef.value?.offsetWidth || 0;
400
-
401
526
  updateHeaderOffsetTop();
402
527
  setHeaderCellWidth();
403
528
  setFooterCellWidth();
@@ -612,6 +737,85 @@ function onClickCell(cell: Cell, row: Row, key: string | number) {
612
737
  emit("clickCell", cell, row, key);
613
738
  }
614
739
 
740
+ function onBodyClick(event: MouseEvent) {
741
+ const target = event.target as HTMLElement;
742
+
743
+ const row = target.closest("tr");
744
+
745
+ if (!row) return;
746
+
747
+ const rowId = row.getAttribute("data-row-id");
748
+
749
+ if (!rowId) return;
750
+
751
+ const rowData = flatTableRows.value.find((r) => String(r.id) === rowId);
752
+
753
+ if (!rowData) return;
754
+
755
+ // Handle checkbox toggle via event delegation.
756
+ // When unchecking, UCheckbox.onIconClick() calls input.click() which dispatches
757
+ // a second (programmatic) click event with target=INPUT and isTrusted=false.
758
+ // Without this guard, onToggleRowCheckbox fires twice (deselect then reselect).
759
+ const checkboxCell = target.closest("td[data-checkbox-id]");
760
+
761
+ if (checkboxCell) {
762
+ if (target.tagName === "INPUT" && !event.isTrusted) return;
763
+
764
+ onToggleRowCheckbox(rowData);
765
+
766
+ return;
767
+ }
768
+
769
+ // Handle expand icon toggle via event delegation
770
+ const expandIconElement = target.closest("[data-expand-icon]");
771
+
772
+ if (expandIconElement) {
773
+ onToggleExpand(rowData);
774
+
775
+ return;
776
+ }
777
+
778
+ // Handle row click via event delegation
779
+ onClickRow(rowData);
780
+
781
+ // Handle cell click via event delegation
782
+ const cell = target.closest("td");
783
+
784
+ if (cell) {
785
+ const cellKey = cell.getAttribute("data-cell-key");
786
+
787
+ if (cellKey) {
788
+ const cellValue = rowData[cellKey];
789
+
790
+ onClickCell(cellValue, rowData, cellKey);
791
+ }
792
+ }
793
+ }
794
+
795
+ function onBodyDoubleClick(event: MouseEvent) {
796
+ const target = event.target as HTMLElement;
797
+
798
+ const row = target.closest("tr");
799
+
800
+ if (!row) return;
801
+
802
+ const rowId = row.getAttribute("data-row-id");
803
+
804
+ if (!rowId) return;
805
+
806
+ const rowData = flatTableRows.value.find((r) => String(r.id) === rowId);
807
+
808
+ if (!rowData) return;
809
+
810
+ const selection = window.getSelection();
811
+
812
+ if (selection) {
813
+ selection.removeAllRanges();
814
+ }
815
+
816
+ onDoubleClickRow(rowData);
817
+ }
818
+
615
819
  function onChangeSelectAll(selectAll: boolean) {
616
820
  if (selectAll && canSelectAll.value) {
617
821
  localSelectedRows.value = [...flatTableRows.value];
@@ -631,6 +835,7 @@ function onChangeLocalSelectedRows(selectedRows: Row[]) {
631
835
  nextTick(setHeaderCellWidth);
632
836
  }
633
837
 
838
+ // Set flag to prevent selectAll watcher from triggering
634
839
  selectAll.value = !!selectedRows.length;
635
840
 
636
841
  emit("update:selectedRows", localSelectedRows.value);
@@ -642,9 +847,8 @@ function clearSelectedItems() {
642
847
 
643
848
  function onToggleExpand(row: Row) {
644
849
  const expanded = localExpandedRows.value;
645
- const targetIndex = expanded.indexOf(row.id);
646
850
 
647
- if (~targetIndex) {
851
+ if (expandedRowsSet.value.has(row.id)) {
648
852
  const idsToRemove = new Set([row.id, ...getRowChildrenIds(row)]);
649
853
 
650
854
  localExpandedRows.value = expanded.filter((id) => !idsToRemove.has(id));
@@ -853,6 +1057,68 @@ const tableRowAttrs = {
853
1057
  bodyCellSearchMatchActiveAttrs,
854
1058
  bodyCellSearchMatchTextActiveAttrs,
855
1059
  } as unknown as UTableRowAttrs;
1060
+
1061
+ function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
1062
+ if (!isShownDateDivider(rowIndex) || !row.rowDate) return null;
1063
+
1064
+ const isSelected = isRowSelectedWithin(rowIndex);
1065
+
1066
+ const propsDateDivider = isSelected
1067
+ ? bodyRowCheckedDateDividerAttrs.value
1068
+ : bodyRowDateDividerAttrs.value;
1069
+
1070
+ const dividerNode = h(UDivider, {
1071
+ label: getDateDividerData(row.rowDate).label,
1072
+ ...(isSelected ? bodySelectedDateDividerAttrs.value : bodyDateDividerAttrs.value),
1073
+ config: getDateDividerConfig(row, isSelected),
1074
+ });
1075
+
1076
+ return h("tr", { ...propsDateDivider, key: `date-divider-${row.id}` }, [
1077
+ h(
1078
+ "td",
1079
+ {
1080
+ ...bodyCellDateDividerAttrs.value,
1081
+ colspan: colsCount.value,
1082
+ },
1083
+ [dividerNode],
1084
+ ),
1085
+ ]);
1086
+ }
1087
+
1088
+ function renderTableRow(row: FlatRow, rowIndex: number): VNode {
1089
+ return h(
1090
+ UTableRow,
1091
+ {
1092
+ key: row.id,
1093
+ selectable: props.selectable,
1094
+ rowIndex,
1095
+ row,
1096
+ columns: normalizedColumns.value,
1097
+ config: config.value,
1098
+ attrs: tableRowAttrs as unknown as UTableRowAttrs,
1099
+ colsCount: colsCount.value,
1100
+ nestedLevel: Number(row.nestedLevel || 0),
1101
+ emptyCellLabel: props.emptyCellLabel,
1102
+ "data-test": getDataTest("row"),
1103
+ "data-row-id": row.id,
1104
+ isExpanded: expandedRowsSet.value.has(row.id),
1105
+ isChecked: isRowSelected(row),
1106
+ isVisible: isRowVisible(row),
1107
+ columnPositions: columnPositions.value,
1108
+ search: props.search,
1109
+ searchMatchColumns: getRowSearchMatchColumns(row),
1110
+ activeSearchMatchColumn: getRowActiveSearchMatchColumn(row),
1111
+ textEllipsis: props.textEllipsis,
1112
+ } as unknown as UTableRowProps,
1113
+ slots,
1114
+ );
1115
+ }
1116
+
1117
+ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1118
+ return [renderDateDividerRow(row, rowIndex), renderTableRow(row, rowIndex)].filter(
1119
+ Boolean,
1120
+ ) as VNode[];
1121
+ }
856
1122
  </script>
857
1123
 
858
1124
  <template>
@@ -893,10 +1159,10 @@ const tableRowAttrs = {
893
1159
  >
894
1160
  <template v-if="hasSlotContent($slots[`header-${column.key}`], { column, index })">
895
1161
  <!--
896
- @slot Use it to customize needed header cell.
897
- @binding {object} column
898
- @binding {number} index
899
- -->
1162
+ @slot Use it to customize needed header cell.
1163
+ @binding {object} column
1164
+ @binding {number} index
1165
+ -->
900
1166
  <slot :name="`header-${column.key}`" :column="column" :index="index" />
901
1167
  </template>
902
1168
 
@@ -1042,7 +1308,12 @@ const tableRowAttrs = {
1042
1308
  <ULoaderProgress :loading="loading" v-bind="headerLoaderAttrs" />
1043
1309
  </thead>
1044
1310
 
1045
- <tbody v-if="sortedRows.length" v-bind="bodyAttrs">
1311
+ <tbody
1312
+ v-if="sortedRows.length"
1313
+ v-bind="bodyAttrs"
1314
+ @click="onBodyClick"
1315
+ @dblclick="onBodyDoubleClick"
1316
+ >
1046
1317
  <tr
1047
1318
  v-if="hasBeforeFirstRowSlot"
1048
1319
  v-bind="isRowSelected(sortedRows[0]) ? beforeBodyRowCheckedAttrs : beforeBodyRowAttrs"
@@ -1060,105 +1331,9 @@ const tableRowAttrs = {
1060
1331
  />
1061
1332
  </tr>
1062
1333
 
1063
- <template v-for="(row, rowIndex) in renderedRows" :key="row.id">
1064
- <tr
1065
- v-if="isShownDateDivider(rowIndex) && !isRowSelectedWithin(rowIndex) && row.rowDate"
1066
- v-bind="bodyRowDateDividerAttrs"
1067
- >
1068
- <td v-bind="bodyCellDateDividerAttrs" :colspan="colsCount">
1069
- <UDivider
1070
- :label="getDateDividerData(row.rowDate).label"
1071
- v-bind="bodyDateDividerAttrs"
1072
- :config="getDateDividerConfig(row, false)"
1073
- />
1074
- </td>
1075
- </tr>
1076
-
1077
- <tr
1078
- v-if="isShownDateDivider(rowIndex) && isRowSelectedWithin(rowIndex) && row.rowDate"
1079
- v-bind="bodyRowCheckedDateDividerAttrs"
1080
- >
1081
- <td v-bind="bodyCellDateDividerAttrs" :colspan="colsCount">
1082
- <UDivider
1083
- :label="getDateDividerData(row.rowDate).label"
1084
- v-bind="bodySelectedDateDividerAttrs"
1085
- :config="getDateDividerConfig(row, true)"
1086
- />
1087
- </td>
1088
- </tr>
1089
-
1090
- <UTableRow
1091
- :selectable="selectable"
1092
- :row="row"
1093
- :columns="normalizedColumns"
1094
- :config="config"
1095
- :attrs="tableRowAttrs as unknown as UTableRowAttrs"
1096
- :cols-count="colsCount"
1097
- :nested-level="Number(row.nestedLevel || 0)"
1098
- :empty-cell-label="emptyCellLabel"
1099
- :data-test="getDataTest('row')"
1100
- :data-row-id="row.id"
1101
- :is-expanded="localExpandedRows.includes(row.id)"
1102
- :is-checked="isRowSelected(row)"
1103
- :column-positions="columnPositions"
1104
- :search="search"
1105
- :search-match-columns="getRowSearchMatchColumns(row)"
1106
- :active-search-match-column="getRowActiveSearchMatchColumn(row)"
1107
- :text-ellipsis="textEllipsis"
1108
- @click="onClickRow"
1109
- @dblclick="onDoubleClickRow"
1110
- @click-cell="onClickCell"
1111
- @toggle-expand="onToggleExpand"
1112
- @toggle-checkbox="onToggleRowCheckbox"
1113
- >
1114
- <template
1115
- v-for="(value, key, cellIndex) in mapRowColumns(row, normalizedColumns)"
1116
- :key="`${rowIndex}-${cellIndex}`"
1117
- #[`cell-${key}`]="{ value: cellValue, row: cellRow }"
1118
- >
1119
- <!--
1120
- @slot Use it to customize needed table cell.
1121
- @binding {string} value
1122
- @binding {object} row
1123
- @binding {number} index
1124
- @binding {number} cellIndex
1125
- -->
1126
- <slot
1127
- :name="`cell-${key}`"
1128
- :value="cellValue"
1129
- :row="cellRow"
1130
- :index="rowIndex"
1131
- :cell-index="cellIndex"
1132
- />
1133
- </template>
1134
-
1135
- <template #expand="{ row: expandedRow, expanded }">
1136
- <!--
1137
- @slot Use it to customize row expand icon.
1138
- @binding {object} row
1139
- @binding {boolean} expanded
1140
- @binding {number} index
1141
- -->
1142
- <slot name="expand" :index="rowIndex" :row="expandedRow" :expanded="expanded" />
1143
- </template>
1144
-
1145
- <template #nested-row>
1146
- <!--
1147
- @slot Use it to add inside nested row.
1148
- @binding {object} row
1149
- @binding {number} index
1150
- @binding {number} nestedLevel
1151
- -->
1152
- <slot
1153
- v-if="row"
1154
- name="nested-row"
1155
- :index="rowIndex"
1156
- :row="row"
1157
- :nested-level="Number(row.nestedLevel || 0)"
1158
- />
1159
- </template>
1160
- </UTableRow>
1161
- </template>
1334
+ <component
1335
+ :is="() => renderedRows.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat()"
1336
+ />
1162
1337
 
1163
1338
  <tr v-if="props.virtualScroll && virtualScroll.bottomSpacerHeight.value > 0">
1164
1339
  <td