셀 병합·줄바꿈·배치
colSpan과 rowSpan은 셀마다 몇 칸을 차지할지 함수로 정합니다. 공지 행 하나를 여러 컬럼에 걸쳐 쓰거나, 데이터에 적힌 행 수만큼 지역 이름을 합칠 때 씁니다. 합쳐진 셀은 선택, 복사, 엑셀 내보내기에서도 한 칸으로 다뤄집니다. 컬럼에 wrapText:…
실행 예제셀 병합·줄바꿈·배치
여러 행·열을 차지하는 셀, 줄바꿈과 자동 행 높이, 헤더 버튼, 오른쪽→왼쪽 배치와 카드 보기.
이 데모의 코드
import { createGrid, type ColumnDef, type GridApi, type Row } from '@freegridjs/core';
import { 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 });
const regions = [
{ region: h('Seoul', '서울'), items: [[h('Monitor', '모니터'), h('Pending payment for the March order; follow up with the buyer before the end of the week.', '3월 주문 입금 대기 중입니다. 이번 주가 끝나기 전에 구매 담당자에게 다시 연락해야 합니다.')], [h('Keyboard', '키보드'), h('Shipped.', '출고 완료.')], [h('Desk', '책상'), h('Assembly service booked for Friday afternoon, customer asked for a call 30 minutes before arrival.', '금요일 오후 조립 서비스 예약. 도착 30분 전에 전화해 달라는 요청이 있었습니다.')]] },
{ region: h('Busan', '부산'), items: [[h('Chair', '의자'), h('Back-ordered: the supplier expects new stock in two weeks.', '재고 없음: 공급처 입고까지 2주 예상.')], [h('Lamp', '조명'), h('Delivered.', '배송 완료.')]] },
{ region: h('Tokyo', '도쿄'), items: [[h('Monitor arm', '모니터 암'), h('Customs paperwork done.', '통관 서류 처리 완료.')], [h('Cable kit', '케이블 세트'), h('Gift wrap requested.', '선물 포장 요청.')]] },
];
const data: Row[] = [];
for (const r of regions) {
r.items.forEach(([item, memo], i) => data.push({
region: r.region, regionRows: i === 0 ? r.items.length : 1, item, memo,
qty: 1 + ((data.length * 7) % 9), price: 30 + ((data.length * 37) % 400),
}));
data.push({ kind: 'note', item: h(`${r.region}: shipping cut-off is 3 pm`, `${r.region}: 오후 3시 이후 주문은 다음 날 출고`) });
}
const columns: ColumnDef[] = [
// the first row of each region spans all of its rows
{ field: 'region', header: h('Region', '지역'), width: 110, rowSpan: ({ row }) => row.regionRows ?? 1 },
// note rows span the item, memo and qty columns
{ field: 'item', header: h('Item', '품목'), width: 150, colSpan: ({ row }) => (row.kind === 'note' ? 3 : 1),
cellClass: ({ row }) => (row.kind === 'note' ? 'demo-note' : undefined) },
// long memos wrap; autoRowHeight grows the row
{ field: 'memo', header: h('Memo', '메모'), width: 280, wrapText: true },
{ field: 'qty', header: h('Qty', '수량'), type: 'number', width: 90 },
{
field: 'price', header: h('Price', '단가'), type: 'number', format: '#,##0', width: 120, headerClass: 'demo-money',
// a help button in the header; pressing it does not sort
headerRenderer: ({ header }) => {
const span = document.createElement('span');
const b = document.createElement('button');
b.type = 'button';
b.textContent = '?';
b.className = 'demo-help';
b.addEventListener('click', () => ctx.log(h('Prices are in USD, before tax.', '단가는 달러, 세전 금액입니다.')));
span.append(`${header} `, b);
return span;
},
},
];
let grid: GridApi;
let rtl = false;
let cards = false;
const create = () => {
grid?.destroy();
grid = createGrid(ctx.host, {
columns, data, locale: lang, rangeSelection: true,
autoRowHeight: true,
direction: rtl ? 'rtl' : 'ltr',
// cardView: a width in px; below it rows turn into cards (phones). 99999 forces cards here.
cardView: cards ? 99999 : false,
});
};
create();
text(ctx, h('Region cells span their rows, note rows span three columns, long memos wrap and the row grows. Press ? in the Price header.',
'지역 셀은 여러 행을, 안내 행은 세 칸을 차지합니다. 긴 메모는 줄바꿈되고 행 높이가 늘어납니다. 단가 헤더의 ? 버튼도 눌러 보세요.'));
separator(ctx);
toggle(ctx, h('Right to left (RTL)', '오른쪽→왼쪽 (RTL)'), false, on => { rtl = on; create(); ctx.log(`direction: ${on ? 'rtl' : 'ltr'}`); });
toggle(ctx, h('Card view (phones)', '카드 보기 (휴대폰)'), false, on => { cards = on; create(); ctx.log(`cardView: ${on}`); });
return grid!;
}실행 예제행 번호·필터 행·화면 저장
행 번호, 필터 행, 툴팁, 접히는 컬럼 그룹, 미니 차트, 행 간격, 사용자별 화면 저장.
이 데모의 코드
import { createGrid, type GridApi } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, select, separator, text, tr, type DemoContext } from '../ui';
const KEY = 'freegrid-demo-view';
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
const grid: GridApi = createGrid(ctx.host, {
columns: [
{ field: 'name', header: h('Name', '이름'), width: 150, headerTooltip: h('Customer name', '고객 이름'), tooltip: true },
{ field: 'email', header: h('Email', '이메일'), width: 150 }, // cut off: hover to read it all
{ field: 'country', header: h('Country', '국가'), width: 120 },
{
header: h('Sales', '매출'), children: [
{ field: 'total', header: h('Year', '연간'), type: 'number', format: '#,##0', width: 110, columnGroupShow: 'closed' },
{ field: 'q1', header: 'Q1', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
{ field: 'q2', header: 'Q2', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
{ field: 'q3', header: 'Q3', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
{ field: 'q4', header: 'Q4', type: 'number', format: '#,##0', width: 90, columnGroupShow: 'open' },
],
},
// four quarters as a small line chart
{ id: 'trend', header: h('Trend', '추이'), width: 110, valueGetter: r => [r.q1, r.q2, r.q3, r.q4], sparkline: true, sortable: false, filter: false },
{ field: 'joined', header: h('Joined', '가입일'), type: 'date', width: 120 },
],
data: makeRows(500),
locale: lang,
rowNumbers: true,
filterRow: true,
rowSelection: 'multiple',
getRowId: r => String(r.id),
});
text(ctx, h('Type in the boxes under the header: Kim, >50000, 10000~20000, >2025-01-01. Press + on Sales to see the quarters. Hover a cut-off email.',
'헤더 아래 칸에 입력해 보세요: Kim, >50000, 10000~20000, >2025-01-01. 매출 옆 + 버튼으로 분기를 펼치고, 잘린 이메일 위에 마우스도 올려 보세요.'));
separator(ctx);
select(ctx, h('Spacing', '행 간격'), [['compact', h('Compact', '좁게')], ['normal', h('Normal', '보통')], ['comfortable', h('Comfortable', '넓게')]], 'normal',
d => grid.setDensity(d));
button(ctx, h('Save view', '화면 저장'), () => {
localStorage.setItem(KEY, JSON.stringify(grid.getState()));
ctx.log(h('saved: columns, sort, filters, groups, selection', '저장: 컬럼, 정렬, 필터, 그룹, 선택'));
});
button(ctx, h('Restore view', '화면 복원'), () => {
const saved = localStorage.getItem(KEY);
if (!saved) return ctx.log(h('nothing saved yet', '저장한 화면이 없습니다'));
grid.setState(JSON.parse(saved));
ctx.log(h('restored', '복원했습니다'));
});
button(ctx, h('Reset', '초기화'), () => {
grid.setState({ sort: [], filters: {}, quickFilter: '', columnGroups: {}, selected: [] });
grid.resetColumnState();
});
return grid;
}colSpan과 rowSpan은 셀마다 몇 칸을 차지할지 함수로 정합니다. 공지 행 하나를 여러 컬럼에 걸쳐 쓰거나, 데이터에 적힌 행 수만큼 지역 이름을 합칠 때 씁니다. 합쳐진 셀은 선택, 복사, 엑셀 내보내기에서도 한 칸으로 다뤄집니다. 컬럼에 wrapText: true를 주면 긴 글이 여러 줄로 감기고, autoRowHeight: true를 켜면 행 높이가 내용에 맞춰 늘어납니다.
headerRenderer로 헤더에 아이콘이나 도움말 버튼을 넣을 수 있고, 그 안의 버튼을 눌러도 정렬되지 않습니다. direction: 'rtl'은 아랍어·히브리어처럼 오른쪽에서 왼쪽으로 읽는 화면을 위한 배치이고, cardView는 화면이 좁을 때 행을 카드로 바꿔 휴대폰에서도 읽기 쉽게 합니다. 휴대폰에서는 포커스된 셀을 한 번 더 누르면 편집하고, 길게 누르면 메뉴가 열립니다.
createGrid(el, {
columns: [
// one cell spans the row's first three columns when it is a note row
{ field: 'title', colSpan: ({ row }) => (row.kind === 'note' ? 3 : 1) },
{ field: 'region', rowSpan: ({ row }) => row.regionRows ?? 1 },
// long text wraps; the row grows to fit
{ field: 'memo', width: 220, wrapText: true },
// icons or buttons in the header; sorting still works
{ field: 'amount', type: 'number', headerClass: 'money',
headerRenderer: ({ header }) => `${header} <button type="button" data-fg-no-sort>?</button>` },
],
data,
autoRowHeight: true,
direction: 'rtl', // Arabic, Hebrew: first column on the right
cardView: 640, // narrower than 640px: rows become cards (phones)
});보기 편의: 행 번호, 필터 행, 툴팁, 밀도, 상태 저장
rowNumbers는 왼쪽에 1, 2, 3… 번호 컬럼을 붙이고, filterRow는 헤더 바로 아래에 입력칸을 두어 타이핑하는 대로 걸러 줍니다. 숫자는 >10, 10~20, 날짜는 >2026-01-01처럼 씁니다. tooltip과 headerTooltip으로 설명을 붙이고, 잘린 글자는 마우스를 올리면 전체가 보입니다. columnGroupShow로 그룹 헤더를 접고 펼 수 있고, sparkline은 숫자 배열을 셀 안의 작은 차트로 그립니다. density로 행 간격을 바꾸고, autoHeight를 켜면 그리드가 행 수만큼 늘어납니다. getState()와 setState()로 사용자별 화면 설정을 저장하고 복원하세요.
// rows numbered, a filter row, tooltips, compact rows
const grid = createGrid(el, {
columns: [
{ field: 'name', headerTooltip: 'Customer name', tooltip: true },
{ field: 'note', width: 160 }, // cut-off text shows in full on hover
{ header: 'Sales', children: [
{ field: 'total', columnGroupShow: 'closed' },
{ field: 'q1', columnGroupShow: 'open' },
{ field: 'q2', columnGroupShow: 'open' },
] },
{ id: 'trend', valueGetter: r => [r.q1, r.q2, r.q3, r.q4], sparkline: true },
],
data, rowNumbers: true, filterRow: true, density: 'compact',
});
// save each person's view, restore it next time
localStorage.setItem('orders-view', JSON.stringify(grid.getState()));
grid.setState(JSON.parse(localStorage.getItem('orders-view') ?? '{}'));