FreeGridv1.15.1한국어

Merged cells

merge: true joins consecutive cells with the same value into one tall cell, and rowSpan sets how many rows a cell covers, as in a printed report.

Live demoMerged cells

Neighbouring cells with the same value become one tall cell, as in a report.

Code for this demo
import { createGrid, type ColumnDef, type Row } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, select, separator, text, toggle, tr, type DemoContext } from '../ui';

export default function mount(ctx: DemoContext) {
  const { lang } = ctx;
  const h = (en: string, ko: string) => tr(lang, { en, ko });
  // sorted by country, then company, so identical neighbours sit together
  const data: Row[] = makeRows(300).sort((a, b) => a.country.localeCompare(b.country) || a.company.localeCompare(b.company));
  let byCompany = true;
  let align: 'top' | 'middle' | 'bottom' = 'top';
  const columns = (): ColumnDef[] => [
    { field: 'country', header: h('Country', '국가'), width: 150, merge: true, mergeAlign: align },
    // companies merge only inside the same country
    { field: 'company', header: h('Company', '회사'), width: 150,
      merge: byCompany ? (a, b, rowA, rowB) => a === b && rowA.country === rowB.country : false, mergeAlign: align },
    { field: 'name', header: h('Name', '이름'), width: 170 },
    { field: 'q1', header: h('Q1 revenue', '1분기 매출'), type: 'number', format: '#,##0', width: 150 },
    { field: 'q2', header: h('Q2 revenue', '2분기 매출'), type: 'number', format: '#,##0', width: 150 },
  ];
  // sortWithinMerges: sorting by revenue keeps each country and company block in place
  const grid = createGrid(ctx.host, { columns: columns(), data, locale: lang, theme: 'simple', rowNumbers: true, sortWithinMerges: true, rowSelection: 'multiple' });

  text(ctx, h('Countries and companies with the same value one after another become one tall cell. Sort by a revenue column: rows move only inside their block.',
    '같은 국가·회사가 이어지면 한 칸으로 합쳐집니다. 매출 컬럼으로 정렬해 보세요. 행은 자기 블록 안에서만 움직입니다.'));
  separator(ctx);
  toggle(ctx, h('Merge companies too', '회사도 병합'), true, on => { byCompany = on; grid.setColumns(columns()); ctx.log(`company merge: ${on}`); });
  select(ctx, h('Text', '글자 위치'), [['top', h('Top', '위')], ['middle', h('Middle', '가운데')], ['bottom', h('Bottom', '아래')]], 'top',
    v => { align = v; grid.setColumns(columns()); ctx.log(`mergeAlign: ${v}`); });
  toggle(ctx, h('Sort within blocks', '병합 안에서만 정렬'), true, on => { grid.setSortWithinMerges(on); ctx.log(`sortWithinMerges: ${on}`); });
  separator(ctx);
  // mergeSelection: one checkbox per row, per country block (level 1) or per company block (level 2)
  select(ctx, h('Checkboxes', '체크박스'), [['rows', h('Every row', '모든 행')], ['1', h('Level 1 (country)', '1차(국가)')], ['2', h('Level 2 (company)', '2차(회사)')]], 'rows', v => {
    grid.setMergeSelection(v === 'rows' ? 'rows' : Number(v));
    ctx.log(`mergeSelection: ${v}`);
  });
  button(ctx, h('Show selected groups', '선택한 그룹 보기'), () => {
    const groups = grid.getSelectedGroups();
    if (!groups.length) {
      ctx.log(grid.getMergeSelection() === 'rows'
        ? h(`${grid.getSelectedRows().length} rows selected (pick level 1 or 2 to get groups)`, `${grid.getSelectedRows().length}행 선택됨(1차·2차를 고르면 그룹으로 나옵니다)`)
        : h('No group selected', '선택한 그룹이 없습니다'));
      return;
    }
    for (const g of groups) ctx.log(`${Object.values(g.values).join(' / ')}: ${g.rows.length}${h(' rows', '행')}`);
  });
  button(ctx, h('Groups with all rows', '그룹 전체 행 보기'), () => {
    const groups = grid.getSelectedGroupsAllRows();
    if (!groups.length) { ctx.log(h('No group selected (pick level 1 or 2)', '선택한 그룹이 없습니다(1차·2차를 고르세요)')); return; }
    for (const g of groups) ctx.log(`${Object.values(g.values).join(' / ')}: ${h('all', '전체')} ${g.rows.length}${h(' rows', '행')}`);
  });
  separator(ctx);
  // merged cells take the theme's colours, borders and fonts like any other cell
  select(ctx, h('Theme', '테마'), [
    ['simple', h('Simple', '심플형')],
    ['erp', h('ERP (Korean)', '관리자·ERP형')],
    ['light', 'Light'], ['dark', 'Dark'],
    ['board', h('Board (Korean)', '게시판형')],
    ['excel', h('Excel-like', '엑셀형')],
    ['classic', h('Classic navy', '진한 헤더형')],
  ], 'simple', v => { grid.setTheme(v); ctx.log(`theme: ${v}`); });
  return grid;
}

Merged cells turn a run of identical values into one tall cell, the way a printed report shows a region once beside all of its rows. Set merge: true on a column and consecutive rows with the same value merge; sort by that column first so equal values sit together. A function decides for itself, for example to merge teams only inside the same region.

rowSpan says outright how many rows a cell covers, from your data, such as a project that spans the next three tasks. When both are set on a column, rowSpan wins. Merges follow sorting and filtering, and Excel exports keep them as merged cells.

createGrid(el, {
  columns: [
    // neighbours with the same value become one tall cell; sort by this column first
    { field: 'region', merge: true },
    // a function decides: merge teams only inside the same region
    { field: 'team', merge: (a, b, rowA, rowB) => a === b && rowA.region === rowB.region },
    // or say how many rows a cell covers; rowSpan wins over merge
    { field: 'project', rowSpan: ({ row }) => row.projectRows ?? 1 },
    { field: 'q1', type: 'number' },
  ],
  data: rowsSortedByRegion,
});

Place the text with mergeAlign ('top', 'middle' or 'bottom'); when a block is taller than the view, the text stays in the part you can see while you scroll. With sortWithinMerges: true, sorting keeps the blocks in place and reorders only the rows inside each one: merge by country and company, sort by revenue, and every company is sorted by revenue on its own.

Checkboxes per block

With rowSelection, mergeSelection decides where the checkboxes go. 'rows' (the default) puts one on every row. A number puts one checkbox on each block of that merge column, counted from the left among the columns with merge: 1 gives one per country, 2 one per company inside a country. The box is drawn over its block like a merged cell and checks every row of it; Shift checks the blocks in between, and a block where only some rows are checked shows a dash. getSelectedGroups() hands the selection back block by block, with the selected rows of each block; getSelectedGroupsAllRows() gives every row of those blocks instead, even when only some were checked.

const grid = createGrid(el, {
  columns: [
    { field: 'country', merge: true },
    { field: 'company', merge: true },
    { field: 'name' },
  ],
  data,
  rowSelection: 'multiple',
  mergeSelection: 1, // one checkbox per country
});

grid.setMergeSelection(2); // one per company inside a country

grid.getSelectedGroups();
// [{ level: 2, values: { country: 'KR', company: 'Acme' }, rows: [...] }, ...]
grid.getSelectedGroupsAllRows(); // the same blocks, rows = every row of each block
grid.getSelectedRows(); // still every selected row, flat

Merging works on rows in the browser: it is skipped while the rows are grouped and with server data (datasource). To span columns instead of rows, use colSpan, which is free.

Column options

NameTypeDescription
mergeboolean | (a, b, rowA, rowB) => booleanConsecutive rows with the same value (or for which the function returns true) become one tall cell.
rowSpan({ value, row, rowIndex }) => numberHow many rows this cell covers, decided by your data. Wins over merge in the same column.
mergeAlign'top' | 'middle' | 'bottom'Column: where the text of a merged cell sits (default 'top'). It stays in the part of the block that is in view while you scroll.
sortWithinMergesboolean (option)Grid: sorting reorders rows only inside each merged block; the blocks keep their order. A merge column you sort by does not form blocks.
setSortWithinMerges(on) / getSortWithinMerges()void / booleanTurn sortWithinMerges on or off; the rows are sorted again at once.
mergeSelection'rows' | number (option)Grid, with rowSelection: 'rows' (default) puts a checkbox on every row; n puts one checkbox on each block of the n-th merge column from the left (1 = first, 2 = first and second together). It checks every row of the block.
setMergeSelection(level) / getMergeSelection()void / 'rows' | numberChange where the checkboxes go; rows that are selected stay selected.
getSelectedGroupsAllRows(){ level, values, rows }[]Like getSelectedGroups(), but rows holds every row of each touched block that passes the filters, selected or not.
getSelectedGroups(){ level, values, rows }[]The selection by block, in display order: values holds the merge column values of the block (by field), rows its selected rows. [] with one checkbox per row.

Questions about this page

Can FreeGrid merge cells with the same value?

Yes. merge: true on a column joins consecutive cells with the same value into one tall cell, and rowSpan sets how many rows a cell covers.