그리드 전역 headerAlign의 기본값은 center입니다. 이 예제는 전역 값을 명시해 헤더 제목을 가운데 정렬합니다. 개별 컬럼 headerAlign을 지정하면 그 컬럼 값이 우선합니다. rows 생성 옵션을 생략하고 그리드를 만든 뒤 grid.setRows(rows)로 데이터를 넣습니다. 조회한 배열도 같은 함수로 넣을 수 있습니다. 전체 교체 시 변경 상태·Undo/Redo는 초기화됩니다. Vue 3에서는 마운트 후 호출하고 해제 시 destroy합니다.
이 예제의 필수값 host 필수값 — 그리드를 붙일 실제 DOM 요소 또는 그 요소를 찾는 선택자입니다.columns[].field 필수값 — 표시할 컬럼마다 고유한 문자열을 지정합니다.행의 rowKey 값 필수값 — setRows/생성 rows/추가 행에 데이터를 넣을 때 각 행에 고유 키가 필요합니다. 기본 키 이름은 id입니다.이 예제는 생성 옵션 rows를 생략하고 생성 후 조회 데이터를 연결합니다. 생성 후 grid.setRows(rows)를 호출합니다.
데이터 설정과 행 추가의 차이 생성 옵션 rows는 생략할 수 있습니다. 생성 후 setRows(rows)로 조회 결과를 넣으면 전체 데이터를 교체하고 변경 상태·Undo/Redo를 초기화합니다. 기존 데이터에 신규 행을 추가하려면 insertRows(rows)를 사용합니다. insert(row)와 insert(rows)도 지원합니다. 일괄 추가는 한 번의 Undo로 취소되며 ID 중복·누락/계산 실패 시 모두 거부합니다. 컬럼 parse/validate는 셀 편집 때 적용되므로, 추가할 행 값은 호출 전에 준비하세요.
const grid = new ZixoGrid(host, { columns, rowKey: 'id' });
grid.setRows(rows); // 전체 교체 · 새 기준 데이터
grid.insertRows(newRows); // 기존 행 유지 · 여러 행을 변경 목록에 추가
grid.undo(); // 추가한 여러 행을 한 번에 취소행 데이터: 최소 100행 · start.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/start.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>그리드 생성 · 데이터 연결</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>그리드 생성 · 데이터 연결</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="start.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './start.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
headerAlign: 'center',
};
const grid = new ZixoGrid(host, options);
grid.setRows(rows); // 생성 후 데이터 넣기 · 조회 결과로 전체 교체 가능
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'그리드 전역 headerAlign의 기본값은 center입니다. 이 예제는 전역 값을 명시해 헤더 제목을 가운데 정렬합니다. 개별 컬럼 headerAlign을 지정하면 그 컬럼 값이 우선합니다. rows 생성 옵션을 생략하고 그리드를 만든 뒤 grid.setRows(rows)로 데이터를 넣습니다. 조회한 배열도 같은 함수로 넣을 수 있습니다. 전체 교체 시 변경 상태·Undo/Redo는 초기화됩니다. Vue 3에서는 마운트 후 호출하고 해제 시 destroy합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './start.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
headerAlign: 'center',
};
grid = new ZixoGrid(gridHost.value, options);
grid.setRows(rows); // 생성 후 데이터 넣기 · 조회 결과로 전체 교체 가능
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'그리드 전역 headerAlign의 기본값은 center입니다. 이 예제는 전역 값을 명시해 헤더 제목을 가운데 정렬합니다. 개별 컬럼 headerAlign을 지정하면 그 컬럼 값이 우선합니다. rows 생성 옵션을 생략하고 그리드를 만든 뒤 grid.setRows(rows)로 데이터를 넣습니다. 조회한 배열도 같은 함수로 넣을 수 있습니다. 전체 교체 시 변경 상태·Undo/Redo는 초기화됩니다. Vue 3에서는 마운트 후 호출하고 해제 시 destroy합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>그리드 생성 · 데이터 연결</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
생성 옵션 rows를 생략하고 생성 후 setRows(rows)로100행을 바인딩합니다. setRows는 전체 교체와 새 기준 설정이며 변경 목록·Undo/Redo를 초기화합니다. insert/insertRows는 기존 행 뒤에 신규 추가하고 insert 변경으로 기록합니다. clearRows는 빈 기준으로 초기화하므로 서버 삭제 저장 용도가 아닙니다. 원본 데이터는 행 데이터 탭의 별도 파일에 있습니다. 전체 함수 API 메뉴에서 인수·반환값·오류 조건을 확인하세요.
이 예제의 필수값 rows와 행의 rowKey 필수값 — setRows/insertRows에 행 배열과 고유 키를 제공합니다. insert에는 단일 행도 가능합니다. []는 setRows/clearRows의 빈 기준 상태로 사용할 수 있습니다.조회 결과 전체 교체는 setRows, 신규 추가는 insert/insertRows입니다. setRows는 기준 상태와 변경 목록·Undo/Redo를 초기화하고 insertRows는 추가 변경으로 기록합니다. 생성 후 grid.setRows(rows)를 호출합니다.
행 데이터: 최소 100행 · data-binding.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/data-binding.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>데이터 설정 · 교체 · 추가</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>데이터 설정 · 교체 · 추가</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="data-binding.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './data-binding.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
function result() {
return JSON.stringify({
rows: grid.getRows().length,
changes: grid.getChangedRows(),
});
}
const grid = new ZixoGrid(host, options);
grid.setRows(rows); // 생성 후 데이터 넣기 · 조회 결과로 전체 교체 가능
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('setRows · 조회 결과 교체', () => {
grid.setRows(rows.map((row) => ({ ...row, name: '조회 결과 ' + row.name })));
return result();
});
button('insert · 한 행 추가', () => {
const id = Math.max(0, ...grid.getRows().map((row) => row.id)) + 1;
grid.insert({ ...rows[0], id, name: '신규 행' });
return result();
});
button('insertRows · 두 행 추가', () => {
const id = Math.max(0, ...grid.getRows().map((row) => row.id)) + 1;
grid.insertRows(
rows
.slice(0, 2)
.map((row, i) => ({ ...row, id: id + i, name: '일괄 신규 ' + i })),
);
return result();
});
button('변경 목록 확인', () => result());
button('clearRows · 전체 비우기', () => {
grid.clearRows();
return result();
});
button('100행 복원', () => {
grid.setRows(rows);
return result();
});
report.textContent =
'생성 옵션 rows를 생략하고 생성 후 setRows(rows)로100행을 바인딩합니다. setRows는 전체 교체와 새 기준 설정이며 변경 목록·Undo/Redo를 초기화합니다. insert/insertRows는 기존 행 뒤에 신규 추가하고 insert 변경으로 기록합니다. clearRows는 빈 기준으로 초기화하므로 서버 삭제 저장 용도가 아닙니다. 원본 데이터는 행 데이터 탭의 별도 파일에 있습니다. 전체 함수 API 메뉴에서 인수·반환값·오류 조건을 확인하세요.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './data-binding.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
function result() {
return JSON.stringify({
rows: grid.getRows().length,
changes: grid.getChangedRows(),
});
}
grid = new ZixoGrid(gridHost.value, options);
grid.setRows(rows); // 생성 후 데이터 넣기 · 조회 결과로 전체 교체 가능
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('setRows · 조회 결과 교체', () => {
grid.setRows(
rows.map((row) => ({ ...row, name: '조회 결과 ' + row.name })),
);
return result();
});
button('insert · 한 행 추가', () => {
const id = Math.max(0, ...grid.getRows().map((row) => row.id)) + 1;
grid.insert({ ...rows[0], id, name: '신규 행' });
return result();
});
button('insertRows · 두 행 추가', () => {
const id = Math.max(0, ...grid.getRows().map((row) => row.id)) + 1;
grid.insertRows(
rows
.slice(0, 2)
.map((row, i) => ({ ...row, id: id + i, name: '일괄 신규 ' + i })),
);
return result();
});
button('변경 목록 확인', () => result());
button('clearRows · 전체 비우기', () => {
grid.clearRows();
return result();
});
button('100행 복원', () => {
grid.setRows(rows);
return result();
});
message.value =
'생성 옵션 rows를 생략하고 생성 후 setRows(rows)로100행을 바인딩합니다. setRows는 전체 교체와 새 기준 설정이며 변경 목록·Undo/Redo를 초기화합니다. insert/insertRows는 기존 행 뒤에 신규 추가하고 insert 변경으로 기록합니다. clearRows는 빈 기준으로 초기화하므로 서버 삭제 저장 용도가 아닙니다. 원본 데이터는 행 데이터 탭의 별도 파일에 있습니다. 전체 함수 API 메뉴에서 인수·반환값·오류 조건을 확인하세요.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>데이터 설정 · 교체 · 추가</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
rowKey는 행 위치가 아닌 고유 키입니다. 정렬 후에도 같은 키로 행을 수정합니다. 중복 키는 거부됩니다.
이 예제의 필수값 행의 rowKey 값 필수값 — 모든 적재 행에서 유효하고 고유한 문자열 또는 유한한 숫자를 반환해야 합니다. 0은 유효하며 누락·빈 문자열·중복은 허용하지 않습니다.행을 식별하는 키 필드 이름을 생성 전에 지정할 수 있습니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.rowKey = 'orderId'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · row-id.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/row-id.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>안정적인 행 ID</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>안정적인 행 ID</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="row-id.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './row-id.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'orderId',
height: 360,
rows: rows,
columns: [
{ field: 'orderId', title: '주문 ID' },
{ field: 'name', title: '주문' },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('첫 주문 이름 수정', () =>
grid.updateCells([{ id: 'ORD-A', field: 'name', value: '수정한 주문' }]),
);
button('행 확인', () => grid.getRows());
report.textContent =
'rowKey는 행 위치가 아닌 고유 키입니다. 정렬 후에도 같은 키로 행을 수정합니다. 중복 키는 거부됩니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './row-id.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'orderId',
height: 360,
rows: rows,
columns: [
{ field: 'orderId', title: '주문 ID' },
{ field: 'name', title: '주문' },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('첫 주문 이름 수정', () =>
grid.updateCells([{ id: 'ORD-A', field: 'name', value: '수정한 주문' }]),
);
button('행 확인', () => grid.getRows());
message.value =
'rowKey는 행 위치가 아닌 고유 키입니다. 정렬 후에도 같은 키로 행을 수정합니다. 중복 키는 거부됩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>안정적인 행 ID</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
orderId: 'ORD-A',
name: '첫 주문',
},
{
orderId: 'ORD-B',
name: '둘째 주문',
},
{
orderId: 'ORD-A-2',
name: '첫 주문 3',
},
{
orderId: 'ORD-B-2',
name: '둘째 주문 4',
},
{
orderId: 'ORD-A-3',
name: '첫 주문 5',
},
{
orderId: 'ORD-B-3',
name: '둘째 주문 6',
},
{
orderId: 'ORD-A-4',
name: '첫 주문 7',
},
{
orderId: 'ORD-B-4',
name: '둘째 주문 8',
},
{
orderId: 'ORD-A-5',
name: '첫 주문 9',
},
{
orderId: 'ORD-B-5',
name: '둘째 주문 10',
},
{
orderId: 'ORD-A-6',
name: '첫 주문 11',
},
{
orderId: 'ORD-B-6',
name: '둘째 주문 12',
},
{
orderId: 'ORD-A-7',
name: '첫 주문 13',
},
{
orderId: 'ORD-B-7',
name: '둘째 주문 14',
},
{
orderId: 'ORD-A-8',
name: '첫 주문 15',
},
{
orderId: 'ORD-B-8',
name: '둘째 주문 16',
},
{
orderId: 'ORD-A-9',
name: '첫 주문 17',
},
{
orderId: 'ORD-B-9',
name: '둘째 주문 18',
},
{
orderId: 'ORD-A-10',
name: '첫 주문 19',
},
{
orderId: 'ORD-B-10',
name: '둘째 주문 20',
},
{
orderId: 'ORD-A-11',
name: '첫 주문 21',
},
{
orderId: 'ORD-B-11',
name: '둘째 주문 22',
},
{
orderId: 'ORD-A-12',
name: '첫 주문 23',
},
{
orderId: 'ORD-B-12',
name: '둘째 주문 24',
},
{
orderId: 'ORD-A-13',
name: '첫 주문 25',
},
{
orderId: 'ORD-B-13',
name: '둘째 주문 26',
},
{
orderId: 'ORD-A-14',
name: '첫 주문 27',
},
{
orderId: 'ORD-B-14',
name: '둘째 주문 28',
},
{
orderId: 'ORD-A-15',
name: '첫 주문 29',
},
{
orderId: 'ORD-B-15',
name: '둘째 주문 30',
},
{
orderId: 'ORD-A-16',
name: '첫 주문 31',
},
{
orderId: 'ORD-B-16',
name: '둘째 주문 32',
},
{
orderId: 'ORD-A-17',
name: '첫 주문 33',
},
{
orderId: 'ORD-B-17',
name: '둘째 주문 34',
},
{
orderId: 'ORD-A-18',
name: '첫 주문 35',
},
{
orderId: 'ORD-B-18',
name: '둘째 주문 36',
},
{
orderId: 'ORD-A-19',
name: '첫 주문 37',
},
{
orderId: 'ORD-B-19',
name: '둘째 주문 38',
},
{
orderId: 'ORD-A-20',
name: '첫 주문 39',
},
{
orderId: 'ORD-B-20',
name: '둘째 주문 40',
},
{
orderId: 'ORD-A-21',
name: '첫 주문 41',
},
{
orderId: 'ORD-B-21',
name: '둘째 주문 42',
},
{
orderId: 'ORD-A-22',
name: '첫 주문 43',
},
{
orderId: 'ORD-B-22',
name: '둘째 주문 44',
},
{
orderId: 'ORD-A-23',
name: '첫 주문 45',
},
{
orderId: 'ORD-B-23',
name: '둘째 주문 46',
},
{
orderId: 'ORD-A-24',
name: '첫 주문 47',
},
{
orderId: 'ORD-B-24',
name: '둘째 주문 48',
},
{
orderId: 'ORD-A-25',
name: '첫 주문 49',
},
{
orderId: 'ORD-B-25',
name: '둘째 주문 50',
},
{
orderId: 'ORD-A-26',
name: '첫 주문 51',
},
{
orderId: 'ORD-B-26',
name: '둘째 주문 52',
},
{
orderId: 'ORD-A-27',
name: '첫 주문 53',
},
{
orderId: 'ORD-B-27',
name: '둘째 주문 54',
},
{
orderId: 'ORD-A-28',
name: '첫 주문 55',
},
{
orderId: 'ORD-B-28',
name: '둘째 주문 56',
},
{
orderId: 'ORD-A-29',
name: '첫 주문 57',
},
{
orderId: 'ORD-B-29',
name: '둘째 주문 58',
},
{
orderId: 'ORD-A-30',
name: '첫 주문 59',
},
{
orderId: 'ORD-B-30',
name: '둘째 주문 60',
},
{
orderId: 'ORD-A-31',
name: '첫 주문 61',
},
{
orderId: 'ORD-B-31',
name: '둘째 주문 62',
},
{
orderId: 'ORD-A-32',
name: '첫 주문 63',
},
{
orderId: 'ORD-B-32',
name: '둘째 주문 64',
},
{
orderId: 'ORD-A-33',
name: '첫 주문 65',
},
{
orderId: 'ORD-B-33',
name: '둘째 주문 66',
},
{
orderId: 'ORD-A-34',
name: '첫 주문 67',
},
{
orderId: 'ORD-B-34',
name: '둘째 주문 68',
},
{
orderId: 'ORD-A-35',
name: '첫 주문 69',
},
{
orderId: 'ORD-B-35',
name: '둘째 주문 70',
},
{
orderId: 'ORD-A-36',
name: '첫 주문 71',
},
{
orderId: 'ORD-B-36',
name: '둘째 주문 72',
},
{
orderId: 'ORD-A-37',
name: '첫 주문 73',
},
{
orderId: 'ORD-B-37',
name: '둘째 주문 74',
},
{
orderId: 'ORD-A-38',
name: '첫 주문 75',
},
{
orderId: 'ORD-B-38',
name: '둘째 주문 76',
},
{
orderId: 'ORD-A-39',
name: '첫 주문 77',
},
{
orderId: 'ORD-B-39',
name: '둘째 주문 78',
},
{
orderId: 'ORD-A-40',
name: '첫 주문 79',
},
{
orderId: 'ORD-B-40',
name: '둘째 주문 80',
},
{
orderId: 'ORD-A-41',
name: '첫 주문 81',
},
{
orderId: 'ORD-B-41',
name: '둘째 주문 82',
},
{
orderId: 'ORD-A-42',
name: '첫 주문 83',
},
{
orderId: 'ORD-B-42',
name: '둘째 주문 84',
},
{
orderId: 'ORD-A-43',
name: '첫 주문 85',
},
{
orderId: 'ORD-B-43',
name: '둘째 주문 86',
},
{
orderId: 'ORD-A-44',
name: '첫 주문 87',
},
{
orderId: 'ORD-B-44',
name: '둘째 주문 88',
},
{
orderId: 'ORD-A-45',
name: '첫 주문 89',
},
{
orderId: 'ORD-B-45',
name: '둘째 주문 90',
},
{
orderId: 'ORD-A-46',
name: '첫 주문 91',
},
{
orderId: 'ORD-B-46',
name: '둘째 주문 92',
},
{
orderId: 'ORD-A-47',
name: '첫 주문 93',
},
{
orderId: 'ORD-B-47',
name: '둘째 주문 94',
},
{
orderId: 'ORD-A-48',
name: '첫 주문 95',
},
{
orderId: 'ORD-B-48',
name: '둘째 주문 96',
},
{
orderId: 'ORD-A-49',
name: '첫 주문 97',
},
{
orderId: 'ORD-B-49',
name: '둘째 주문 98',
},
{
orderId: 'ORD-A-50',
name: '첫 주문 99',
},
{
orderId: 'ORD-B-50',
name: '둘째 주문 100',
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
height:auto는 상위 컨테이너와 화면 경계 안의 세로 배치에서 검색 영역·하단 영역·gap·여백을 뺀 공간을 채웁니다. 예제 부모 최대480px - 검색60px - 하단40px이므로 그리드는 최대380px이며 화면이 작으면 설명·상위 여백까지 제외해 더 작아집니다. host 높이를 지정하지 않아도 됩니다. 부모가 내용에 따라 늘어나는 auto이면 화면 높이에서 host 위쪽과 아래 형제 영역을 제외합니다. ResizeObserver로 부모·형제 변경을 반영하며 재생성하지 않아 데이터·선택을 유지합니다. 좌우 그리드는 각 영역에서 독립적으로 높이를 계산합니다. 같은 부모의 상하 auto 여러 개는 높이를 균등 분할하지 않으므로 영역별 부모 높이를 배정하세요. 세로 block/flex/grid 배치에서 absolute/fixed 형제는 계산에서 제외합니다. 복잡한 다열 배치는 별도의 세로 컨테이너로 감싸세요. host는 그리드 전용이며 자동 모드가 height/min-height/flex를 관리하고 destroy 시 원래 인라인 값을 복원합니다.
상위 경로의 하단 여백과 형제 영역을 제외하고 확정 높이·max-height·화면 경계로 제한합니다. 조상의 강제 높이나 다른 콘텐츠가 화면을 넘으면 페이지 스크롤은 남을 수 있습니다. 좌우 그리드의 옆 패널은 차감하지 않으며 가로 flex 너비 분배를 유지합니다. 같은 부모에서 상하 auto를 여러 개 쓰면 영역별 높이를 배정하세요. 크기 변경 시 재생성하지 않으므로 데이터·선택을 유지합니다. 기존 숫자·%·vh 높이는 별도로 유지합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.height = 'auto'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · auto-height.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/auto-height.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>부모 남은 공간 · 자동 높이</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>부모 남은 공간 · 자동 높이</h1>
<div id="toolbar" class="toolbar"></div>
<div class="auto-parent" style="max-height: 480px">
<div style="height: 60px">검색 영역</div>
<div id="grid"></div>
<div style="height: 40px">하단 영역</div>
</div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="auto-height.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './auto-height.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 'auto',
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button(
'부모 최대 높이 360',
() => (grid.host.parentElement.style.maxHeight = '360px'),
);
button(
'부모 최대 높이 520',
() => (grid.host.parentElement.style.maxHeight = '520px'),
);
button(
'검색 영역 확대',
() => (grid.host.previousElementSibling.style.height = '120px'),
);
button(
'검색 영역 복원',
() => (grid.host.previousElementSibling.style.height = '60px'),
);
report.textContent =
'height:auto는 상위 컨테이너와 화면 경계 안의 세로 배치에서 검색 영역·하단 영역·gap·여백을 뺀 공간을 채웁니다. 예제 부모 최대480px - 검색60px - 하단40px이므로 그리드는 최대380px이며 화면이 작으면 설명·상위 여백까지 제외해 더 작아집니다. host 높이를 지정하지 않아도 됩니다. 부모가 내용에 따라 늘어나는 auto이면 화면 높이에서 host 위쪽과 아래 형제 영역을 제외합니다. ResizeObserver로 부모·형제 변경을 반영하며 재생성하지 않아 데이터·선택을 유지합니다. 좌우 그리드는 각 영역에서 독립적으로 높이를 계산합니다. 같은 부모의 상하 auto 여러 개는 높이를 균등 분할하지 않으므로 영역별 부모 높이를 배정하세요. 세로 block/flex/grid 배치에서 absolute/fixed 형제는 계산에서 제외합니다. 복잡한 다열 배치는 별도의 세로 컨테이너로 감싸세요. host는 그리드 전용이며 자동 모드가 height/min-height/flex를 관리하고 destroy 시 원래 인라인 값을 복원합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './auto-height.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 'auto',
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button(
'부모 최대 높이 360',
() => (grid.host.parentElement.style.maxHeight = '360px'),
);
button(
'부모 최대 높이 520',
() => (grid.host.parentElement.style.maxHeight = '520px'),
);
button(
'검색 영역 확대',
() => (grid.host.previousElementSibling.style.height = '120px'),
);
button(
'검색 영역 복원',
() => (grid.host.previousElementSibling.style.height = '60px'),
);
message.value =
'height:auto는 상위 컨테이너와 화면 경계 안의 세로 배치에서 검색 영역·하단 영역·gap·여백을 뺀 공간을 채웁니다. 예제 부모 최대480px - 검색60px - 하단40px이므로 그리드는 최대380px이며 화면이 작으면 설명·상위 여백까지 제외해 더 작아집니다. host 높이를 지정하지 않아도 됩니다. 부모가 내용에 따라 늘어나는 auto이면 화면 높이에서 host 위쪽과 아래 형제 영역을 제외합니다. ResizeObserver로 부모·형제 변경을 반영하며 재생성하지 않아 데이터·선택을 유지합니다. 좌우 그리드는 각 영역에서 독립적으로 높이를 계산합니다. 같은 부모의 상하 auto 여러 개는 높이를 균등 분할하지 않으므로 영역별 부모 높이를 배정하세요. 세로 block/flex/grid 배치에서 absolute/fixed 형제는 계산에서 제외합니다. 복잡한 다열 배치는 별도의 세로 컨테이너로 감싸세요. host는 그리드 전용이며 자동 모드가 height/min-height/flex를 관리하고 destroy 시 원래 인라인 값을 복원합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>부모 남은 공간 · 자동 높이</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div class="auto-parent" style="max-height: 480px">
<div style="height: 60px">검색 영역</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div style="height: 40px">하단 영역</div>
</div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
부모480px × host75%=360px입니다. 그리드 높이100%는360px, 50%는180px이며 그리드 높이 선택으로 비교합니다. 부모 높이만 지정하고 host가 auto이면 퍼센트 기준이 없어 두 값이 모두 내용 높이가 될 수 있습니다. host에도 height:100% 또는 400px 등 확정 높이가 필요하며 min-height만으로는 부족합니다. 부모 높이 변경은 자동 반영됩니다. height 생략 시 host가 비어 있고 지정 높이도 없으면 기본420px이며 명시한 퍼센트를420px로 대체하는 것은 아닙니다. 생성 옵션을 바꾸는 선택은 새 그리드를 생성하므로 편집·선택 상태를 초기화합니다.
퍼센트의 직접 기준은 그리드를 생성한 host div의 콘텐츠 높이입니다. 부모480px·host75%이면 host360px이고, options.height 100%는360px·50%는180px입니다. 부모만 고정하고 host를 auto로 두면 정상 계산할 수 없습니다. 생성 후 options.height 값만 바꾸는 것은 자동 반영되지 않습니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.height = '100%'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · percent-height.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/percent-height.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>부모 div · 퍼센트 높이</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>부모 div · 퍼센트 높이</h1>
<div id="toolbar" class="toolbar"></div>
<div class="percent-parent" style="height: 480px">
<div id="grid" style="height: 75%"></div>
</div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="percent-height.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './percent-height.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: '100%',
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
let grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('부모 높이 360', () => (grid.host.parentElement.style.height = '360px'));
button('부모 높이 520', () => (grid.host.parentElement.style.height = '520px'));
select('그리드 높이', ['100%', '50%'], (height) => {
const container = grid.host;
grid.destroy();
grid = new ZixoGrid(container, { ...options, height });
report.textContent =
'host ' +
grid.host.clientHeight +
'px × ' +
height +
' → grid ' +
grid.root.offsetHeight +
'px';
});
report.textContent =
'부모480px × host75%=360px입니다. 그리드 높이100%는360px, 50%는180px이며 그리드 높이 선택으로 비교합니다. 부모 높이만 지정하고 host가 auto이면 퍼센트 기준이 없어 두 값이 모두 내용 높이가 될 수 있습니다. host에도 height:100% 또는 400px 등 확정 높이가 필요하며 min-height만으로는 부족합니다. 부모 높이 변경은 자동 반영됩니다. height 생략 시 host가 비어 있고 지정 높이도 없으면 기본420px이며 명시한 퍼센트를420px로 대체하는 것은 아닙니다. 생성 옵션을 바꾸는 선택은 새 그리드를 생성하므로 편집·선택 상태를 초기화합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './percent-height.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: '100%',
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button(
'부모 높이 360',
() => (grid.host.parentElement.style.height = '360px'),
);
button(
'부모 높이 520',
() => (grid.host.parentElement.style.height = '520px'),
);
select('그리드 높이', ['100%', '50%'], (height) => {
const container = grid.host;
grid.destroy();
grid = new ZixoGrid(container, { ...options, height });
message.value =
'host ' +
grid.host.clientHeight +
'px × ' +
height +
' → grid ' +
grid.root.offsetHeight +
'px';
});
message.value =
'부모480px × host75%=360px입니다. 그리드 높이100%는360px, 50%는180px이며 그리드 높이 선택으로 비교합니다. 부모 높이만 지정하고 host가 auto이면 퍼센트 기준이 없어 두 값이 모두 내용 높이가 될 수 있습니다. host에도 height:100% 또는 400px 등 확정 높이가 필요하며 min-height만으로는 부족합니다. 부모 높이 변경은 자동 반영됩니다. height 생략 시 host가 비어 있고 지정 높이도 없으면 기본420px이며 명시한 퍼센트를420px로 대체하는 것은 아닙니다. 생성 옵션을 바꾸는 선택은 새 그리드를 생성하므로 편집·선택 상태를 초기화합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>부모 div · 퍼센트 높이</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div class="percent-parent" style="height: 480px">
<div ref="gridHost" class="vue-grid-host" style="height: 75%"></div>
</div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
주문 flex:2와 수량 flex:1은 고정 단가 100px을 제외한 실제 표시 영역을 2:1로 나눕니다. 실제 표시 영역이 1000px이면 주문 600px, 수량 300px, 단가 100px입니다. 세로 스크롤바가 차지하는 너비는 제외하고 계산합니다. 컨테이너가 600px 미만이면 단가를 숨깁니다. flex는 width에 더하는 값이 아니며 생략/0일 때 width를 사용합니다. minWidth/maxWidth 제한으로 생긴 차이는 재분배하지 않고, 최소 너비 합이 표시 영역보다 크면 가로 스크롤이 생깁니다. 컬럼 경계를 드래그하거나 setColumnWidth로 너비를 지정하면 해당 컬럼은 flex:0인 고정 너비로 전환됩니다.
flex와 hideBelow로 컬럼의 공간 배분과 작은 화면 표시를 지정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].flex = 1처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · responsive.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/responsive.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>반응형 · flex</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>반응형 · flex</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="responsive.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './responsive.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', flex: 2, minWidth: 100 },
{ field: 'qty', title: '수량', flex: 1, minWidth: 80 },
{ field: 'price', title: '단가', width: 100, hideBelow: 600 },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'주문 flex:2와 수량 flex:1은 고정 단가 100px을 제외한 실제 표시 영역을 2:1로 나눕니다. 실제 표시 영역이 1000px이면 주문 600px, 수량 300px, 단가 100px입니다. 세로 스크롤바가 차지하는 너비는 제외하고 계산합니다. 컨테이너가 600px 미만이면 단가를 숨깁니다. flex는 width에 더하는 값이 아니며 생략/0일 때 width를 사용합니다. minWidth/maxWidth 제한으로 생긴 차이는 재분배하지 않고, 최소 너비 합이 표시 영역보다 크면 가로 스크롤이 생깁니다. 컬럼 경계를 드래그하거나 setColumnWidth로 너비를 지정하면 해당 컬럼은 flex:0인 고정 너비로 전환됩니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './responsive.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', flex: 2, minWidth: 100 },
{ field: 'qty', title: '수량', flex: 1, minWidth: 80 },
{ field: 'price', title: '단가', width: 100, hideBelow: 600 },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'주문 flex:2와 수량 flex:1은 고정 단가 100px을 제외한 실제 표시 영역을 2:1로 나눕니다. 실제 표시 영역이 1000px이면 주문 600px, 수량 300px, 단가 100px입니다. 세로 스크롤바가 차지하는 너비는 제외하고 계산합니다. 컨테이너가 600px 미만이면 단가를 숨깁니다. flex는 width에 더하는 값이 아니며 생략/0일 때 width를 사용합니다. minWidth/maxWidth 제한으로 생긴 차이는 재분배하지 않고, 최소 너비 합이 표시 영역보다 크면 가로 스크롤이 생깁니다. 컬럼 경계를 드래그하거나 setColumnWidth로 너비를 지정하면 해당 컬럼은 flex:0인 고정 너비로 전환됩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>반응형 · flex</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
전역 headerAlign:left를 주문 컬럼이 상속하고, 수량은 컬럼 headerAlign:center, 단가는 컬럼 headerAlign:right가 우선합니다. 전역·컬럼 모두 생략하면 기본 center입니다. headerAlign:center는 컬럼 전체 너비의 중앙에 제목을 배치합니다. 정렬·필터·메뉴 아이콘이 표시되어도 제목의 중심은 유지됩니다. align은 본문 셀 정렬로 헤더와 독립입니다. 헤더 클릭은 정렬, Shift+클릭은 다중 정렬입니다. 헤더 드래그 시 삽입 위치가 표시됩니다. 가로 스크롤은 헤더와 데이터가 함께 이동하며 세로 스크롤바 폭도 자동 반영합니다. 별도 동기화 설정은 필요하지 않습니다.
본문 가로 정렬은 컬럼 align에 지정합니다. 헤더는 컬럼 headerAlign → 그리드 headerAlign → center 순서로 적용합니다. 생성 후 너비·순서·헤더 높이는 setColumnWidth/moveColumn/setHeaderHeight를 사용합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[1].align = 'right'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · column-layout.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/column-layout.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>정렬 · 너비 · 순서 · 높이</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>정렬 · 너비 · 순서 · 높이</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="column-layout.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './column-layout.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
type: 'number',
parse: Number,
align: 'right',
headerAlign: 'center',
},
{
field: 'price',
title: '단가',
type: 'number',
parse: Number,
headerAlign: 'right',
},
],
plugins: [],
headerAlign: 'left',
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('너비 180', () => grid.setColumnWidth('name', 180));
button('수량을 맨 앞으로', () => grid.moveColumn('qty', 'name'));
button('헤더 높이 60', () => grid.setHeaderHeight(60));
report.textContent =
'전역 headerAlign:left를 주문 컬럼이 상속하고, 수량은 컬럼 headerAlign:center, 단가는 컬럼 headerAlign:right가 우선합니다. 전역·컬럼 모두 생략하면 기본 center입니다. headerAlign:center는 컬럼 전체 너비의 중앙에 제목을 배치합니다. 정렬·필터·메뉴 아이콘이 표시되어도 제목의 중심은 유지됩니다. align은 본문 셀 정렬로 헤더와 독립입니다. 헤더 클릭은 정렬, Shift+클릭은 다중 정렬입니다. 헤더 드래그 시 삽입 위치가 표시됩니다. 가로 스크롤은 헤더와 데이터가 함께 이동하며 세로 스크롤바 폭도 자동 반영합니다. 별도 동기화 설정은 필요하지 않습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './column-layout.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
type: 'number',
parse: Number,
align: 'right',
headerAlign: 'center',
},
{
field: 'price',
title: '단가',
type: 'number',
parse: Number,
headerAlign: 'right',
},
],
plugins: [],
headerAlign: 'left',
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('너비 180', () => grid.setColumnWidth('name', 180));
button('수량을 맨 앞으로', () => grid.moveColumn('qty', 'name'));
button('헤더 높이 60', () => grid.setHeaderHeight(60));
message.value =
'전역 headerAlign:left를 주문 컬럼이 상속하고, 수량은 컬럼 headerAlign:center, 단가는 컬럼 headerAlign:right가 우선합니다. 전역·컬럼 모두 생략하면 기본 center입니다. headerAlign:center는 컬럼 전체 너비의 중앙에 제목을 배치합니다. 정렬·필터·메뉴 아이콘이 표시되어도 제목의 중심은 유지됩니다. align은 본문 셀 정렬로 헤더와 독립입니다. 헤더 클릭은 정렬, Shift+클릭은 다중 정렬입니다. 헤더 드래그 시 삽입 위치가 표시됩니다. 가로 스크롤은 헤더와 데이터가 함께 이동하며 세로 스크롤바 폭도 자동 반영합니다. 별도 동기화 설정은 필요하지 않습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>정렬 · 너비 · 순서 · 높이</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
첫 컬럼은 wrap:true/headerWrap:true로 셀과 헤더를 줄바꿈하고, 둘째 컬럼은 false로 전체 설정을 덮어써 한 줄로 표시합니다. 셋째 컬럼은 옵션을 생략해 그리드 wrap:true/headerWrap:true를 상속합니다. 셀 내용이 길면 행 높이는 자동으로 늘어나며 짧은 셀과 여러 줄 내용 모두 세로 중앙에 표시됩니다. 너비 버튼으로 줄 수와 행 높이 변화를 확인하세요. 헤더 높이는 자동으로 늘어나지 않으므로 headerHeight:72를 지정했습니다. 첫 헤더의 줄바꿈과 둘째 헤더의 한 줄 생략을 비교하세요.
셀과 헤더 줄바꿈을 생성 전에 따로 지정할 수도 있습니다. 헤더 높이는 headerHeight로 설정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].wrap = true; options.columns[0].headerWrap = true처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · column-wrap.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/column-wrap.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>컬럼 줄바꿈 · 자동 행 높이</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>컬럼 줄바꿈 · 자동 행 높이</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="column-wrap.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './column-wrap.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'name',
title: '셀과 헤더 줄바꿈을 켠 긴 설명 컬럼',
width: 180,
wrap: true,
headerWrap: true,
},
{
field: 'singleLine',
title: '셀과 헤더 줄바꿈을 끈 긴 설명 컬럼',
width: 180,
wrap: false,
headerWrap: false,
},
{ field: 'inherited', title: '그리드 설정 상속', width: 240 },
],
plugins: [],
wrap: true,
headerWrap: true,
headerHeight: 72,
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('줄바꿈 컬럼 140px', () => grid.setColumnWidth('name', 140));
button('줄바꿈 컬럼 300px', () => grid.setColumnWidth('name', 300));
button('기본 너비 180px', () => grid.setColumnWidth('name', 180));
report.textContent =
'첫 컬럼은 wrap:true/headerWrap:true로 셀과 헤더를 줄바꿈하고, 둘째 컬럼은 false로 전체 설정을 덮어써 한 줄로 표시합니다. 셋째 컬럼은 옵션을 생략해 그리드 wrap:true/headerWrap:true를 상속합니다. 셀 내용이 길면 행 높이는 자동으로 늘어나며 짧은 셀과 여러 줄 내용 모두 세로 중앙에 표시됩니다. 너비 버튼으로 줄 수와 행 높이 변화를 확인하세요. 헤더 높이는 자동으로 늘어나지 않으므로 headerHeight:72를 지정했습니다. 첫 헤더의 줄바꿈과 둘째 헤더의 한 줄 생략을 비교하세요.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './column-wrap.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'name',
title: '셀과 헤더 줄바꿈을 켠 긴 설명 컬럼',
width: 180,
wrap: true,
headerWrap: true,
},
{
field: 'singleLine',
title: '셀과 헤더 줄바꿈을 끈 긴 설명 컬럼',
width: 180,
wrap: false,
headerWrap: false,
},
{ field: 'inherited', title: '그리드 설정 상속', width: 240 },
],
plugins: [],
wrap: true,
headerWrap: true,
headerHeight: 72,
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('줄바꿈 컬럼 140px', () => grid.setColumnWidth('name', 140));
button('줄바꿈 컬럼 300px', () => grid.setColumnWidth('name', 300));
button('기본 너비 180px', () => grid.setColumnWidth('name', 180));
message.value =
'첫 컬럼은 wrap:true/headerWrap:true로 셀과 헤더를 줄바꿈하고, 둘째 컬럼은 false로 전체 설정을 덮어써 한 줄로 표시합니다. 셋째 컬럼은 옵션을 생략해 그리드 wrap:true/headerWrap:true를 상속합니다. 셀 내용이 길면 행 높이는 자동으로 늘어나며 짧은 셀과 여러 줄 내용 모두 세로 중앙에 표시됩니다. 너비 버튼으로 줄 수와 행 높이 변화를 확인하세요. 헤더 높이는 자동으로 늘어나지 않으므로 headerHeight:72를 지정했습니다. 첫 헤더의 줄바꿈과 둘째 헤더의 한 줄 생략을 비교하세요.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>컬럼 줄바꿈 · 자동 행 높이</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요.',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 2,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 2',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 3,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 3',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 4,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 4',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 5,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 5',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 6,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 6',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 7,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 7',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 8,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 8',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 9,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 9',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 10,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 10',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 11,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 11',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 12,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 12',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 13,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 13',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 14,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 14',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 15,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 15',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 16,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 16',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 17,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 17',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 18,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 18',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 19,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 19',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 20,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 20',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 21,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 21',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 22,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 22',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 23,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 23',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 24,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 24',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 25,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 25',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 26,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 26',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 27,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 27',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 28,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 28',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 29,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 29',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 30,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 30',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 31,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 31',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 32,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 32',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 33,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 33',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 34,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 34',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 35,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 35',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 36,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 36',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 37,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 37',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 38,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 38',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 39,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 39',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 40,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 40',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 41,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 41',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 42,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 42',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 43,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 43',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 44,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 44',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 45,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 45',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 46,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 46',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 47,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 47',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 48,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 48',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 49,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 49',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 50,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 50',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 51,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 51',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 52,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 52',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 53,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 53',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 54,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 54',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 55,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 55',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 56,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 56',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 57,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 57',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 58,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 58',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 59,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 59',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 60,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 60',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 61,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 61',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 62,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 62',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 63,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 63',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 64,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 64',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 65,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 65',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 66,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 66',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 67,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 67',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 68,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 68',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 69,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 69',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 70,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 70',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 71,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 71',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 72,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 72',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 73,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 73',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 74,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 74',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 75,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 75',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 76,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 76',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 77,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 77',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 78,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 78',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 79,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 79',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 80,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 80',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 81,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 81',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 82,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 82',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 83,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 83',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 84,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 84',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 85,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 85',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 86,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 86',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 87,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 87',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 88,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 88',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 89,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 89',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 90,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 90',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 91,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 91',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 92,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 92',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 93,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 93',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 94,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 94',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 95,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 95',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 96,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 96',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 97,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 97',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 98,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 98',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 99,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 99',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
{
id: 100,
name: '긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 너비를 줄이거나 늘려 행 높이가 내용에 맞게 바뀌는 모습을 확인하세요. 100',
singleLine:
'긴 설명은 컬럼 너비에 맞춰 여러 줄로 표시됩니다. 이 컬럼은 wrap:false로 한 줄만 표시합니다.',
inherited:
'그리드 wrap:true 설정을 상속합니다. 컬럼 옵션을 생략하면 전체 설정을 따릅니다.',
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
columnMenu 배열로 헤더 메뉴에 넣을 영역과 순서를 지정합니다. columns=컬럼 표시 설정, filter=컬럼 필터이며 기본값은 전체입니다. 컬럼 배열은 그리드 배열을 대체하며 []는 메뉴를 숨깁니다. 이 예제의 전체 헤더 메뉴 선택은 주문·수량·단가의 모든 헤더에 적용합니다. 선택하면 컬럼별 columnMenu를 제거하고 그리드 기본 구성을 함께 상속합니다. 초기 선택은 전체이며 세 컬럼의 실제 메뉴와 일치합니다. 필터/정렬 상태와 API는 메뉴를 숨겨도 유지합니다. 컬럼 표시 설정은 표시 컬럼 관리와 같은 컬럼 검색·체크 목록을 하위 창으로 제공합니다. 목록의 체크박스·행 간격·색상·검색창·스크롤 스타일은 필터 값 목록과 같습니다. 체크하면 즉시 표시하고 해제하면 즉시 숨깁니다. 마지막 표시 컬럼은 숨길 수 없으며 권한이 없는 컬럼은 목록에서 제외합니다. 메뉴에는 정렬 항목을 제공하지 않습니다. 필터는 값 검색·모두 선택·체크 목록과 적용/초기화 버튼으로 구성합니다. 검색 중 적용하면 검색 결과에서 체크한 값만 반영합니다. 텍스트/숫자/날짜 필터를 누르면 두 조건을 AND/OR로 결합하는 하위 창이 열립니다. 각 적용은 해당 컬럼의 기존 필터만 교체하며 다른 컬럼·전역 조건은 유지합니다. Escape로 하위 창부터 닫고, 바깥 클릭은 전체를 닫습니다. 목록은 적재한 데이터 기준이며 200개씩 더 볼 수 있습니다. 서버 전체 데이터의 조건 조회는 조건 필터를 사용합니다.
개별 컬럼에만 적용할 때 메뉴 영역과 순서를 지정합니다. 실행 예제의 전체 헤더 메뉴 선택은 주문·수량·단가에 함께 적용됩니다. 빈 배열은 메뉴를 숨깁니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].columnMenu = ['columns', 'filter']처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · column-tools.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/column-tools.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>컬럼 메뉴 · 표시 · 필터</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>컬럼 메뉴 · 표시 · 필터</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="column-tools.js"></script>
</body>
</html>
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './column-tools.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
columnMenu: ['columns', 'filter'],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('컬럼 표시 설정', () => grid.openColumns());
select(
'전체 헤더 메뉴',
['전체', '컬럼 표시 설정만', '필터만', '메뉴 없음'],
(value) => {
const choices = {
'컬럼 표시 설정만': ['columns'],
필터만: ['filter'],
전체: ['columns', 'filter'],
'메뉴 없음': [],
};
grid.options.columnMenu = choices[value];
for (const column of grid.columns) delete column.columnMenu;
grid.refresh();
},
);
report.textContent =
'columnMenu 배열로 헤더 메뉴에 넣을 영역과 순서를 지정합니다. columns=컬럼 표시 설정, filter=컬럼 필터이며 기본값은 전체입니다. 컬럼 배열은 그리드 배열을 대체하며 []는 메뉴를 숨깁니다. 이 예제의 전체 헤더 메뉴 선택은 주문·수량·단가의 모든 헤더에 적용합니다. 선택하면 컬럼별 columnMenu를 제거하고 그리드 기본 구성을 함께 상속합니다. 초기 선택은 전체이며 세 컬럼의 실제 메뉴와 일치합니다. 필터/정렬 상태와 API는 메뉴를 숨겨도 유지합니다. 컬럼 표시 설정은 표시 컬럼 관리와 같은 컬럼 검색·체크 목록을 하위 창으로 제공합니다. 목록의 체크박스·행 간격·색상·검색창·스크롤 스타일은 필터 값 목록과 같습니다. 체크하면 즉시 표시하고 해제하면 즉시 숨깁니다. 마지막 표시 컬럼은 숨길 수 없으며 권한이 없는 컬럼은 목록에서 제외합니다. 메뉴에는 정렬 항목을 제공하지 않습니다. 필터는 값 검색·모두 선택·체크 목록과 적용/초기화 버튼으로 구성합니다. 검색 중 적용하면 검색 결과에서 체크한 값만 반영합니다. 텍스트/숫자/날짜 필터를 누르면 두 조건을 AND/OR로 결합하는 하위 창이 열립니다. 각 적용은 해당 컬럼의 기존 필터만 교체하며 다른 컬럼·전역 조건은 유지합니다. Escape로 하위 창부터 닫고, 바깥 클릭은 전체를 닫습니다. 목록은 적재한 데이터 기준이며 200개씩 더 볼 수 있습니다. 서버 전체 데이터의 조건 조회는 조건 필터를 사용합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './column-tools.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
columnMenu: ['columns', 'filter'],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('컬럼 표시 설정', () => grid.openColumns());
select(
'전체 헤더 메뉴',
['전체', '컬럼 표시 설정만', '필터만', '메뉴 없음'],
(value) => {
const choices = {
'컬럼 표시 설정만': ['columns'],
필터만: ['filter'],
전체: ['columns', 'filter'],
'메뉴 없음': [],
};
grid.options.columnMenu = choices[value];
for (const column of grid.columns) delete column.columnMenu;
grid.refresh();
},
);
message.value =
'columnMenu 배열로 헤더 메뉴에 넣을 영역과 순서를 지정합니다. columns=컬럼 표시 설정, filter=컬럼 필터이며 기본값은 전체입니다. 컬럼 배열은 그리드 배열을 대체하며 []는 메뉴를 숨깁니다. 이 예제의 전체 헤더 메뉴 선택은 주문·수량·단가의 모든 헤더에 적용합니다. 선택하면 컬럼별 columnMenu를 제거하고 그리드 기본 구성을 함께 상속합니다. 초기 선택은 전체이며 세 컬럼의 실제 메뉴와 일치합니다. 필터/정렬 상태와 API는 메뉴를 숨겨도 유지합니다. 컬럼 표시 설정은 표시 컬럼 관리와 같은 컬럼 검색·체크 목록을 하위 창으로 제공합니다. 목록의 체크박스·행 간격·색상·검색창·스크롤 스타일은 필터 값 목록과 같습니다. 체크하면 즉시 표시하고 해제하면 즉시 숨깁니다. 마지막 표시 컬럼은 숨길 수 없으며 권한이 없는 컬럼은 목록에서 제외합니다. 메뉴에는 정렬 항목을 제공하지 않습니다. 필터는 값 검색·모두 선택·체크 목록과 적용/초기화 버튼으로 구성합니다. 검색 중 적용하면 검색 결과에서 체크한 값만 반영합니다. 텍스트/숫자/날짜 필터를 누르면 두 조건을 AND/OR로 결합하는 하위 창이 열립니다. 각 적용은 해당 컬럼의 기존 필터만 교체하며 다른 컬럼·전역 조건은 유지합니다. Escape로 하위 창부터 닫고, 바깥 클릭은 전체를 닫습니다. 목록은 적재한 데이터 기준이며 200개씩 더 볼 수 있습니다. 서버 전체 데이터의 조건 조회는 조건 필터를 사용합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>컬럼 메뉴 · 표시 · 필터</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
좌우 컬럼과 상하 행을 고정합니다. 고정 행은 본문에서 제외됩니다.
컬럼 pinned와 그리드 pinnedTopRows/pinnedBottomRows를 생성 전에 지정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].pinned = 'left'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · pinned.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/pinned.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>컬럼 · 행 고정</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>컬럼 · 행 고정</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="pinned.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './pinned.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240, pinned: 'left' },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{
field: 'price',
title: '단가',
type: 'number',
parse: Number,
pinned: 'right',
},
],
plugins: [],
pinnedTopRows: [1],
pinnedBottomRows: [100],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'좌우 컬럼과 상하 행을 고정합니다. 고정 행은 본문에서 제외됩니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './pinned.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240, pinned: 'left' },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{
field: 'price',
title: '단가',
type: 'number',
parse: Number,
pinned: 'right',
},
],
plugins: [],
pinnedTopRows: [1],
pinnedBottomRows: [100],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'좌우 컬럼과 상하 행을 고정합니다. 고정 행은 본문에서 제외됩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>컬럼 · 행 고정</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '주문 1',
qty: 0,
price: 1000,
},
{
id: 2,
name: '주문 2',
qty: 1,
price: 1000,
},
{
id: 3,
name: '주문 3',
qty: 2,
price: 1000,
},
{
id: 4,
name: '주문 4',
qty: 3,
price: 1000,
},
{
id: 5,
name: '주문 5',
qty: 4,
price: 1000,
},
{
id: 6,
name: '주문 6',
qty: 5,
price: 1000,
},
{
id: 7,
name: '주문 7',
qty: 6,
price: 1000,
},
{
id: 8,
name: '주문 8',
qty: 7,
price: 1000,
},
{
id: 9,
name: '주문 9',
qty: 8,
price: 1000,
},
{
id: 10,
name: '주문 10',
qty: 9,
price: 1000,
},
{
id: 11,
name: '주문 11',
qty: 10,
price: 1000,
},
{
id: 12,
name: '주문 12',
qty: 11,
price: 1000,
},
{
id: 13,
name: '주문 13',
qty: 12,
price: 1000,
},
{
id: 14,
name: '주문 14',
qty: 13,
price: 1000,
},
{
id: 15,
name: '주문 15',
qty: 14,
price: 1000,
},
{
id: 16,
name: '주문 16',
qty: 15,
price: 1000,
},
{
id: 17,
name: '주문 17',
qty: 16,
price: 1000,
},
{
id: 18,
name: '주문 18',
qty: 17,
price: 1000,
},
{
id: 19,
name: '주문 19',
qty: 18,
price: 1000,
},
{
id: 20,
name: '주문 20',
qty: 19,
price: 1000,
},
{
id: 21,
name: '주문 21',
qty: 20,
price: 1000,
},
{
id: 22,
name: '주문 22',
qty: 21,
price: 1000,
},
{
id: 23,
name: '주문 23',
qty: 22,
price: 1000,
},
{
id: 24,
name: '주문 24',
qty: 23,
price: 1000,
},
{
id: 25,
name: '주문 25',
qty: 24,
price: 1000,
},
{
id: 26,
name: '주문 26',
qty: 25,
price: 1000,
},
{
id: 27,
name: '주문 27',
qty: 26,
price: 1000,
},
{
id: 28,
name: '주문 28',
qty: 27,
price: 1000,
},
{
id: 29,
name: '주문 29',
qty: 28,
price: 1000,
},
{
id: 30,
name: '주문 30',
qty: 29,
price: 1000,
},
{
id: 31,
name: '주문 1 31',
qty: 0,
price: 1000,
},
{
id: 32,
name: '주문 2 32',
qty: 1,
price: 1000,
},
{
id: 33,
name: '주문 3 33',
qty: 2,
price: 1000,
},
{
id: 34,
name: '주문 4 34',
qty: 3,
price: 1000,
},
{
id: 35,
name: '주문 5 35',
qty: 4,
price: 1000,
},
{
id: 36,
name: '주문 6 36',
qty: 5,
price: 1000,
},
{
id: 37,
name: '주문 7 37',
qty: 6,
price: 1000,
},
{
id: 38,
name: '주문 8 38',
qty: 7,
price: 1000,
},
{
id: 39,
name: '주문 9 39',
qty: 8,
price: 1000,
},
{
id: 40,
name: '주문 10 40',
qty: 9,
price: 1000,
},
{
id: 41,
name: '주문 11 41',
qty: 10,
price: 1000,
},
{
id: 42,
name: '주문 12 42',
qty: 11,
price: 1000,
},
{
id: 43,
name: '주문 13 43',
qty: 12,
price: 1000,
},
{
id: 44,
name: '주문 14 44',
qty: 13,
price: 1000,
},
{
id: 45,
name: '주문 15 45',
qty: 14,
price: 1000,
},
{
id: 46,
name: '주문 16 46',
qty: 15,
price: 1000,
},
{
id: 47,
name: '주문 17 47',
qty: 16,
price: 1000,
},
{
id: 48,
name: '주문 18 48',
qty: 17,
price: 1000,
},
{
id: 49,
name: '주문 19 49',
qty: 18,
price: 1000,
},
{
id: 50,
name: '주문 20 50',
qty: 19,
price: 1000,
},
{
id: 51,
name: '주문 21 51',
qty: 20,
price: 1000,
},
{
id: 52,
name: '주문 22 52',
qty: 21,
price: 1000,
},
{
id: 53,
name: '주문 23 53',
qty: 22,
price: 1000,
},
{
id: 54,
name: '주문 24 54',
qty: 23,
price: 1000,
},
{
id: 55,
name: '주문 25 55',
qty: 24,
price: 1000,
},
{
id: 56,
name: '주문 26 56',
qty: 25,
price: 1000,
},
{
id: 57,
name: '주문 27 57',
qty: 26,
price: 1000,
},
{
id: 58,
name: '주문 28 58',
qty: 27,
price: 1000,
},
{
id: 59,
name: '주문 29 59',
qty: 28,
price: 1000,
},
{
id: 60,
name: '주문 30 60',
qty: 29,
price: 1000,
},
{
id: 61,
name: '주문 1 61',
qty: 0,
price: 1000,
},
{
id: 62,
name: '주문 2 62',
qty: 1,
price: 1000,
},
{
id: 63,
name: '주문 3 63',
qty: 2,
price: 1000,
},
{
id: 64,
name: '주문 4 64',
qty: 3,
price: 1000,
},
{
id: 65,
name: '주문 5 65',
qty: 4,
price: 1000,
},
{
id: 66,
name: '주문 6 66',
qty: 5,
price: 1000,
},
{
id: 67,
name: '주문 7 67',
qty: 6,
price: 1000,
},
{
id: 68,
name: '주문 8 68',
qty: 7,
price: 1000,
},
{
id: 69,
name: '주문 9 69',
qty: 8,
price: 1000,
},
{
id: 70,
name: '주문 10 70',
qty: 9,
price: 1000,
},
{
id: 71,
name: '주문 11 71',
qty: 10,
price: 1000,
},
{
id: 72,
name: '주문 12 72',
qty: 11,
price: 1000,
},
{
id: 73,
name: '주문 13 73',
qty: 12,
price: 1000,
},
{
id: 74,
name: '주문 14 74',
qty: 13,
price: 1000,
},
{
id: 75,
name: '주문 15 75',
qty: 14,
price: 1000,
},
{
id: 76,
name: '주문 16 76',
qty: 15,
price: 1000,
},
{
id: 77,
name: '주문 17 77',
qty: 16,
price: 1000,
},
{
id: 78,
name: '주문 18 78',
qty: 17,
price: 1000,
},
{
id: 79,
name: '주문 19 79',
qty: 18,
price: 1000,
},
{
id: 80,
name: '주문 20 80',
qty: 19,
price: 1000,
},
{
id: 81,
name: '주문 21 81',
qty: 20,
price: 1000,
},
{
id: 82,
name: '주문 22 82',
qty: 21,
price: 1000,
},
{
id: 83,
name: '주문 23 83',
qty: 22,
price: 1000,
},
{
id: 84,
name: '주문 24 84',
qty: 23,
price: 1000,
},
{
id: 85,
name: '주문 25 85',
qty: 24,
price: 1000,
},
{
id: 86,
name: '주문 26 86',
qty: 25,
price: 1000,
},
{
id: 87,
name: '주문 27 87',
qty: 26,
price: 1000,
},
{
id: 88,
name: '주문 28 88',
qty: 27,
price: 1000,
},
{
id: 89,
name: '주문 29 89',
qty: 28,
price: 1000,
},
{
id: 90,
name: '주문 30 90',
qty: 29,
price: 1000,
},
{
id: 91,
name: '주문 1 91',
qty: 0,
price: 1000,
},
{
id: 92,
name: '주문 2 92',
qty: 1,
price: 1000,
},
{
id: 93,
name: '주문 3 93',
qty: 2,
price: 1000,
},
{
id: 94,
name: '주문 4 94',
qty: 3,
price: 1000,
},
{
id: 95,
name: '주문 5 95',
qty: 4,
price: 1000,
},
{
id: 96,
name: '주문 6 96',
qty: 5,
price: 1000,
},
{
id: 97,
name: '주문 7 97',
qty: 6,
price: 1000,
},
{
id: 98,
name: '주문 8 98',
qty: 7,
price: 1000,
},
{
id: 99,
name: '주문 9 99',
qty: 8,
price: 1000,
},
{
id: 100,
name: '주문 10 100',
qty: 9,
price: 1000,
},
];
rowNumbers:true는 맨 앞에 고정된 No. 컬럼을 표시합니다. 현재 정렬·필터·트리 펼침 상태의 데이터 행 순서로1부터 번호를 매깁니다. 이 컬럼은 이동·숨김·편집할 수 없고 별도 배경을 사용합니다. 원본 행 데이터나 CSV·저장 데이터에는 번호를 추가하지 않습니다.
화면 전용 행번호를 맨 앞에 표시합니다. 원본 데이터 필드를 추가하지 않습니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.rowNumbers = true처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · row-numbers.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/row-numbers.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>자동 행번호</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>자동 행번호</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="row-numbers.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './row-numbers.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
rowNumbers: true,
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('이름 역순', () => grid.setSort([{ field: 'name', dir: 'desc' }]));
button('정렬 해제', () => grid.setSort([]));
button('첫 표시 행 삭제', () => grid.deleteRows([grid.getRows()[0].id]));
report.textContent =
'rowNumbers:true는 맨 앞에 고정된 No. 컬럼을 표시합니다. 현재 정렬·필터·트리 펼침 상태의 데이터 행 순서로1부터 번호를 매깁니다. 이 컬럼은 이동·숨김·편집할 수 없고 별도 배경을 사용합니다. 원본 행 데이터나 CSV·저장 데이터에는 번호를 추가하지 않습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './row-numbers.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
rowNumbers: true,
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('이름 역순', () => grid.setSort([{ field: 'name', dir: 'desc' }]));
button('정렬 해제', () => grid.setSort([]));
button('첫 표시 행 삭제', () => grid.deleteRows([grid.getRows()[0].id]));
message.value =
'rowNumbers:true는 맨 앞에 고정된 No. 컬럼을 표시합니다. 현재 정렬·필터·트리 펼침 상태의 데이터 행 순서로1부터 번호를 매깁니다. 이 컬럼은 이동·숨김·편집할 수 없고 별도 배경을 사용합니다. 원본 행 데이터나 CSV·저장 데이터에는 번호를 추가하지 않습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>자동 행번호</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
컬럼 배치와 정렬·필터·고정 상태를 저장하고 복원합니다.
생성된 그리드의 컬럼 배치를 저장·복원합니다. savedLayout에는 saveLayout 반환값을 사용합니다. 생성 후 grid.saveLayout(); grid.restoreLayout(savedLayout)를 호출합니다.
행 데이터: 최소 100행 · layout-state.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/layout-state.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>레이아웃 저장 · 복원</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>레이아웃 저장 · 복원</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="layout-state.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './layout-state.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
let layout;
button('레이아웃 저장', () => {
layout = grid.saveLayout();
localStorage.setItem('zixo-feature-layout', JSON.stringify(layout));
return layout;
});
button('너비 변경', () => grid.setColumnWidth('name', 100));
button('레이아웃 복원', () =>
grid.restoreLayout(
layout ?? JSON.parse(localStorage.getItem('zixo-feature-layout')),
),
);
report.textContent = '컬럼 배치와 정렬·필터·고정 상태를 저장하고 복원합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './layout-state.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
let layout;
button('레이아웃 저장', () => {
layout = grid.saveLayout();
localStorage.setItem('zixo-feature-layout', JSON.stringify(layout));
return layout;
});
button('너비 변경', () => grid.setColumnWidth('name', 100));
button('레이아웃 복원', () =>
grid.restoreLayout(
layout ?? JSON.parse(localStorage.getItem('zixo-feature-layout')),
),
);
message.value = '컬럼 배치와 정렬·필터·고정 상태를 저장하고 복원합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>레이아웃 저장 · 복원</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
Shift·Ctrl로 범위를 선택하고 Ctrl+C/V로 복사/붙여넣기 합니다. Fill Handle은 값을 복제합니다.
생성 후 행 키와 필드로 선택합니다. 복사·붙여넣기·Fill은 선택 범위에 적용됩니다. 생성 후 grid.selectCell(1, 'name'); grid.selectAll()를 호출합니다.
행 데이터: 최소 100행 · selection.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/selection.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>범위 선택 · 클립보드 · Fill</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>범위 선택 · 클립보드 · Fill</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="selection.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './selection.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('선택 셀 확인', () => grid.selectedCells());
button('선택 전체', () => grid.selectAll());
report.textContent =
'Shift·Ctrl로 범위를 선택하고 Ctrl+C/V로 복사/붙여넣기 합니다. Fill Handle은 값을 복제합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './selection.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('선택 셀 확인', () => grid.selectedCells());
button('선택 전체', () => grid.selectAll());
message.value =
'Shift·Ctrl로 범위를 선택하고 Ctrl+C/V로 복사/붙여넣기 합니다. Fill Handle은 값을 복제합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>범위 선택 · 클립보드 · Fill</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
방향키·Home/End·PageUp/Down은 이동, Enter/F2는 편집, Tab은 다음 셀입니다. 활성 셀로 위치를 표시합니다.
활성 셀을 선택한 뒤 방향키·Tab·Home/End 등으로 이동합니다. 키보드 입력은 별도 컬럼 초기 속성이 아닙니다. 생성 후 grid.selectCell(1, 'name')를 호출합니다.
행 데이터: 최소 100행 · keyboard.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/keyboard.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>키보드 탐색 · 포커스</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>키보드 탐색 · 포커스</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="keyboard.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './keyboard.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('첫 셀 선택', () => {
grid.selectCell(1, 'name');
grid.root.focus();
});
report.textContent =
'방향키·Home/End·PageUp/Down은 이동, Enter/F2는 편집, Tab은 다음 셀입니다. 활성 셀로 위치를 표시합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './keyboard.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('첫 셀 선택', () => {
grid.selectCell(1, 'name');
grid.root.focus();
});
message.value =
'방향키·Home/End·PageUp/Down은 이동, Enter/F2는 편집, Tab은 다음 셀입니다. 활성 셀로 위치를 표시합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>키보드 탐색 · 포커스</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
검증 실패 시 10,000셀 전체를 거부하고 기존 값을 유지합니다. 가로 스크롤은 셀을 다시 만들지 않고 헤더와 함께 이동합니다. 세로 이동 시 표시 행을 갱신합니다.
각 컬럼의 parse/validate를 생성 전에 지정합니다. pasteText는 전체 범위를 검증한 뒤 한 번에 적용합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].validate = value => value >= 0 || '음수 금지'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · large-paste.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/large-paste.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>100×100 원자 붙여넣기</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>100×100 원자 붙여넣기</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="large-paste.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './large-paste.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: Array.from({ length: 100 }, (_, j) => ({
field: 'c' + j,
title: 'C' + j,
width: 70,
parse: Number,
validate: (v) => v >= 0 || '음수 금지',
})),
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
const text = Array.from({ length: 100 }, () =>
Array(100).fill('1').join('\t'),
).join('\n');
button('100×100 반영', () => grid.pasteText(text, { id: 1, field: 'c0' }));
button('마지막 값 검증 실패', () =>
grid.pasteText(text.replace(/1$/, '-1'), { id: 1, field: 'c0' }),
);
report.textContent =
'검증 실패 시 10,000셀 전체를 거부하고 기존 값을 유지합니다. 가로 스크롤은 셀을 다시 만들지 않고 헤더와 함께 이동합니다. 세로 이동 시 표시 행을 갱신합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './large-paste.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: Array.from({ length: 100 }, (_, j) => ({
field: 'c' + j,
title: 'C' + j,
width: 70,
parse: Number,
validate: (v) => v >= 0 || '음수 금지',
})),
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
const text = Array.from({ length: 100 }, () =>
Array(100).fill('1').join('\t'),
).join('\n');
button('100×100 반영', () => grid.pasteText(text, { id: 1, field: 'c0' }));
button('마지막 값 검증 실패', () =>
grid.pasteText(text.replace(/1$/, '-1'), { id: 1, field: 'c0' }),
);
message.value =
'검증 실패 시 10,000셀 전체를 거부하고 기존 값을 유지합니다. 가로 스크롤은 셀을 다시 만들지 않고 헤더와 함께 이동합니다. 세로 이동 시 표시 행을 갱신합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>100×100 원자 붙여넣기</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 최소 100행. 대량 데이터는 배열 생성식을 사용합니다.
export const rows = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
...Object.fromEntries(Array.from({ length: 100 }, (_, j) => ['c' + j, 0])),
}));
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
Enter/F2/더블클릭으로 셀을 직접 편집합니다. Enter/Tab 확정, Escape 취소. parse 후 validate를 실행합니다.
편집할 컬럼의 editor/parse/validate를 설정합니다. 수정은 셀 내부에서 진행합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[1].validate = value => (Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · inline-edit.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/inline-edit.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>셀 내부 편집 · 검증</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>셀 내부 편집 · 검증</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="inline-edit.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './inline-edit.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (v) =>
(Number.isFinite(v) && v >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'Enter/F2/더블클릭으로 셀을 직접 편집합니다. Enter/Tab 확정, Escape 취소. parse 후 validate를 실행합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './inline-edit.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (v) =>
(Number.isFinite(v) && v >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'Enter/F2/더블클릭으로 셀을 직접 편집합니다. Enter/Tab 확정, Escape 취소. parse 후 validate를 실행합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>셀 내부 편집 · 검증</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
원클릭 select · radio · checkbox 예제 독립 실행 ↗
select·radio·checkbox는 셀에서 바로 변경하며 선택지의 값 타입을 유지합니다.
select/radio 선택지는 컬럼 options에 지정합니다. checkbox는 boolean 값을 사용합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].options = [{value:1,label:'사용'},{value:0,label:'중지'}]처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · select-editor.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/select-editor.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>원클릭 select · radio · checkbox</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>원클릭 select · radio · checkbox</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="select-editor.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './select-editor.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'status',
title: '상태',
editor: 'select',
options: [
{ value: 1, label: '사용' },
{ value: 0, label: '중지' },
],
},
{
field: 'choice',
title: '구분',
editor: 'radio',
options: ['A', 'B', 'C'],
width: 260,
},
{ field: 'active', title: '사용', editor: 'checkbox' },
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'select·radio·checkbox는 셀에서 바로 변경하며 선택지의 값 타입을 유지합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './select-editor.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'status',
title: '상태',
editor: 'select',
options: [
{ value: 1, label: '사용' },
{ value: 0, label: '중지' },
],
},
{
field: 'choice',
title: '구분',
editor: 'radio',
options: ['A', 'B', 'C'],
width: 260,
},
{ field: 'active', title: '사용', editor: 'checkbox' },
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'select·radio·checkbox는 셀에서 바로 변경하며 선택지의 값 타입을 유지합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>원클릭 select · radio · checkbox</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
status: 1,
choice: 'A',
active: true,
},
{
id: 2,
status: 0,
choice: 'B',
active: false,
},
{
id: 3,
status: 1,
choice: 'A',
active: true,
},
{
id: 4,
status: 0,
choice: 'B',
active: false,
},
{
id: 5,
status: 1,
choice: 'A',
active: true,
},
{
id: 6,
status: 0,
choice: 'B',
active: false,
},
{
id: 7,
status: 1,
choice: 'A',
active: true,
},
{
id: 8,
status: 0,
choice: 'B',
active: false,
},
{
id: 9,
status: 1,
choice: 'A',
active: true,
},
{
id: 10,
status: 0,
choice: 'B',
active: false,
},
{
id: 11,
status: 1,
choice: 'A',
active: true,
},
{
id: 12,
status: 0,
choice: 'B',
active: false,
},
{
id: 13,
status: 1,
choice: 'A',
active: true,
},
{
id: 14,
status: 0,
choice: 'B',
active: false,
},
{
id: 15,
status: 1,
choice: 'A',
active: true,
},
{
id: 16,
status: 0,
choice: 'B',
active: false,
},
{
id: 17,
status: 1,
choice: 'A',
active: true,
},
{
id: 18,
status: 0,
choice: 'B',
active: false,
},
{
id: 19,
status: 1,
choice: 'A',
active: true,
},
{
id: 20,
status: 0,
choice: 'B',
active: false,
},
{
id: 21,
status: 1,
choice: 'A',
active: true,
},
{
id: 22,
status: 0,
choice: 'B',
active: false,
},
{
id: 23,
status: 1,
choice: 'A',
active: true,
},
{
id: 24,
status: 0,
choice: 'B',
active: false,
},
{
id: 25,
status: 1,
choice: 'A',
active: true,
},
{
id: 26,
status: 0,
choice: 'B',
active: false,
},
{
id: 27,
status: 1,
choice: 'A',
active: true,
},
{
id: 28,
status: 0,
choice: 'B',
active: false,
},
{
id: 29,
status: 1,
choice: 'A',
active: true,
},
{
id: 30,
status: 0,
choice: 'B',
active: false,
},
{
id: 31,
status: 1,
choice: 'A',
active: true,
},
{
id: 32,
status: 0,
choice: 'B',
active: false,
},
{
id: 33,
status: 1,
choice: 'A',
active: true,
},
{
id: 34,
status: 0,
choice: 'B',
active: false,
},
{
id: 35,
status: 1,
choice: 'A',
active: true,
},
{
id: 36,
status: 0,
choice: 'B',
active: false,
},
{
id: 37,
status: 1,
choice: 'A',
active: true,
},
{
id: 38,
status: 0,
choice: 'B',
active: false,
},
{
id: 39,
status: 1,
choice: 'A',
active: true,
},
{
id: 40,
status: 0,
choice: 'B',
active: false,
},
{
id: 41,
status: 1,
choice: 'A',
active: true,
},
{
id: 42,
status: 0,
choice: 'B',
active: false,
},
{
id: 43,
status: 1,
choice: 'A',
active: true,
},
{
id: 44,
status: 0,
choice: 'B',
active: false,
},
{
id: 45,
status: 1,
choice: 'A',
active: true,
},
{
id: 46,
status: 0,
choice: 'B',
active: false,
},
{
id: 47,
status: 1,
choice: 'A',
active: true,
},
{
id: 48,
status: 0,
choice: 'B',
active: false,
},
{
id: 49,
status: 1,
choice: 'A',
active: true,
},
{
id: 50,
status: 0,
choice: 'B',
active: false,
},
{
id: 51,
status: 1,
choice: 'A',
active: true,
},
{
id: 52,
status: 0,
choice: 'B',
active: false,
},
{
id: 53,
status: 1,
choice: 'A',
active: true,
},
{
id: 54,
status: 0,
choice: 'B',
active: false,
},
{
id: 55,
status: 1,
choice: 'A',
active: true,
},
{
id: 56,
status: 0,
choice: 'B',
active: false,
},
{
id: 57,
status: 1,
choice: 'A',
active: true,
},
{
id: 58,
status: 0,
choice: 'B',
active: false,
},
{
id: 59,
status: 1,
choice: 'A',
active: true,
},
{
id: 60,
status: 0,
choice: 'B',
active: false,
},
{
id: 61,
status: 1,
choice: 'A',
active: true,
},
{
id: 62,
status: 0,
choice: 'B',
active: false,
},
{
id: 63,
status: 1,
choice: 'A',
active: true,
},
{
id: 64,
status: 0,
choice: 'B',
active: false,
},
{
id: 65,
status: 1,
choice: 'A',
active: true,
},
{
id: 66,
status: 0,
choice: 'B',
active: false,
},
{
id: 67,
status: 1,
choice: 'A',
active: true,
},
{
id: 68,
status: 0,
choice: 'B',
active: false,
},
{
id: 69,
status: 1,
choice: 'A',
active: true,
},
{
id: 70,
status: 0,
choice: 'B',
active: false,
},
{
id: 71,
status: 1,
choice: 'A',
active: true,
},
{
id: 72,
status: 0,
choice: 'B',
active: false,
},
{
id: 73,
status: 1,
choice: 'A',
active: true,
},
{
id: 74,
status: 0,
choice: 'B',
active: false,
},
{
id: 75,
status: 1,
choice: 'A',
active: true,
},
{
id: 76,
status: 0,
choice: 'B',
active: false,
},
{
id: 77,
status: 1,
choice: 'A',
active: true,
},
{
id: 78,
status: 0,
choice: 'B',
active: false,
},
{
id: 79,
status: 1,
choice: 'A',
active: true,
},
{
id: 80,
status: 0,
choice: 'B',
active: false,
},
{
id: 81,
status: 1,
choice: 'A',
active: true,
},
{
id: 82,
status: 0,
choice: 'B',
active: false,
},
{
id: 83,
status: 1,
choice: 'A',
active: true,
},
{
id: 84,
status: 0,
choice: 'B',
active: false,
},
{
id: 85,
status: 1,
choice: 'A',
active: true,
},
{
id: 86,
status: 0,
choice: 'B',
active: false,
},
{
id: 87,
status: 1,
choice: 'A',
active: true,
},
{
id: 88,
status: 0,
choice: 'B',
active: false,
},
{
id: 89,
status: 1,
choice: 'A',
active: true,
},
{
id: 90,
status: 0,
choice: 'B',
active: false,
},
{
id: 91,
status: 1,
choice: 'A',
active: true,
},
{
id: 92,
status: 0,
choice: 'B',
active: false,
},
{
id: 93,
status: 1,
choice: 'A',
active: true,
},
{
id: 94,
status: 0,
choice: 'B',
active: false,
},
{
id: 95,
status: 1,
choice: 'A',
active: true,
},
{
id: 96,
status: 0,
choice: 'B',
active: false,
},
{
id: 97,
status: 1,
choice: 'A',
active: true,
},
{
id: 98,
status: 0,
choice: 'B',
active: false,
},
{
id: 99,
status: 1,
choice: 'A',
active: true,
},
{
id: 100,
status: 0,
choice: 'B',
active: false,
},
];
dateFormat으로 컬럼마다 표시·입력 형식을 지정합니다. YYYY.MM.DD, DD/MM/YYYY, YYYY년 M월 D일을 비교하세요. 저장 값은 YYYY-MM-DD입니다. ISO 입력·918/0918 단축 입력·달력 선택도 지원하며 잘못된 날짜는 거부합니다.
날짜 컬럼의 표시·입력 형식이며 저장 값은 ISO 날짜로 유지합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].dateFormat = 'YYYY.MM.DD'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · date-editor.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/date-editor.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>날짜 입력 · 달력</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>날짜 입력 · 달력</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="date-editor.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './date-editor.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'date',
title: '점 구분 일자',
editor: 'date',
dateFormat: 'YYYY.MM.DD',
width: 200,
},
{
field: 'dateSlash',
title: '일/월/년',
editor: 'date',
dateFormat: 'DD/MM/YYYY',
width: 200,
},
{
field: 'dateKorean',
title: '한국어 일자',
editor: 'date',
dateFormat: 'YYYY년 M월 D일',
width: 220,
},
],
plugins: [editorsPlugin()],
referenceYear: 2026,
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'dateFormat으로 컬럼마다 표시·입력 형식을 지정합니다. YYYY.MM.DD, DD/MM/YYYY, YYYY년 M월 D일을 비교하세요. 저장 값은 YYYY-MM-DD입니다. ISO 입력·918/0918 단축 입력·달력 선택도 지원하며 잘못된 날짜는 거부합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './date-editor.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'date',
title: '점 구분 일자',
editor: 'date',
dateFormat: 'YYYY.MM.DD',
width: 200,
},
{
field: 'dateSlash',
title: '일/월/년',
editor: 'date',
dateFormat: 'DD/MM/YYYY',
width: 200,
},
{
field: 'dateKorean',
title: '한국어 일자',
editor: 'date',
dateFormat: 'YYYY년 M월 D일',
width: 220,
},
],
plugins: [editorsPlugin()],
referenceYear: 2026,
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'dateFormat으로 컬럼마다 표시·입력 형식을 지정합니다. YYYY.MM.DD, DD/MM/YYYY, YYYY년 M월 D일을 비교하세요. 저장 값은 YYYY-MM-DD입니다. ISO 입력·918/0918 단축 입력·달력 선택도 지원하며 잘못된 날짜는 거부합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>날짜 입력 · 달력</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 2,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 3,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 4,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 5,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 6,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 7,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 8,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 9,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 10,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 11,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 12,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 13,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 14,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 15,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 16,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 17,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 18,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 19,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 20,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 21,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 22,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 23,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 24,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 25,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 26,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 27,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 28,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 29,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 30,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 31,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 32,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 33,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 34,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 35,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 36,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 37,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 38,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 39,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 40,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 41,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 42,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 43,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 44,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 45,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 46,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 47,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 48,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 49,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 50,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 51,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 52,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 53,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 54,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 55,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 56,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 57,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 58,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 59,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 60,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 61,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 62,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 63,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 64,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 65,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 66,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 67,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 68,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 69,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 70,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 71,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 72,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 73,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 74,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 75,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 76,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 77,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 78,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 79,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 80,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 81,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 82,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 83,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 84,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 85,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 86,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 87,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 88,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 89,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 90,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 91,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 92,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 93,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 94,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 95,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 96,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 97,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 98,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 99,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
{
id: 100,
date: '2026-09-18',
dateSlash: '2026-09-18',
dateKorean: '2026-09-18',
},
];
입력 후 비동기 선택지를 조회합니다. 이전 요청의 signal을 fetch에 전달해 취소합니다.
이 예제의 필수값 loadOptions 필수값 — 이 예제처럼 비동기 선택지를 조회할 때 함수를 제공합니다. 반환값은 선택지 배열입니다. 정적 options만 쓰는 경우에는 생략합니다.비동기 선택지는 loadOptions(query,row,{signal})으로 조회하고 debounce로 요청 대기를 지정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].debounce = 180처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · async-editor.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/async-editor.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>비동기 자동완성</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>비동기 자동완성</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="async-editor.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './async-editor.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'code',
title: '도시 코드',
editor: 'combo',
width: 260,
debounce: 180,
loadOptions: async (query, row, { signal }) => {
const response = await fetch('/docs/city-options.json', { signal });
if (!response.ok) throw Error('코드 조회 실패');
return (await response.json()).filter(
(x) => x.label.includes(query) || x.value.includes(query),
);
},
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'입력 후 비동기 선택지를 조회합니다. 이전 요청의 signal을 fetch에 전달해 취소합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './async-editor.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'code',
title: '도시 코드',
editor: 'combo',
width: 260,
debounce: 180,
loadOptions: async (query, row, { signal }) => {
const response = await fetch('/docs/city-options.json', { signal });
if (!response.ok) throw Error('코드 조회 실패');
return (await response.json()).filter(
(x) => x.label.includes(query) || x.value.includes(query),
);
},
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'입력 후 비동기 선택지를 조회합니다. 이전 요청의 signal을 fetch에 전달해 취소합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>비동기 자동완성</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
code: 'SEOUL',
},
{
id: 2,
code: 'SEOUL',
},
{
id: 3,
code: 'SEOUL',
},
{
id: 4,
code: 'SEOUL',
},
{
id: 5,
code: 'SEOUL',
},
{
id: 6,
code: 'SEOUL',
},
{
id: 7,
code: 'SEOUL',
},
{
id: 8,
code: 'SEOUL',
},
{
id: 9,
code: 'SEOUL',
},
{
id: 10,
code: 'SEOUL',
},
{
id: 11,
code: 'SEOUL',
},
{
id: 12,
code: 'SEOUL',
},
{
id: 13,
code: 'SEOUL',
},
{
id: 14,
code: 'SEOUL',
},
{
id: 15,
code: 'SEOUL',
},
{
id: 16,
code: 'SEOUL',
},
{
id: 17,
code: 'SEOUL',
},
{
id: 18,
code: 'SEOUL',
},
{
id: 19,
code: 'SEOUL',
},
{
id: 20,
code: 'SEOUL',
},
{
id: 21,
code: 'SEOUL',
},
{
id: 22,
code: 'SEOUL',
},
{
id: 23,
code: 'SEOUL',
},
{
id: 24,
code: 'SEOUL',
},
{
id: 25,
code: 'SEOUL',
},
{
id: 26,
code: 'SEOUL',
},
{
id: 27,
code: 'SEOUL',
},
{
id: 28,
code: 'SEOUL',
},
{
id: 29,
code: 'SEOUL',
},
{
id: 30,
code: 'SEOUL',
},
{
id: 31,
code: 'SEOUL',
},
{
id: 32,
code: 'SEOUL',
},
{
id: 33,
code: 'SEOUL',
},
{
id: 34,
code: 'SEOUL',
},
{
id: 35,
code: 'SEOUL',
},
{
id: 36,
code: 'SEOUL',
},
{
id: 37,
code: 'SEOUL',
},
{
id: 38,
code: 'SEOUL',
},
{
id: 39,
code: 'SEOUL',
},
{
id: 40,
code: 'SEOUL',
},
{
id: 41,
code: 'SEOUL',
},
{
id: 42,
code: 'SEOUL',
},
{
id: 43,
code: 'SEOUL',
},
{
id: 44,
code: 'SEOUL',
},
{
id: 45,
code: 'SEOUL',
},
{
id: 46,
code: 'SEOUL',
},
{
id: 47,
code: 'SEOUL',
},
{
id: 48,
code: 'SEOUL',
},
{
id: 49,
code: 'SEOUL',
},
{
id: 50,
code: 'SEOUL',
},
{
id: 51,
code: 'SEOUL',
},
{
id: 52,
code: 'SEOUL',
},
{
id: 53,
code: 'SEOUL',
},
{
id: 54,
code: 'SEOUL',
},
{
id: 55,
code: 'SEOUL',
},
{
id: 56,
code: 'SEOUL',
},
{
id: 57,
code: 'SEOUL',
},
{
id: 58,
code: 'SEOUL',
},
{
id: 59,
code: 'SEOUL',
},
{
id: 60,
code: 'SEOUL',
},
{
id: 61,
code: 'SEOUL',
},
{
id: 62,
code: 'SEOUL',
},
{
id: 63,
code: 'SEOUL',
},
{
id: 64,
code: 'SEOUL',
},
{
id: 65,
code: 'SEOUL',
},
{
id: 66,
code: 'SEOUL',
},
{
id: 67,
code: 'SEOUL',
},
{
id: 68,
code: 'SEOUL',
},
{
id: 69,
code: 'SEOUL',
},
{
id: 70,
code: 'SEOUL',
},
{
id: 71,
code: 'SEOUL',
},
{
id: 72,
code: 'SEOUL',
},
{
id: 73,
code: 'SEOUL',
},
{
id: 74,
code: 'SEOUL',
},
{
id: 75,
code: 'SEOUL',
},
{
id: 76,
code: 'SEOUL',
},
{
id: 77,
code: 'SEOUL',
},
{
id: 78,
code: 'SEOUL',
},
{
id: 79,
code: 'SEOUL',
},
{
id: 80,
code: 'SEOUL',
},
{
id: 81,
code: 'SEOUL',
},
{
id: 82,
code: 'SEOUL',
},
{
id: 83,
code: 'SEOUL',
},
{
id: 84,
code: 'SEOUL',
},
{
id: 85,
code: 'SEOUL',
},
{
id: 86,
code: 'SEOUL',
},
{
id: 87,
code: 'SEOUL',
},
{
id: 88,
code: 'SEOUL',
},
{
id: 89,
code: 'SEOUL',
},
{
id: 90,
code: 'SEOUL',
},
{
id: 91,
code: 'SEOUL',
},
{
id: 92,
code: 'SEOUL',
},
{
id: 93,
code: 'SEOUL',
},
{
id: 94,
code: 'SEOUL',
},
{
id: 95,
code: 'SEOUL',
},
{
id: 96,
code: 'SEOUL',
},
{
id: 97,
code: 'SEOUL',
},
{
id: 98,
code: 'SEOUL',
},
{
id: 99,
code: 'SEOUL',
},
{
id: 100,
code: 'SEOUL',
},
];
두 번째 행의 단가는 읽기 전용입니다. readonly는 editable보다 우선하며 모델에서도 검사합니다.
행과 컬럼을 기준으로 읽기 전용 조건을 지정합니다. readonly는 editable보다 우선합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.readonly = (row, column) => row.id === 2 && column.field === 'price'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · readonly.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/readonly.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>조건부 readonly</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>조건부 readonly</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="readonly.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './readonly.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
readonly: (row, column) => row.id === 2 && column.field === 'price',
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'두 번째 행의 단가는 읽기 전용입니다. readonly는 editable보다 우선하며 모델에서도 검사합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './readonly.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
readonly: (row, column) => row.id === 2 && column.field === 'price',
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'두 번째 행의 단가는 읽기 전용입니다. readonly는 editable보다 우선하며 모델에서도 검사합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>조건부 readonly</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
insert(row)는 한 행, insertRows(rows) 또는 insert(rows)는 여러 행을 한 번에 추가합니다. 일괄 추가는 Undo 한 번으로 취소합니다. ID 중복·누락 또는 계산 실패 시 전체 추가를 거부합니다. setRows(rows)는 전체 데이터를 교체하고 새 기준으로 초기화합니다. clearRows()는 setRows([])와 동일하게 전체를 비우고 변경 목록·Undo/Redo도 초기화합니다. 삭제 내역을 저장하려면 deleteRows(ids)를 사용하세요.
이 예제의 필수값 setRows(rows), insertRows(rows)의 rows 필수값 — 행 배열을 전달합니다. []도 유효합니다. 각 행에 rowKey 값이 필요합니다.updateCells(changes)의 id·field·value 필수값 — 수정할 행 키·컬럼·값을 전달합니다. 0·false·빈 문자열도 값으로 사용할 수 있습니다.행 추가·셀 수정·행 삭제는 생성된 그리드의 API를 사용합니다. 각 변수에는 추가 행·변경 셀·행 키를 전달합니다. 생성 후 grid.insertRows(newRows); grid.updateCells(changes); grid.deleteRows(ids)를 호출합니다.
데이터 설정과 행 추가의 차이 생성 옵션 rows는 생략할 수 있습니다. 생성 후 setRows(rows)로 조회 결과를 넣으면 전체 데이터를 교체하고 변경 상태·Undo/Redo를 초기화합니다. 기존 데이터에 신규 행을 추가하려면 insertRows(rows)를 사용합니다. insert(row)와 insert(rows)도 지원합니다. 일괄 추가는 한 번의 Undo로 취소되며 ID 중복·누락/계산 실패 시 모두 거부합니다. 컬럼 parse/validate는 셀 편집 때 적용되므로, 추가할 행 값은 호출 전에 준비하세요.
const grid = new ZixoGrid(host, { columns, rowKey: 'id' });
grid.setRows(rows); // 전체 교체 · 새 기준 데이터
grid.insertRows(newRows); // 기존 행 유지 · 여러 행을 변경 목록에 추가
grid.undo(); // 추가한 여러 행을 한 번에 취소행 데이터: 최소 100행 · crud.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/crud.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>행 추가 · 수정 · 삭제 · 원복</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>행 추가 · 수정 · 삭제 · 원복</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="crud.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows, newRows } from './crud.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('행 추가', () =>
grid.insert({ id: 101, name: '새 주문', qty: 1, price: 1000 }),
);
button('여러 행 추가', () => grid.insertRows(newRows));
button('Undo', () => grid.undo());
button('Redo', () => grid.redo());
button('첫 행 수정', () =>
grid.updateCells([{ id: 1, field: 'name', value: '수정한 주문' }]),
);
button('첫 행 삭제', () => grid.deleteRows([1]));
button('전체 비우기', () => grid.clearRows());
button('조회 데이터 복원', () => grid.setRows(rows));
button('원복', () => grid.revertChanges());
report.textContent =
'insert(row)는 한 행, insertRows(rows) 또는 insert(rows)는 여러 행을 한 번에 추가합니다. 일괄 추가는 Undo 한 번으로 취소합니다. ID 중복·누락 또는 계산 실패 시 전체 추가를 거부합니다. setRows(rows)는 전체 데이터를 교체하고 새 기준으로 초기화합니다. clearRows()는 setRows([])와 동일하게 전체를 비우고 변경 목록·Undo/Redo도 초기화합니다. 삭제 내역을 저장하려면 deleteRows(ids)를 사용하세요.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows, newRows } from './crud.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('행 추가', () =>
grid.insert({ id: 101, name: '새 주문', qty: 1, price: 1000 }),
);
button('여러 행 추가', () => grid.insertRows(newRows));
button('Undo', () => grid.undo());
button('Redo', () => grid.redo());
button('첫 행 수정', () =>
grid.updateCells([{ id: 1, field: 'name', value: '수정한 주문' }]),
);
button('첫 행 삭제', () => grid.deleteRows([1]));
button('전체 비우기', () => grid.clearRows());
button('조회 데이터 복원', () => grid.setRows(rows));
button('원복', () => grid.revertChanges());
message.value =
'insert(row)는 한 행, insertRows(rows) 또는 insert(rows)는 여러 행을 한 번에 추가합니다. 일괄 추가는 Undo 한 번으로 취소합니다. ID 중복·누락 또는 계산 실패 시 전체 추가를 거부합니다. setRows(rows)는 전체 데이터를 교체하고 새 기준으로 초기화합니다. clearRows()는 setRows([])와 동일하게 전체를 비우고 변경 목록·Undo/Redo도 초기화합니다. 삭제 내역을 저장하려면 deleteRows(ids)를 사용하세요.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>행 추가 · 수정 · 삭제 · 원복</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export const newRows = [
{ id: 102, name: '새 주문 A', qty: 2, price: 1500 },
{ id: 103, name: '새 주문 B', qty: 3, price: 2000 },
{ id: 104, name: '새 주문 C', qty: 4, price: 2500 },
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
rowReorder:true는 별도 컬럼 없이 첫 번째 보이는 데이터 컬럼의 셀 앞에 순서 손잡이를 표시합니다. rowDragField로 대상 컬럼 field를 지정할 수 있으며 예제는 name을 지정합니다. 지정 컬럼을 숨기면 손잡이도 숨겨집니다. 드래그 삽입선 또는 손잡이에서 Alt+위/아래로 행을 이동합니다. getRows는 바뀐 순서를 반환하고 순서 변경 시 getChangedRows·save의 order에 적재된 행 키 전체가 순서대로 포함됩니다. 배열의 순서는 DB에 자동 저장되지 않으므로 서버가 order를 저장해야 하며 이 개발 서버 예제는 실제 반영 후 재조회합니다. 정렬·검색·필터·트리·그룹·고정행·저장 중에는 이동을 막으며 미조회 행은 이동 대상이 아닙니다. Undo/Redo·원복·저장 실패 정책에도 순서를 반영합니다.
별도 컬럼 없이 데이터 셀 앞에 손잡이를 표시합니다. rowDragField로 대상 field를 지정합니다. 서버 저장 시 changes.order를 처리해야 합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.rowReorder = true처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
로컬 rows는 빈 배열이며 서버에서100행을 조회합니다. · row-reorder.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/row-reorder.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>행 드래그 · 순서 저장</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>행 드래그 · 순서 저장</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="row-reorder.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './row-reorder.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 260 },
{ field: 'quantity', title: '수량', parse: Number },
],
plugins: [],
rowReorder: true,
rowDragField: 'name',
rowNumbers: true,
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw Error('조회 실패');
return response.json();
},
},
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('Undo', () => grid.undo());
button('Redo', () => grid.redo());
button('순서 저장', () =>
grid.save(async (changes, { signal }) => {
const response = await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes),
signal,
});
if (!response.ok) throw Error('저장 실패');
report.textContent = '저장 순서: ' + JSON.stringify(changes.order);
return response.json();
}),
);
button('서버 재조회', () => grid.setSearch(''));
report.textContent =
'rowReorder:true는 별도 컬럼 없이 첫 번째 보이는 데이터 컬럼의 셀 앞에 순서 손잡이를 표시합니다. rowDragField로 대상 컬럼 field를 지정할 수 있으며 예제는 name을 지정합니다. 지정 컬럼을 숨기면 손잡이도 숨겨집니다. 드래그 삽입선 또는 손잡이에서 Alt+위/아래로 행을 이동합니다. getRows는 바뀐 순서를 반환하고 순서 변경 시 getChangedRows·save의 order에 적재된 행 키 전체가 순서대로 포함됩니다. 배열의 순서는 DB에 자동 저장되지 않으므로 서버가 order를 저장해야 하며 이 개발 서버 예제는 실제 반영 후 재조회합니다. 정렬·검색·필터·트리·그룹·고정행·저장 중에는 이동을 막으며 미조회 행은 이동 대상이 아닙니다. Undo/Redo·원복·저장 실패 정책에도 순서를 반영합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './row-reorder.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 260 },
{ field: 'quantity', title: '수량', parse: Number },
],
plugins: [],
rowReorder: true,
rowDragField: 'name',
rowNumbers: true,
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw Error('조회 실패');
return response.json();
},
},
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('Undo', () => grid.undo());
button('Redo', () => grid.redo());
button('순서 저장', () =>
grid.save(async (changes, { signal }) => {
const response = await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes),
signal,
});
if (!response.ok) throw Error('저장 실패');
message.value = '저장 순서: ' + JSON.stringify(changes.order);
return response.json();
}),
);
button('서버 재조회', () => grid.setSearch(''));
message.value =
'rowReorder:true는 별도 컬럼 없이 첫 번째 보이는 데이터 컬럼의 셀 앞에 순서 손잡이를 표시합니다. rowDragField로 대상 컬럼 field를 지정할 수 있으며 예제는 name을 지정합니다. 지정 컬럼을 숨기면 손잡이도 숨겨집니다. 드래그 삽입선 또는 손잡이에서 Alt+위/아래로 행을 이동합니다. getRows는 바뀐 순서를 반환하고 순서 변경 시 getChangedRows·save의 order에 적재된 행 키 전체가 순서대로 포함됩니다. 배열의 순서는 DB에 자동 저장되지 않으므로 서버가 order를 저장해야 하며 이 개발 서버 예제는 실제 반영 후 재조회합니다. 정렬·검색·필터·트리·그룹·고정행·저장 중에는 이동을 막으며 미조회 행은 이동 대상이 아닙니다. Undo/Redo·원복·저장 실패 정책에도 순서를 반영합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>행 드래그 · 순서 저장</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 로컬 rows는 빈 배열이며 서버에서100행을 조회합니다.
export const rows = [];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
변경 셀 강조와 insert/update/delete 목록을 확인합니다. 기준값으로 복원하면 해당 셀의 변경 표시가 사라집니다.
생성 후 변경 목록을 확인하거나 기준 데이터로 원복합니다. 생성 후 grid.getChangedRows(); grid.revertChanges()를 호출합니다.
행 데이터: 최소 100행 · dirty.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/dirty.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>변경 셀 · 상태 표시</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>변경 셀 · 상태 표시</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="dirty.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './dirty.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('첫 셀 변경', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]),
);
button('변경 목록', () => grid.getChangedRows());
button('기준값 복원', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 2 }]),
);
report.textContent =
'변경 셀 강조와 insert/update/delete 목록을 확인합니다. 기준값으로 복원하면 해당 셀의 변경 표시가 사라집니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './dirty.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('첫 셀 변경', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]),
);
button('변경 목록', () => grid.getChangedRows());
button('기준값 복원', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 2 }]),
);
message.value =
'변경 셀 강조와 insert/update/delete 목록을 확인합니다. 기준값으로 복원하면 해당 셀의 변경 표시가 사라집니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>변경 셀 · 상태 표시</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
트랜잭션 단위로 되돌리고 재실행합니다. Ctrl+Z/Ctrl+Y도 지원합니다.
생성 후 기록된 변경 이력을 취소하거나 다시 적용합니다. 생성 후 grid.undo(); grid.redo()를 호출합니다.
행 데이터: 최소 100행 · undo.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/undo.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Undo · Redo</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>Undo · Redo</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="undo.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './undo.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('수량 수정', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]),
);
button('Undo', () => grid.undo());
button('Redo', () => grid.redo());
report.textContent =
'트랜잭션 단위로 되돌리고 재실행합니다. Ctrl+Z/Ctrl+Y도 지원합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './undo.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('수량 수정', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]),
);
button('Undo', () => grid.undo());
button('Redo', () => grid.redo());
message.value =
'트랜잭션 단위로 되돌리고 재실행합니다. Ctrl+Z/Ctrl+Y도 지원합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>Undo · Redo</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
저장 실패 · optimistic · rollback 예제 독립 실행 ↗
저장 실패는 keep 또는 rollback 정책으로 처리합니다. 성공 응답에만 변경 상태를 확정합니다.
이 예제의 필수값 grid.save(saveFn)의 saveFn 필수값 — 실제 저장 함수입니다. 저장 성공 시 {success:true}를 반환해야 합니다. failure·optimistic은 기본값이 있어 선택입니다.실제 저장 함수와 실패 정책을 save 호출에 전달합니다. 생성 후 grid.save(saveHandler, {optimistic:true, failure:'rollback'})를 호출합니다.
행 데이터: 최소 100행 · save-policy.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/save-policy.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>저장 실패 · optimistic · rollback</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>저장 실패 · optimistic · rollback</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="save-policy.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './save-policy.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('수량 변경', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]),
);
const fail = async () => ({ success: false, message: '저장 실패 예제' });
button('실패 후 유지', () =>
grid.save(fail, { optimistic: true, failure: 'keep' }),
);
button('실패 후 rollback', () =>
grid.save(fail, { optimistic: true, failure: 'rollback' }),
);
button('성공 처리', () => grid.save(async () => ({ success: true })));
report.textContent =
'저장 실패는 keep 또는 rollback 정책으로 처리합니다. 성공 응답에만 변경 상태를 확정합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './save-policy.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('수량 변경', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]),
);
const fail = async () => ({ success: false, message: '저장 실패 예제' });
button('실패 후 유지', () =>
grid.save(fail, { optimistic: true, failure: 'keep' }),
);
button('실패 후 rollback', () =>
grid.save(fail, { optimistic: true, failure: 'rollback' }),
);
button('성공 처리', () => grid.save(async () => ({ success: true })));
message.value =
'저장 실패는 keep 또는 rollback 정책으로 처리합니다. 성공 응답에만 변경 상태를 확정합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>저장 실패 · optimistic · rollback</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
setSearch는 표시 행을 좁히며 openSearch는 표시 중인 셀의 검색 결과를 이동합니다.
생성 후 통합 검색어를 적용합니다. 빈 문자열로 검색을 해제합니다. 생성 후 grid.setSearch('서울')를 호출합니다.
행 데이터: 최소 100행 · search.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/search.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>통합 검색 · 결과 이동</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>통합 검색 · 결과 이동</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="search.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './search.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
const input = document.createElement('input');
input.placeholder = '통합 검색';
input.setAttribute('aria-label', '통합 검색');
toolbar.append(input);
button('검색 적용', () => grid.setSearch(input.value));
button('검색 해제', () => grid.setSearch(''));
button('셀 결과 이동', () => grid.openSearch());
report.textContent =
'setSearch는 표시 행을 좁히며 openSearch는 표시 중인 셀의 검색 결과를 이동합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './search.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
const searchText = ref('');
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('검색 적용', () => grid.setSearch(searchText.value));
button('검색 해제', () => grid.setSearch(''));
button('셀 결과 이동', () => grid.openSearch());
message.value =
'setSearch는 표시 행을 좁히며 openSearch는 표시 중인 셀의 검색 결과를 이동합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>통합 검색 · 결과 이동</h1>
<div class="toolbar">
<input
v-model="searchText"
placeholder="통합 검색"
aria-label="통합 검색"
/>
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
숫자 조건에는 숫자 값을 전달합니다. GUI는 평면 AND/OR이며 API는 중첩 rules를 지원합니다.
생성 후 컬럼 필터를 적용합니다. 조건 트리는 setFilter로 설정합니다. 생성 후 grid.setFilters([{field:'qty',op:'gte',value:10}])를 호출합니다.
행 데이터: 최소 100행 · filter.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/filter.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>컬럼 필터 · AND/OR 조건</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>컬럼 필터 · AND/OR 조건</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="filter.js"></script>
</body>
</html>
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './filter.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('수량 3 이상', () =>
grid.setFilter({ op: 'and', rules: [{ field: 'qty', op: 'gte', value: 3 }] }),
);
button('서울 또는 수량 3 이상', () =>
grid.setFilter({
op: 'or',
rules: [
{ field: 'name', op: 'contains', value: '서울' },
{ field: 'qty', op: 'gte', value: 3 },
],
}),
);
button('조건 빌더', () => grid.openFilterBuilder());
button('조건 해제', () => grid.setFilter(null));
report.textContent =
'숫자 조건에는 숫자 값을 전달합니다. GUI는 평면 AND/OR이며 API는 중첩 rules를 지원합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './filter.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('수량 3 이상', () =>
grid.setFilter({
op: 'and',
rules: [{ field: 'qty', op: 'gte', value: 3 }],
}),
);
button('서울 또는 수량 3 이상', () =>
grid.setFilter({
op: 'or',
rules: [
{ field: 'name', op: 'contains', value: '서울' },
{ field: 'qty', op: 'gte', value: 3 },
],
}),
);
button('조건 빌더', () => grid.openFilterBuilder());
button('조건 해제', () => grid.setFilter(null));
message.value =
'숫자 조건에는 숫자 값을 전달합니다. GUI는 평면 AND/OR이며 API는 중첩 rules를 지원합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>컬럼 필터 · AND/OR 조건</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
수량이나 단가를 편집하면 금액을 다시 계산합니다. 계산 컬럼은 읽기 전용입니다.
계산 컬럼의 dependsOn과 calculate를 함께 설정합니다. 이 예제의 네 번째 컬럼은 금액입니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[3].calculate = row => row.qty * row.price처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · computed.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/computed.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>계산 컬럼</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>계산 컬럼</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="computed.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './computed.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{
field: 'amount',
title: '금액',
type: 'number',
dependsOn: ['qty', 'price'],
calculate: (r) => r.qty * r.price,
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'수량이나 단가를 편집하면 금액을 다시 계산합니다. 계산 컬럼은 읽기 전용입니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './computed.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{
field: 'amount',
title: '금액',
type: 'number',
dependsOn: ['qty', 'price'],
calculate: (r) => r.qty * r.price,
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'수량이나 단가를 편집하면 금액을 다시 계산합니다. 계산 컬럼은 읽기 전용입니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>계산 컬럼</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
all·filtered·selected 집계 범위와 sum·avg·min·max 집계 종류를 구성합니다.
집계 필드·종류·범위와 표시 이름을 지정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.summary = [{field:'qty',type:'sum',scope:'all',label:'전체 수량'}]처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · summary.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/summary.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>전체 · 필터 · 선택 Summary</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>전체 · 필터 · 선택 Summary</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="summary.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './summary.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
summary: [
{ field: 'qty', type: 'sum', scope: 'all', label: '전체 수량' },
{ field: 'price', type: 'avg', scope: 'filtered', label: '표시 평균' },
{ field: 'price', type: 'max', scope: 'selected', label: '선택 최대' },
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('서울만 보기', () => grid.setSearch('서울'));
button('검색 해제', () => grid.setSearch(''));
report.textContent =
'all·filtered·selected 집계 범위와 sum·avg·min·max 집계 종류를 구성합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './summary.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
summary: [
{ field: 'qty', type: 'sum', scope: 'all', label: '전체 수량' },
{ field: 'price', type: 'avg', scope: 'filtered', label: '표시 평균' },
{ field: 'price', type: 'max', scope: 'selected', label: '선택 최대' },
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('서울만 보기', () => grid.setSearch('서울'));
button('검색 해제', () => grid.setSearch(''));
message.value =
'all·filtered·selected 집계 범위와 sum·avg·min·max 집계 종류를 구성합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>전체 · 필터 · 선택 Summary</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
type:image는 URL로 이미지를 표시합니다. 이미지가 없거나 최종 로딩 실패 시 기본 대체 표시는 빈 상태이며 imagePlaceholder로 원하는 문구를 지정합니다. 크기·대체 문구·로딩 실패 시 대체 이미지를 설정합니다. imageText와 imageTextPosition(left/right/top/bottom)으로 같은 셀의 텍스트 위치를 지정합니다. imageText는 문자열 또는 (value,row,grid) 함수이며 {left,right,top,bottom} 객체를 반환하면 여러 방향에 동시에 표시합니다. 위치 선택으로 상품 셀의 배치를 바꿔보세요. 텍스트는 HTML로 해석하지 않고 이미지 로딩 실패 후에도 유지합니다. type:button 또는 기존 editor:button으로 버튼을 표시하고 onClick(row,grid,event)으로 동작을 연결합니다. buttonTextBefore/buttonTextAfter로 버튼 좌우에 텍스트를 넣고, 문자열 또는 (value,row,grid) 함수로 지정합니다. buttonVisible:false 또는 (row,grid)=>boolean으로 버튼만 숨길 수 있습니다. 이 예제는5의 배수 행에서 버튼을 숨기며 좌우 텍스트는 유지합니다. 좌우 텍스트 클릭은 셀 선택이며 버튼 동작을 실행하지 않습니다. 키보드 Enter/Space로도 버튼을 실행할 수 있습니다.
이미지 컬럼은 imageSrc/imageWidth/imageHeight와 imageText/imageTextPosition으로 텍스트를 함께 배치하고, 버튼 컬럼은 buttonLabel/buttonTextBefore/buttonTextAfter/onClick을 설정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].type = 'image'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · cell-content.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/cell-content.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>이미지 · 텍스트 · 버튼 컬럼</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>이미지 · 텍스트 · 버튼 컬럼</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="cell-content.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './cell-content.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'photo',
title: '상품 이미지',
type: 'image',
imageWidth: 48,
imageHeight: 48,
imageFit: 'contain',
imageAlt: (value, row) => row.name,
imageFallback: '/examples/assets/placeholder.svg',
imagePlaceholder: '이미지 없음',
editable: false,
width: 140,
align: 'center',
},
{
field: 'name',
title: '텍스트 + 이미지',
type: 'image',
imageSrc: (v, row) => row.photo,
imageAlt: '',
imageText: (value) => value,
imageTextPosition: 'right',
imageWidth: 32,
imageHeight: 32,
width: 280,
},
{
field: 'action',
title: '작업',
type: 'button',
buttonLabel: (value, row) => (row.id === 1 ? '상품 조회' : '상세 보기'),
buttonTextBefore: (value, row) => '#' + row.id,
buttonTextAfter: (value, row) =>
row.id % 4 === 2 ? '사용 불가' : '상품 확인',
buttonVisible: (row) => row.id % 5 !== 0,
buttonDisabled: (row) => row.id % 4 === 2,
onClick: (row, grid, event) => {
report.textContent =
'선택한 상품: ' + row.name + ' (#' + row.id + ') · ' + event.type;
},
editable: false,
width: 300,
},
{
field: 'composite',
title: '상하좌우 함께 배치',
type: 'image',
imageSrc: (v, row) => row.photo,
imageAlt: '',
imageWidth: 32,
imageHeight: 32,
imageText: (v, row) => ({
left: '#' + row.id,
right: row.name,
top: '상품 정보',
bottom: '재고 확인',
}),
width: 300,
editable: false,
},
],
plugins: [],
rowHeight: 112,
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
select('텍스트 위치', ['오른쪽', '왼쪽', '위', '아래'], (value) => {
grid.columns.find((column) => column.field === 'name').imageTextPosition = {
오른쪽: 'right',
왼쪽: 'left',
위: 'top',
아래: 'bottom',
}[value];
grid.refresh();
});
report.textContent =
'type:image는 URL로 이미지를 표시합니다. 이미지가 없거나 최종 로딩 실패 시 기본 대체 표시는 빈 상태이며 imagePlaceholder로 원하는 문구를 지정합니다. 크기·대체 문구·로딩 실패 시 대체 이미지를 설정합니다. imageText와 imageTextPosition(left/right/top/bottom)으로 같은 셀의 텍스트 위치를 지정합니다. imageText는 문자열 또는 (value,row,grid) 함수이며 {left,right,top,bottom} 객체를 반환하면 여러 방향에 동시에 표시합니다. 위치 선택으로 상품 셀의 배치를 바꿔보세요. 텍스트는 HTML로 해석하지 않고 이미지 로딩 실패 후에도 유지합니다. type:button 또는 기존 editor:button으로 버튼을 표시하고 onClick(row,grid,event)으로 동작을 연결합니다. buttonTextBefore/buttonTextAfter로 버튼 좌우에 텍스트를 넣고, 문자열 또는 (value,row,grid) 함수로 지정합니다. buttonVisible:false 또는 (row,grid)=>boolean으로 버튼만 숨길 수 있습니다. 이 예제는5의 배수 행에서 버튼을 숨기며 좌우 텍스트는 유지합니다. 좌우 텍스트 클릭은 셀 선택이며 버튼 동작을 실행하지 않습니다. 키보드 Enter/Space로도 버튼을 실행할 수 있습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './cell-content.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'photo',
title: '상품 이미지',
type: 'image',
imageWidth: 48,
imageHeight: 48,
imageFit: 'contain',
imageAlt: (value, row) => row.name,
imageFallback: '/examples/assets/placeholder.svg',
imagePlaceholder: '이미지 없음',
editable: false,
width: 140,
align: 'center',
},
{
field: 'name',
title: '텍스트 + 이미지',
type: 'image',
imageSrc: (v, row) => row.photo,
imageAlt: '',
imageText: (value) => value,
imageTextPosition: 'right',
imageWidth: 32,
imageHeight: 32,
width: 280,
},
{
field: 'action',
title: '작업',
type: 'button',
buttonLabel: (value, row) => (row.id === 1 ? '상품 조회' : '상세 보기'),
buttonTextBefore: (value, row) => '#' + row.id,
buttonTextAfter: (value, row) =>
row.id % 4 === 2 ? '사용 불가' : '상품 확인',
buttonVisible: (row) => row.id % 5 !== 0,
buttonDisabled: (row) => row.id % 4 === 2,
onClick: (row, grid, event) => {
message.value =
'선택한 상품: ' + row.name + ' (#' + row.id + ') · ' + event.type;
},
editable: false,
width: 300,
},
{
field: 'composite',
title: '상하좌우 함께 배치',
type: 'image',
imageSrc: (v, row) => row.photo,
imageAlt: '',
imageWidth: 32,
imageHeight: 32,
imageText: (v, row) => ({
left: '#' + row.id,
right: row.name,
top: '상품 정보',
bottom: '재고 확인',
}),
width: 300,
editable: false,
},
],
plugins: [],
rowHeight: 112,
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
select('텍스트 위치', ['오른쪽', '왼쪽', '위', '아래'], (value) => {
grid.columns.find((column) => column.field === 'name').imageTextPosition = {
오른쪽: 'right',
왼쪽: 'left',
위: 'top',
아래: 'bottom',
}[value];
grid.refresh();
});
message.value =
'type:image는 URL로 이미지를 표시합니다. 이미지가 없거나 최종 로딩 실패 시 기본 대체 표시는 빈 상태이며 imagePlaceholder로 원하는 문구를 지정합니다. 크기·대체 문구·로딩 실패 시 대체 이미지를 설정합니다. imageText와 imageTextPosition(left/right/top/bottom)으로 같은 셀의 텍스트 위치를 지정합니다. imageText는 문자열 또는 (value,row,grid) 함수이며 {left,right,top,bottom} 객체를 반환하면 여러 방향에 동시에 표시합니다. 위치 선택으로 상품 셀의 배치를 바꿔보세요. 텍스트는 HTML로 해석하지 않고 이미지 로딩 실패 후에도 유지합니다. type:button 또는 기존 editor:button으로 버튼을 표시하고 onClick(row,grid,event)으로 동작을 연결합니다. buttonTextBefore/buttonTextAfter로 버튼 좌우에 텍스트를 넣고, 문자열 또는 (value,row,grid) 함수로 지정합니다. buttonVisible:false 또는 (row,grid)=>boolean으로 버튼만 숨길 수 있습니다. 이 예제는5의 배수 행에서 버튼을 숨기며 좌우 텍스트는 유지합니다. 좌우 텍스트 클릭은 셀 선택이며 버튼 동작을 실행하지 않습니다. 키보드 Enter/Space로도 버튼을 실행할 수 있습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>이미지 · 텍스트 · 버튼 컬럼</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '노트북',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 2,
name: '키보드',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 3,
name: '모니터',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 4,
name: '사진 준비 중',
photo: '',
action: '상세',
},
{
id: 5,
name: '노트북 5',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 6,
name: '키보드 6',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 7,
name: '모니터 7',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 8,
name: '사진 준비 중 8',
photo: '',
action: '상세',
},
{
id: 9,
name: '노트북 9',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 10,
name: '키보드 10',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 11,
name: '모니터 11',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 12,
name: '사진 준비 중 12',
photo: '',
action: '상세',
},
{
id: 13,
name: '노트북 13',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 14,
name: '키보드 14',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 15,
name: '모니터 15',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 16,
name: '사진 준비 중 16',
photo: '',
action: '상세',
},
{
id: 17,
name: '노트북 17',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 18,
name: '키보드 18',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 19,
name: '모니터 19',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 20,
name: '사진 준비 중 20',
photo: '',
action: '상세',
},
{
id: 21,
name: '노트북 21',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 22,
name: '키보드 22',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 23,
name: '모니터 23',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 24,
name: '사진 준비 중 24',
photo: '',
action: '상세',
},
{
id: 25,
name: '노트북 25',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 26,
name: '키보드 26',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 27,
name: '모니터 27',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 28,
name: '사진 준비 중 28',
photo: '',
action: '상세',
},
{
id: 29,
name: '노트북 29',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 30,
name: '키보드 30',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 31,
name: '모니터 31',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 32,
name: '사진 준비 중 32',
photo: '',
action: '상세',
},
{
id: 33,
name: '노트북 33',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 34,
name: '키보드 34',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 35,
name: '모니터 35',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 36,
name: '사진 준비 중 36',
photo: '',
action: '상세',
},
{
id: 37,
name: '노트북 37',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 38,
name: '키보드 38',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 39,
name: '모니터 39',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 40,
name: '사진 준비 중 40',
photo: '',
action: '상세',
},
{
id: 41,
name: '노트북 41',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 42,
name: '키보드 42',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 43,
name: '모니터 43',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 44,
name: '사진 준비 중 44',
photo: '',
action: '상세',
},
{
id: 45,
name: '노트북 45',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 46,
name: '키보드 46',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 47,
name: '모니터 47',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 48,
name: '사진 준비 중 48',
photo: '',
action: '상세',
},
{
id: 49,
name: '노트북 49',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 50,
name: '키보드 50',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 51,
name: '모니터 51',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 52,
name: '사진 준비 중 52',
photo: '',
action: '상세',
},
{
id: 53,
name: '노트북 53',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 54,
name: '키보드 54',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 55,
name: '모니터 55',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 56,
name: '사진 준비 중 56',
photo: '',
action: '상세',
},
{
id: 57,
name: '노트북 57',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 58,
name: '키보드 58',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 59,
name: '모니터 59',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 60,
name: '사진 준비 중 60',
photo: '',
action: '상세',
},
{
id: 61,
name: '노트북 61',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 62,
name: '키보드 62',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 63,
name: '모니터 63',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 64,
name: '사진 준비 중 64',
photo: '',
action: '상세',
},
{
id: 65,
name: '노트북 65',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 66,
name: '키보드 66',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 67,
name: '모니터 67',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 68,
name: '사진 준비 중 68',
photo: '',
action: '상세',
},
{
id: 69,
name: '노트북 69',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 70,
name: '키보드 70',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 71,
name: '모니터 71',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 72,
name: '사진 준비 중 72',
photo: '',
action: '상세',
},
{
id: 73,
name: '노트북 73',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 74,
name: '키보드 74',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 75,
name: '모니터 75',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 76,
name: '사진 준비 중 76',
photo: '',
action: '상세',
},
{
id: 77,
name: '노트북 77',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 78,
name: '키보드 78',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 79,
name: '모니터 79',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 80,
name: '사진 준비 중 80',
photo: '',
action: '상세',
},
{
id: 81,
name: '노트북 81',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 82,
name: '키보드 82',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 83,
name: '모니터 83',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 84,
name: '사진 준비 중 84',
photo: '',
action: '상세',
},
{
id: 85,
name: '노트북 85',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 86,
name: '키보드 86',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 87,
name: '모니터 87',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 88,
name: '사진 준비 중 88',
photo: '',
action: '상세',
},
{
id: 89,
name: '노트북 89',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 90,
name: '키보드 90',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 91,
name: '모니터 91',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 92,
name: '사진 준비 중 92',
photo: '',
action: '상세',
},
{
id: 93,
name: '노트북 93',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 94,
name: '키보드 94',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 95,
name: '모니터 95',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 96,
name: '사진 준비 중 96',
photo: '',
action: '상세',
},
{
id: 97,
name: '노트북 97',
photo: '/examples/assets/laptop.svg',
action: '상세',
},
{
id: 98,
name: '키보드 98',
photo: '/examples/assets/keyboard.svg',
action: '상세',
},
{
id: 99,
name: '모니터 99',
photo: '/examples/assets/monitor.svg',
action: '상세',
},
{
id: 100,
name: '사진 준비 중 100',
photo: '',
action: '상세',
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
행·셀의 스타일과 클래스를 함수로 지정하고 조건에 따라 아이콘 문자를 표시합니다. 두 번째 행의 rowStyle.backgroundColor는 줄무늬·hover보다 우선하며 선택 셀의 강조는 유지합니다. background 단축 속성도 지원합니다. 셀별 cellStyle 배경과 행번호의 전용 배경은 행 배경보다 우선합니다.
컬럼 cellStyle/cellClass/icon과 그리드 rowStyle/rowClass로 조건부 표시를 지정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[2].cellStyle = value => ({color:value >= 2000 ? '#b91c1c' : '#1d4ed8'})처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · style.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/style.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>조건부 스타일 · 아이콘</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>조건부 스타일 · 아이콘</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="style.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './style.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{
field: 'price',
title: '단가',
type: 'number',
parse: Number,
cellStyle: (v) => ({ color: v >= 2000 ? '#b91c1c' : '#1d4ed8' }),
cellClass: (v) => (v >= 2000 ? 'high-price' : ''),
icon: (v) => (v >= 2000 ? '▲' : ''),
},
],
plugins: [],
rowClass: (r) => (r.id === 2 ? 'featured-row' : ''),
rowStyle: (r) => (r.id === 2 ? { backgroundColor: '#eff6ff' } : {}),
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'행·셀의 스타일과 클래스를 함수로 지정하고 조건에 따라 아이콘 문자를 표시합니다. 두 번째 행의 rowStyle.backgroundColor는 줄무늬·hover보다 우선하며 선택 셀의 강조는 유지합니다. background 단축 속성도 지원합니다. 셀별 cellStyle 배경과 행번호의 전용 배경은 행 배경보다 우선합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './style.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{
field: 'price',
title: '단가',
type: 'number',
parse: Number,
cellStyle: (v) => ({ color: v >= 2000 ? '#b91c1c' : '#1d4ed8' }),
cellClass: (v) => (v >= 2000 ? 'high-price' : ''),
icon: (v) => (v >= 2000 ? '▲' : ''),
},
],
plugins: [],
rowClass: (r) => (r.id === 2 ? 'featured-row' : ''),
rowStyle: (r) => (r.id === 2 ? { backgroundColor: '#eff6ff' } : {}),
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'행·셀의 스타일과 클래스를 함수로 지정하고 조건에 따라 아이콘 문자를 표시합니다. 두 번째 행의 rowStyle.backgroundColor는 줄무늬·hover보다 우선하며 선택 셀의 강조는 유지합니다. background 단축 속성도 지원합니다. 셀별 cellStyle 배경과 행번호의 전용 배경은 행 배경보다 우선합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>조건부 스타일 · 아이콘</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
툴팁 기본값은 숨김입니다. 이 예제는 tooltipEnabled:true로 켜며, 셀 위에 마우스를 올리거나 방향키로 이동하면 스타일 툴팁을 표시합니다. 그리드와 컬럼별로 켜거나 끌 수 있고 컬럼 설정이 우선합니다. tooltipDelay는 표시 전 대기(ms), tooltipDuration은 표시 후 유지(ms)입니다. 기본값은 300/3000ms이며 컬럼 옵션이 그리드 옵션보다 우선합니다. 이 예제는 200ms 후 표시하고 2500ms 유지하며 수량 컬럼은 5000ms 유지합니다. duration 0은 자동 닫기 없음, 빈 텍스트는 표시하지 않습니다. 툴팁 위에 마우스를 올리면 자동 닫기를 멈추고, 이탈·Escape·스크롤·편집 시 닫습니다. HTML은 해석하지 않습니다.
이 툴팁 예제에서는 tooltipEnabled:true로 표시를 켜며 tooltipDelay/tooltipDuration으로 시간을 설정합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.columns[0].tooltip = (value, row) => String(value)처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · tooltip.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/tooltip.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>텍스트 Tooltip</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>텍스트 Tooltip</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="tooltip.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './tooltip.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'name',
title: '주문',
width: 240,
tooltip: (value, row) =>
'주문 #' + row.id + '\n' + value + ' · 상세 정보를 확인하세요',
},
{
field: 'qty',
title: '수량',
type: 'number',
parse: Number,
tooltipDuration: 5000,
},
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
tooltipEnabled: true,
tooltipDelay: 200,
tooltipDuration: 2500,
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
select('표시 대기(ms)', ['200', '0', '500', '1000'], (value) => {
grid.options.tooltipDelay = Number(value);
grid.hideTooltip();
});
select('유지 시간(ms)', ['2500', '1000', '5000', '0'], (value) => {
grid.options.tooltipDuration = Number(value);
grid.hideTooltip();
});
report.textContent =
'툴팁 기본값은 숨김입니다. 이 예제는 tooltipEnabled:true로 켜며, 셀 위에 마우스를 올리거나 방향키로 이동하면 스타일 툴팁을 표시합니다. 그리드와 컬럼별로 켜거나 끌 수 있고 컬럼 설정이 우선합니다. tooltipDelay는 표시 전 대기(ms), tooltipDuration은 표시 후 유지(ms)입니다. 기본값은 300/3000ms이며 컬럼 옵션이 그리드 옵션보다 우선합니다. 이 예제는 200ms 후 표시하고 2500ms 유지하며 수량 컬럼은 5000ms 유지합니다. duration 0은 자동 닫기 없음, 빈 텍스트는 표시하지 않습니다. 툴팁 위에 마우스를 올리면 자동 닫기를 멈추고, 이탈·Escape·스크롤·편집 시 닫습니다. HTML은 해석하지 않습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './tooltip.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{
field: 'name',
title: '주문',
width: 240,
tooltip: (value, row) =>
'주문 #' + row.id + '\n' + value + ' · 상세 정보를 확인하세요',
},
{
field: 'qty',
title: '수량',
type: 'number',
parse: Number,
tooltipDuration: 5000,
},
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
tooltipEnabled: true,
tooltipDelay: 200,
tooltipDuration: 2500,
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
select('표시 대기(ms)', ['200', '0', '500', '1000'], (value) => {
grid.options.tooltipDelay = Number(value);
grid.hideTooltip();
});
select('유지 시간(ms)', ['2500', '1000', '5000', '0'], (value) => {
grid.options.tooltipDuration = Number(value);
grid.hideTooltip();
});
message.value =
'툴팁 기본값은 숨김입니다. 이 예제는 tooltipEnabled:true로 켜며, 셀 위에 마우스를 올리거나 방향키로 이동하면 스타일 툴팁을 표시합니다. 그리드와 컬럼별로 켜거나 끌 수 있고 컬럼 설정이 우선합니다. tooltipDelay는 표시 전 대기(ms), tooltipDuration은 표시 후 유지(ms)입니다. 기본값은 300/3000ms이며 컬럼 옵션이 그리드 옵션보다 우선합니다. 이 예제는 200ms 후 표시하고 2500ms 유지하며 수량 컬럼은 5000ms 유지합니다. duration 0은 자동 닫기 없음, 빈 텍스트는 표시하지 않습니다. 툴팁 위에 마우스를 올리면 자동 닫기를 멈추고, 이탈·Escape·스크롤·편집 시 닫습니다. HTML은 해석하지 않습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>텍스트 Tooltip</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
locale과 setLocale은 상태·메뉴·달력·검증 오류 및 숫자 표시 언어를 지정합니다. 빈 상태 문구와 내부 검증 오류를 표시한 뒤 언어를 바꾸면 문구도 바뀝니다. messages로 직접 지정한 문구·업무 데이터·서버/사용자 검증 메시지는 원문을 유지합니다. theme은 표시 테마를 지정합니다.
처음 사용할 언어와 문구를 지정합니다. 생성 후 setLocale/setTheme로 언어와 테마를 바꿉니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.locale = 'ko'; options.messages = {empty:'주문이 없습니다'}처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · locale.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/locale.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>한국어 / 영어 · 테마</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>한국어 / 영어 · 테마</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="locale.js"></script>
</body>
</html>
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './locale.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
locale: 'ko',
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
select('언어', ['ko', 'en'], (v) => grid.setLocale(v));
select('테마', ['light', 'dark', 'compact'], (v) => grid.setTheme(v));
button('빈 상태 문구', () => grid.setRows([]));
button('행 복원', () => grid.setRows(options.rows));
button('내부 검증 오류', () =>
grid.attempt(() => grid.setColumnWidth('missing', 100)),
);
report.textContent =
'locale과 setLocale은 상태·메뉴·달력·검증 오류 및 숫자 표시 언어를 지정합니다. 빈 상태 문구와 내부 검증 오류를 표시한 뒤 언어를 바꾸면 문구도 바뀝니다. messages로 직접 지정한 문구·업무 데이터·서버/사용자 검증 메시지는 원문을 유지합니다. theme은 표시 테마를 지정합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './locale.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
locale: 'ko',
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
select('언어', ['ko', 'en'], (v) => grid.setLocale(v));
select('테마', ['light', 'dark', 'compact'], (v) => grid.setTheme(v));
button('빈 상태 문구', () => grid.setRows([]));
button('행 복원', () => grid.setRows(options.rows));
button('내부 검증 오류', () =>
grid.attempt(() => grid.setColumnWidth('missing', 100)),
);
message.value =
'locale과 setLocale은 상태·메뉴·달력·검증 오류 및 숫자 표시 언어를 지정합니다. 빈 상태 문구와 내부 검증 오류를 표시한 뒤 언어를 바꾸면 문구도 바뀝니다. messages로 직접 지정한 문구·업무 데이터·서버/사용자 검증 메시지는 원문을 유지합니다. theme은 표시 테마를 지정합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>한국어 / 영어 · 테마</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
분류→지역별로 묶고 각 그룹 합계를 표시합니다. +/−로 접고 펼칩니다.
그룹 필드와 집계는 groupingPlugin의 fields/aggregates에 지정합니다. 함께 사용할 다른 플러그인도 배열에 포함하세요. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.plugins = [groupingPlugin({fields:['category','region']})]처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · group.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/group.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>다단 그룹 · 그룹 집계</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>다단 그룹 · 그룹 집계</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="group.js"></script>
</body>
</html>
import { ZixoGrid, groupingPlugin } from '../../src/index.js';
import { rows } from './group.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'category', title: '분류' },
{ field: 'region', title: '지역' },
{ field: 'name', title: '주문' },
{ field: 'qty', title: '수량' },
],
plugins: [
groupingPlugin({
fields: ['category', 'region'],
aggregates: [{ field: 'qty', type: 'sum', label: '수량 합계' }],
}),
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'분류→지역별로 묶고 각 그룹 합계를 표시합니다. +/−로 접고 펼칩니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, groupingPlugin } from '../../src/index.js';
import { rows } from './group.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'category', title: '분류' },
{ field: 'region', title: '지역' },
{ field: 'name', title: '주문' },
{ field: 'qty', title: '수량' },
],
plugins: [
groupingPlugin({
fields: ['category', 'region'],
aggregates: [{ field: 'qty', type: 'sum', label: '수량 합계' }],
}),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'분류→지역별로 묶고 각 그룹 합계를 표시합니다. +/−로 접고 펼칩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>다단 그룹 · 그룹 집계</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '주문 A',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 2,
name: '주문 B',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 3,
name: '주문 A 3',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 4,
name: '주문 B 4',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 5,
name: '주문 A 5',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 6,
name: '주문 B 6',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 7,
name: '주문 A 7',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 8,
name: '주문 B 8',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 9,
name: '주문 A 9',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 10,
name: '주문 B 10',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 11,
name: '주문 A 11',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 12,
name: '주문 B 12',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 13,
name: '주문 A 13',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 14,
name: '주문 B 14',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 15,
name: '주문 A 15',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 16,
name: '주문 B 16',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 17,
name: '주문 A 17',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 18,
name: '주문 B 18',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 19,
name: '주문 A 19',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 20,
name: '주문 B 20',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 21,
name: '주문 A 21',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 22,
name: '주문 B 22',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 23,
name: '주문 A 23',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 24,
name: '주문 B 24',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 25,
name: '주문 A 25',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 26,
name: '주문 B 26',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 27,
name: '주문 A 27',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 28,
name: '주문 B 28',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 29,
name: '주문 A 29',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 30,
name: '주문 B 30',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 31,
name: '주문 A 31',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 32,
name: '주문 B 32',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 33,
name: '주문 A 33',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 34,
name: '주문 B 34',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 35,
name: '주문 A 35',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 36,
name: '주문 B 36',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 37,
name: '주문 A 37',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 38,
name: '주문 B 38',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 39,
name: '주문 A 39',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 40,
name: '주문 B 40',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 41,
name: '주문 A 41',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 42,
name: '주문 B 42',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 43,
name: '주문 A 43',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 44,
name: '주문 B 44',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 45,
name: '주문 A 45',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 46,
name: '주문 B 46',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 47,
name: '주문 A 47',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 48,
name: '주문 B 48',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 49,
name: '주문 A 49',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 50,
name: '주문 B 50',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 51,
name: '주문 A 51',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 52,
name: '주문 B 52',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 53,
name: '주문 A 53',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 54,
name: '주문 B 54',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 55,
name: '주문 A 55',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 56,
name: '주문 B 56',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 57,
name: '주문 A 57',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 58,
name: '주문 B 58',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 59,
name: '주문 A 59',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 60,
name: '주문 B 60',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 61,
name: '주문 A 61',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 62,
name: '주문 B 62',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 63,
name: '주문 A 63',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 64,
name: '주문 B 64',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 65,
name: '주문 A 65',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 66,
name: '주문 B 66',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 67,
name: '주문 A 67',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 68,
name: '주문 B 68',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 69,
name: '주문 A 69',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 70,
name: '주문 B 70',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 71,
name: '주문 A 71',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 72,
name: '주문 B 72',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 73,
name: '주문 A 73',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 74,
name: '주문 B 74',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 75,
name: '주문 A 75',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 76,
name: '주문 B 76',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 77,
name: '주문 A 77',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 78,
name: '주문 B 78',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 79,
name: '주문 A 79',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 80,
name: '주문 B 80',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 81,
name: '주문 A 81',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 82,
name: '주문 B 82',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 83,
name: '주문 A 83',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 84,
name: '주문 B 84',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 85,
name: '주문 A 85',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 86,
name: '주문 B 86',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 87,
name: '주문 A 87',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 88,
name: '주문 B 88',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 89,
name: '주문 A 89',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 90,
name: '주문 B 90',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 91,
name: '주문 A 91',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 92,
name: '주문 B 92',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 93,
name: '주문 A 93',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 94,
name: '주문 B 94',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 95,
name: '주문 A 95',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 96,
name: '주문 B 96',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 97,
name: '주문 A 97',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 98,
name: '주문 B 98',
qty: 3,
category: '제품',
region: '부산',
},
{
id: 99,
name: '주문 A 99',
qty: 2,
category: '제품',
region: '서울',
},
{
id: 100,
name: '주문 B 100',
qty: 3,
category: '제품',
region: '부산',
},
];
100행을 5단계 계층으로 구성합니다. defaultExpanded: true는 처음 펼침, false는 처음 접힘이며 기본값은 false입니다. 5단계 검색 시 1~4단계 조상을 유지합니다. parentId로 부모 행의 키를 참조합니다.
트리 필드와 처음 펼침 여부는 treePlugin 설정에 지정합니다. 함께 사용할 다른 플러그인도 배열에 포함하세요. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.plugins = [treePlugin({field:'name',defaultExpanded:true})]처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
초기 펼침 옵션 treePlugin 설정 또는 그리드 tree 설정에 defaultExpanded를 지정합니다. true는 적재된 부모 전체 펼침, false는 접힘이며 기본값은 false입니다. expanded 배열이 있으면 그 배열을 우선 사용합니다. expanded: []는 모두 접힘입니다. 미조회 lazy 자식은 자동 조회하지 않습니다.
treePlugin({ field: 'name', defaultExpanded: true }); // 처음 펼침
treePlugin({ field: 'name', defaultExpanded: false }); // 처음 접힘 (기본값)
treePlugin({ field: 'name', expanded: [1, 2] }); // 지정한 부모만 펼침예제의 ‘처음 표시’를 바꾸면 새 옵션으로 그리드를 다시 생성합니다. 예제에서 편집·선택한 상태는 초기화됩니다.
행 데이터: 최소 100행 · tree.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/tree.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>트리 · 연결선 · 조상 필터</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>트리 · 연결선 · 조상 필터</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="tree.js"></script>
</body>
</html>
import { ZixoGrid, treePlugin } from '../../src/index.js';
import { rows } from './tree.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '업무', width: 360 }],
plugins: [
treePlugin({ field: 'name', showLines: true, defaultExpanded: true }),
],
};
let grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
select('처음 표시', ['펼침', '접힘'], (value) => {
grid.destroy();
grid = new ZixoGrid(host, {
...options,
plugins: [
treePlugin({
field: 'name',
showLines: true,
defaultExpanded: value === '펼침',
}),
],
});
});
button('연결선 켜기', () => grid.setTreeLines(true));
button('연결선 끄기', () => grid.setTreeLines(false));
button('5단계 검색', () => grid.setSearch('5단계'));
button('검색 해제', () => grid.setSearch(''));
report.textContent =
'100행을 5단계 계층으로 구성합니다. defaultExpanded: true는 처음 펼침, false는 처음 접힘이며 기본값은 false입니다. 5단계 검색 시 1~4단계 조상을 유지합니다. parentId로 부모 행의 키를 참조합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, treePlugin } from '../../src/index.js';
import { rows } from './tree.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '업무', width: 360 }],
plugins: [
treePlugin({ field: 'name', showLines: true, defaultExpanded: true }),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
select('처음 표시', ['펼침', '접힘'], (value) => {
grid.destroy();
grid = new ZixoGrid(gridHost.value, {
...options,
plugins: [
treePlugin({
field: 'name',
showLines: true,
defaultExpanded: value === '펼침',
}),
],
});
});
button('연결선 켜기', () => grid.setTreeLines(true));
button('연결선 끄기', () => grid.setTreeLines(false));
button('5단계 검색', () => grid.setSearch('5단계'));
button('검색 해제', () => grid.setSearch(''));
message.value =
'100행을 5단계 계층으로 구성합니다. defaultExpanded: true는 처음 펼침, false는 처음 접힘이며 기본값은 false입니다. 5단계 검색 시 1~4단계 조상을 유지합니다. parentId로 부모 행의 키를 참조합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>트리 · 연결선 · 조상 필터</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
parentId: null,
name: '1단계 · 프로젝트',
},
{
id: 2,
parentId: 1,
name: '2단계 · 업무 영역',
},
{
id: 3,
parentId: 2,
name: '3단계 · 세부 업무',
},
{
id: 4,
parentId: 3,
name: '4단계 · 실행 항목',
},
{
id: 5,
parentId: 4,
name: '5단계 · 검증 항목',
},
{
id: 6,
parentId: null,
name: '1단계 · 프로젝트 6',
},
{
id: 7,
parentId: 6,
name: '2단계 · 업무 영역 7',
},
{
id: 8,
parentId: 7,
name: '3단계 · 세부 업무 8',
},
{
id: 9,
parentId: 8,
name: '4단계 · 실행 항목 9',
},
{
id: 10,
parentId: 9,
name: '5단계 · 검증 항목 10',
},
{
id: 11,
parentId: null,
name: '1단계 · 프로젝트 11',
},
{
id: 12,
parentId: 11,
name: '2단계 · 업무 영역 12',
},
{
id: 13,
parentId: 12,
name: '3단계 · 세부 업무 13',
},
{
id: 14,
parentId: 13,
name: '4단계 · 실행 항목 14',
},
{
id: 15,
parentId: 14,
name: '5단계 · 검증 항목 15',
},
{
id: 16,
parentId: null,
name: '1단계 · 프로젝트 16',
},
{
id: 17,
parentId: 16,
name: '2단계 · 업무 영역 17',
},
{
id: 18,
parentId: 17,
name: '3단계 · 세부 업무 18',
},
{
id: 19,
parentId: 18,
name: '4단계 · 실행 항목 19',
},
{
id: 20,
parentId: 19,
name: '5단계 · 검증 항목 20',
},
{
id: 21,
parentId: null,
name: '1단계 · 프로젝트 21',
},
{
id: 22,
parentId: 21,
name: '2단계 · 업무 영역 22',
},
{
id: 23,
parentId: 22,
name: '3단계 · 세부 업무 23',
},
{
id: 24,
parentId: 23,
name: '4단계 · 실행 항목 24',
},
{
id: 25,
parentId: 24,
name: '5단계 · 검증 항목 25',
},
{
id: 26,
parentId: null,
name: '1단계 · 프로젝트 26',
},
{
id: 27,
parentId: 26,
name: '2단계 · 업무 영역 27',
},
{
id: 28,
parentId: 27,
name: '3단계 · 세부 업무 28',
},
{
id: 29,
parentId: 28,
name: '4단계 · 실행 항목 29',
},
{
id: 30,
parentId: 29,
name: '5단계 · 검증 항목 30',
},
{
id: 31,
parentId: null,
name: '1단계 · 프로젝트 31',
},
{
id: 32,
parentId: 31,
name: '2단계 · 업무 영역 32',
},
{
id: 33,
parentId: 32,
name: '3단계 · 세부 업무 33',
},
{
id: 34,
parentId: 33,
name: '4단계 · 실행 항목 34',
},
{
id: 35,
parentId: 34,
name: '5단계 · 검증 항목 35',
},
{
id: 36,
parentId: null,
name: '1단계 · 프로젝트 36',
},
{
id: 37,
parentId: 36,
name: '2단계 · 업무 영역 37',
},
{
id: 38,
parentId: 37,
name: '3단계 · 세부 업무 38',
},
{
id: 39,
parentId: 38,
name: '4단계 · 실행 항목 39',
},
{
id: 40,
parentId: 39,
name: '5단계 · 검증 항목 40',
},
{
id: 41,
parentId: null,
name: '1단계 · 프로젝트 41',
},
{
id: 42,
parentId: 41,
name: '2단계 · 업무 영역 42',
},
{
id: 43,
parentId: 42,
name: '3단계 · 세부 업무 43',
},
{
id: 44,
parentId: 43,
name: '4단계 · 실행 항목 44',
},
{
id: 45,
parentId: 44,
name: '5단계 · 검증 항목 45',
},
{
id: 46,
parentId: null,
name: '1단계 · 프로젝트 46',
},
{
id: 47,
parentId: 46,
name: '2단계 · 업무 영역 47',
},
{
id: 48,
parentId: 47,
name: '3단계 · 세부 업무 48',
},
{
id: 49,
parentId: 48,
name: '4단계 · 실행 항목 49',
},
{
id: 50,
parentId: 49,
name: '5단계 · 검증 항목 50',
},
{
id: 51,
parentId: null,
name: '1단계 · 프로젝트 51',
},
{
id: 52,
parentId: 51,
name: '2단계 · 업무 영역 52',
},
{
id: 53,
parentId: 52,
name: '3단계 · 세부 업무 53',
},
{
id: 54,
parentId: 53,
name: '4단계 · 실행 항목 54',
},
{
id: 55,
parentId: 54,
name: '5단계 · 검증 항목 55',
},
{
id: 56,
parentId: null,
name: '1단계 · 프로젝트 56',
},
{
id: 57,
parentId: 56,
name: '2단계 · 업무 영역 57',
},
{
id: 58,
parentId: 57,
name: '3단계 · 세부 업무 58',
},
{
id: 59,
parentId: 58,
name: '4단계 · 실행 항목 59',
},
{
id: 60,
parentId: 59,
name: '5단계 · 검증 항목 60',
},
{
id: 61,
parentId: null,
name: '1단계 · 프로젝트 61',
},
{
id: 62,
parentId: 61,
name: '2단계 · 업무 영역 62',
},
{
id: 63,
parentId: 62,
name: '3단계 · 세부 업무 63',
},
{
id: 64,
parentId: 63,
name: '4단계 · 실행 항목 64',
},
{
id: 65,
parentId: 64,
name: '5단계 · 검증 항목 65',
},
{
id: 66,
parentId: null,
name: '1단계 · 프로젝트 66',
},
{
id: 67,
parentId: 66,
name: '2단계 · 업무 영역 67',
},
{
id: 68,
parentId: 67,
name: '3단계 · 세부 업무 68',
},
{
id: 69,
parentId: 68,
name: '4단계 · 실행 항목 69',
},
{
id: 70,
parentId: 69,
name: '5단계 · 검증 항목 70',
},
{
id: 71,
parentId: null,
name: '1단계 · 프로젝트 71',
},
{
id: 72,
parentId: 71,
name: '2단계 · 업무 영역 72',
},
{
id: 73,
parentId: 72,
name: '3단계 · 세부 업무 73',
},
{
id: 74,
parentId: 73,
name: '4단계 · 실행 항목 74',
},
{
id: 75,
parentId: 74,
name: '5단계 · 검증 항목 75',
},
{
id: 76,
parentId: null,
name: '1단계 · 프로젝트 76',
},
{
id: 77,
parentId: 76,
name: '2단계 · 업무 영역 77',
},
{
id: 78,
parentId: 77,
name: '3단계 · 세부 업무 78',
},
{
id: 79,
parentId: 78,
name: '4단계 · 실행 항목 79',
},
{
id: 80,
parentId: 79,
name: '5단계 · 검증 항목 80',
},
{
id: 81,
parentId: null,
name: '1단계 · 프로젝트 81',
},
{
id: 82,
parentId: 81,
name: '2단계 · 업무 영역 82',
},
{
id: 83,
parentId: 82,
name: '3단계 · 세부 업무 83',
},
{
id: 84,
parentId: 83,
name: '4단계 · 실행 항목 84',
},
{
id: 85,
parentId: 84,
name: '5단계 · 검증 항목 85',
},
{
id: 86,
parentId: null,
name: '1단계 · 프로젝트 86',
},
{
id: 87,
parentId: 86,
name: '2단계 · 업무 영역 87',
},
{
id: 88,
parentId: 87,
name: '3단계 · 세부 업무 88',
},
{
id: 89,
parentId: 88,
name: '4단계 · 실행 항목 89',
},
{
id: 90,
parentId: 89,
name: '5단계 · 검증 항목 90',
},
{
id: 91,
parentId: null,
name: '1단계 · 프로젝트 91',
},
{
id: 92,
parentId: 91,
name: '2단계 · 업무 영역 92',
},
{
id: 93,
parentId: 92,
name: '3단계 · 세부 업무 93',
},
{
id: 94,
parentId: 93,
name: '4단계 · 실행 항목 94',
},
{
id: 95,
parentId: 94,
name: '5단계 · 검증 항목 95',
},
{
id: 96,
parentId: null,
name: '1단계 · 프로젝트 96',
},
{
id: 97,
parentId: 96,
name: '2단계 · 업무 영역 97',
},
{
id: 98,
parentId: 97,
name: '3단계 · 세부 업무 98',
},
{
id: 99,
parentId: 98,
name: '4단계 · 실행 항목 99',
},
{
id: 100,
parentId: 99,
name: '5단계 · 검증 항목 100',
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
상세 버튼으로 하위 그리드를 생성하고 접으면 해제합니다.
이 예제의 필수값 detailPlugin의 field 필수값 — 상세 버튼을 연결할 부모 컬럼 field를 지정합니다. 하위 columns·행 데이터는 원하는 상세 내용에 맞게 제공합니다.상세 컬럼·하위 컬럼·데이터 함수는 detailPlugin에 지정합니다. 함께 사용할 다른 플러그인도 배열에 포함하세요. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.plugins = [detailPlugin({field:'detail',columns:[{field:'name'}],rows:createDetailRows})]처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · detail.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/detail.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Master / Detail</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>Master / Detail</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="detail.js"></script>
</body>
</html>
import { ZixoGrid, detailPlugin } from '../../src/index.js';
import { rows, createDetailRows } from './detail.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{ field: 'detail', title: '상세', editable: false },
],
plugins: [
detailPlugin({
field: 'detail',
columns: [{ field: 'name', title: '하위 주문', width: 300 }],
rows: createDetailRows,
}),
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent = '상세 버튼으로 하위 그리드를 생성하고 접으면 해제합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, detailPlugin } from '../../src/index.js';
import { rows, createDetailRows } from './detail.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{ field: 'detail', title: '상세', editable: false },
],
plugins: [
detailPlugin({
field: 'detail',
columns: [{ field: 'name', title: '하위 주문', width: 300 }],
rows: createDetailRows,
}),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value = '상세 버튼으로 하위 그리드를 생성하고 접으면 해제합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>Master / Detail</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createDetailRows(parent) {
return Array.from({ length: 100 }, (_, i) => ({
id: parent.id * 100 + i + 1,
name: parent.name + ' 상세 ' + (i + 1),
}));
}
처음 펼칠 때 loadChildren으로 자식을 가져옵니다. Detail은 loadRows(row,{signal})로 같은 형태의 조회 계약을 사용합니다. 여러 lazy 요청의 공유 로딩 상태에는 제한이 있습니다.
이 예제의 필수값 loadChildren 필수값 — 이 예제처럼 서버에서 자식을 조회할 때 콜백을 제공합니다. 반환하는 자식 행에도 고유 rowKey와 부모 연결 값이 필요합니다.fetchChildren에는 loadChildren(row,{signal}) 계약의 조회 함수를 전달합니다. 함께 사용할 다른 플러그인도 배열에 포함하세요. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.plugins = [treePlugin({field:'name',loadChildren:fetchChildren})]처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
지연 로딩 예외: 최초에는 부모 1행만 제공하며, 펼치면 자식 100행을 서버에서 조회합니다. · lazy.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/lazy.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tree / Detail lazy 요청</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>Tree / Detail lazy 요청</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="lazy.js"></script>
</body>
</html>
import { ZixoGrid, treePlugin } from '../../src/index.js';
import { rows } from './lazy.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '업무', width: 360 }],
plugins: [
treePlugin({
field: 'name',
loadChildren: async (row, { signal }) => {
const response = await fetch('/docs/lazy-rows.json', { signal });
if (!response.ok) throw Error('자식 조회 실패');
return response.json();
},
}),
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'처음 펼칠 때 loadChildren으로 자식을 가져옵니다. Detail은 loadRows(row,{signal})로 같은 형태의 조회 계약을 사용합니다. 여러 lazy 요청의 공유 로딩 상태에는 제한이 있습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, treePlugin } from '../../src/index.js';
import { rows } from './lazy.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '업무', width: 360 }],
plugins: [
treePlugin({
field: 'name',
loadChildren: async (row, { signal }) => {
const response = await fetch('/docs/lazy-rows.json', { signal });
if (!response.ok) throw Error('자식 조회 실패');
return response.json();
},
}),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'처음 펼칠 때 loadChildren으로 자식을 가져옵니다. Detail은 loadRows(row,{signal})로 같은 형태의 조회 계약을 사용합니다. 여러 lazy 요청의 공유 로딩 상태에는 제한이 있습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>Tree / Detail lazy 요청</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 지연 로딩 예외: 최초에는 부모 1행만 제공하며, 펼치면 자식 100행을 서버에서 조회합니다.
export const rows = [
{ id: 1, parentId: null, name: '펼치면 자식 조회', hasChildren: true },
];
export const childRows = Array.from({ length: 100 }, (_, i) => ({
id: i + 2,
parentId: 1,
name: i === 0 ? '서버에서 가져온 자식' : '서버에서 가져온 자식 ' + (i + 1),
}));
조회 응답은 rows/total/hasMore, 저장 응답은 success입니다. 인증 없는 로컬 개발 서버 예제입니다.
이 예제의 필수값 server.fetch 필수값 — 서버 조회를 연결할 때 조회 함수를 지정합니다. server 자체는 로컬 모드에서 생략합니다.조회 응답 rows·total·hasMore 필수값 — rows 배열·정수 total·boolean hasMore를 모두 반환합니다. 빈 결과도 {rows:[], total:0, hasMore:false}로 반환합니다.fetchRows에는 서버 조회 계약을 구현한 함수를 지정합니다. 생성 전에 연결 설정을 준비합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.server = {size:100, fetch:fetchRows}처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
서버 조회 예외: 초기 배열은 비워 두고 서버에서 페이지당 100행을 조회합니다. · server.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/server.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구 서버 계약
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>서버 조회 · 저장 계약</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>서버 조회 · 저장 계약</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="server.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './server.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 280 },
{ field: 'quantity', title: '수량', parse: Number },
],
plugins: [editorsPlugin()],
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw Error('조회 실패');
return response.json();
},
},
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('서버 저장', () =>
grid.save(async (changes, { signal }) => {
const response = await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes),
signal,
});
if (!response.ok) throw Error('저장 실패');
return response.json();
}),
);
report.textContent =
'조회 응답은 rows/total/hasMore, 저장 응답은 success입니다. 인증 없는 로컬 개발 서버 예제입니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './server.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 280 },
{ field: 'quantity', title: '수량', parse: Number },
],
plugins: [editorsPlugin()],
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw Error('조회 실패');
return response.json();
},
},
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('서버 저장', () =>
grid.save(async (changes, { signal }) => {
const response = await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes),
signal,
});
if (!response.ok) throw Error('저장 실패');
return response.json();
}),
);
message.value =
'조회 응답은 rows/total/hasMore, 저장 응답은 success입니다. 인증 없는 로컬 개발 서버 예제입니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>서버 조회 · 저장 계약</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 서버 조회 예외: 초기 배열은 비워 두고 서버에서 페이지당 100행을 조회합니다.
export const rows = [];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
# 개발용 서버 연동 계약
인증·행별 권한이 없는 개발용 예제입니다. 회사 인증 정보는 포함하지 않습니다. 운영 환경의 인증, CSRF, 행별 권한, 감사로그, 저장 멱등키, DB 트랜잭션은 별도로 구현해야 합니다.
## 조회
POST /api/query, Content-Type: application/json
**필수값 — 조회 요청:** `page`, `size`, `sort`, `filters`, `search`. 빈 정렬/필터는 `[]`, 빈 검색어는 `""`로 전달합니다. `filterTree`는 선택입니다. 그리드의 server.fetch가 받은 request에는 이 항목들이 이미 들어 있습니다.
**필수값 — 조회 응답:** `rows`(배열), `total`(정수), `hasMore`(boolean). 빈 결과도 `{ "rows": [], "total": 0, "hasMore": false }`로 모두 반환합니다. 각 행에는 고유한 rowKey 값이 필요합니다.
요청 전체 예:
```json
{"page":0,"size":2,"sort":[{"field":"id","dir":"asc"}],"filters":[],"filterTree":{"op":"and","rules":[{"field":"quantity","op":"gte","value":0}]},"search":"서버"}
```
응답 전체 예:
```json
{"rows":[{"id":1,"name":"서버 행 1","quantity":0,"price":1000,"date":"2026-09-18","choice":"A","active":true},{"id":2,"name":"서버 행 2","quantity":1,"price":1001,"date":"2026-09-18","choice":"A","active":true}],"total":20000,"hasMore":true}
```
page는 0부터 시작, size는 1~1000. sort는 최대 8개. filters는 AND로 결합, filterTree는 중첩 AND/OR. 필터 깊이 8, 총 노드 100, 그룹당 50개, in 최대 100값. search는 200자 이하. 서버 모드에서 변경이 존재하면 검색·정렬·필터 변경을 거부한다. 저장 또는 원복 후 재조회한다. 조건 변경은 이전 요청을 취소하고 세대 번호가 다른 응답을 폐기한다. 중복 행 ID가 있는 추가 페이지는 전체 거부한다. 초기 조회 실패는 기존 데이터, 추가 실패는 이전 페이지를 유지한다. retry는 실패한 page와 append 값을 다시 사용한다. Infinite Scroll은 viewport 끝 150px 이내에서 다음 페이지를 요청한다.
## 저장
POST /api/save
**필수값 — 저장 요청:** `insert`, `update`, `delete` 배열. 변경이 없는 종류도 `[]`로 전달하며 각 행의 `id`는 필수값입니다.
**필수값 — 저장 성공 응답:** `success: true`. `affected`는 그리드의 성공 판정 필수값이 아닙니다.
```json
{"insert":[{"id":20001,"name":"신규","quantity":1,"price":1000,"date":"2026-09-18","choice":"A","active":true}],"update":[{"id":1,"name":"수정","quantity":2,"price":1000,"date":"2026-09-18","choice":"B","active":true}],"delete":[{"id":2}]}
```
성공: HTTP 200
```json
{"success":true,"affected":3}
```
검증 실패: HTTP 400
```json
{"success":false,"message":"타입 오류: quantity"}
```
ID 충돌/없는 행: HTTP 409. 요청 크기 초과: HTTP 413. 일회 모의 장애: HTTP 503. 모든 저장 검증 후 메모리 Map을 교체해 원자적으로 적용한다. 저장 배치 총 1000행, 요청 최대 1MiB. 허용 필드: id/name/quantity/price/date/choice/active/parentId. 계산 필드(total)는 저장 전에 제거해야 한다.
클라이언트 save의 failure 기본값 keep: 실패하면 변경 상태를 유지. rollback: 마지막 서버 확정 상태로 돌아감. optimistic:true는 응답 전 표시 상태를 확정하고, 실패 시 선택한 정책으로 복구. 저장 중 편집을 차단한다. retry는 실패 후 데이터가 바뀌지 않았을 때 당시 배치를 복원해 동일 작업을 수행한다. 서버가 저장했지만 응답이 유실된 경우 멱등성이 보장되지 않으므로 운영 서버에 요청 ID를 추가해야 한다.
## 행 순서 저장 (rowReorder)
rowReorder:true에서 순서가 바뀌면 저장 배치에 선택 항목 `order`가 추가된다. 값은 현재 적재된 모든 행의 키 배열이며 숫자 키와 문자열 키를 구분한다. 예: `{insert:[], update:[], delete:[], order:[3,1,2]}`. 서버는 배열의 순서를 저장해야 하며 일반 CRUD만 처리하면 순서는 보존되지 않는다. 이 개발 서버는 정수 id만 지원하고 order를 처리해 DB의 해당 행 위치를 교체한다. 미조회 행의 상대 위치는 유지한다. 누락 ID·중복·잘못된 키는 CRUD와 함께 원자적으로 거부한다. 실제 DB에서는 순번 컬럼·트랜잭션으로 영속화해야 한다. 성공 저장은 success:true를 반환하며 order도 새 기준 상태로 확정한다. 기존 배치의 order 생략은 허용한다.
스크롤 끝에서 다음 페이지를 추가합니다. hasMore가 false이면 조회를 멈춥니다.
이 예제의 필수값 server.fetch, 응답 rows·total·hasMore 필수값 — 서버 조회 함수와 세 응답 항목을 모두 제공합니다. size는 생략하면 100입니다.조회 응답의 hasMore로 다음 페이지 여부를 알려 주며 생성 후 loadNext로 추가 조회할 수 있습니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.server = {size:100, fetch:fetchRows}처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
무한 스크롤 예외: 초기 배열은 비워 두고 서버에서 페이지당 100행을 추가합니다. · infinite.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/infinite.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Infinite Scroll</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>Infinite Scroll</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="infinite.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './infinite.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '주문', width: 280 }],
plugins: [],
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw Error('조회 실패');
return response.json();
},
},
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('다음 페이지', () => grid.loadNext());
report.textContent =
'스크롤 끝에서 다음 페이지를 추가합니다. hasMore가 false이면 조회를 멈춥니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './infinite.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '주문', width: 280 }],
plugins: [],
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw Error('조회 실패');
return response.json();
},
},
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('다음 페이지', () => grid.loadNext());
message.value =
'스크롤 끝에서 다음 페이지를 추가합니다. hasMore가 false이면 조회를 멈춥니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>Infinite Scroll</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 무한 스크롤 예외: 초기 배열은 비워 두고 서버에서 페이지당 100행을 추가합니다.
export const rows = [];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
Loading · Empty · Error · 재시도 예제 독립 실행 ↗
로딩은 기존 행을 밀지 않는 오버레이이며 기본 위치는 center(중앙)입니다. 오류 오버레이는 errorDuration 기본 1500ms 뒤 자동 숨김이며 0은 자동 숨김 없음입니다. 새 오류마다 시간이 다시 시작됩니다. 숨겨도 검증 실패·재시도 상태는 유지하며 서버 오류에서는 retry()로 실패 요청을 재시도합니다.
초기 위치를 설정하고 생성 후 setLoading/setLoadingPosition으로 표시와 위치를 변경합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.loadingPosition = 'center'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · loading.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/loading.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loading · Empty · Error · 재시도</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>Loading · Empty · Error · 재시도</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="loading.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './loading.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
errorDuration: 1500,
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
select(
'로딩 위치',
[
'center',
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
],
(v) => grid.setLoadingPosition(v),
);
button('로딩 표시', () => grid.setLoading(true, { message: '조회 중입니다' }));
button('추가 로딩', () => grid.setLoading(true, { append: true }));
button('로딩 종료', () => grid.setLoading(false));
button('오류 표시 · 1.5초', () =>
grid.attempt(() => grid.setColumnWidth('missing', 100)),
);
button('빈 데이터', () => grid.setRows([]));
button('데이터 복원', () => grid.setRows(options.rows));
report.textContent =
'로딩은 기존 행을 밀지 않는 오버레이이며 기본 위치는 center(중앙)입니다. 오류 오버레이는 errorDuration 기본 1500ms 뒤 자동 숨김이며 0은 자동 숨김 없음입니다. 새 오류마다 시간이 다시 시작됩니다. 숨겨도 검증 실패·재시도 상태는 유지하며 서버 오류에서는 retry()로 실패 요청을 재시도합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './loading.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
errorDuration: 1500,
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
select(
'로딩 위치',
[
'center',
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
],
(v) => grid.setLoadingPosition(v),
);
button('로딩 표시', () =>
grid.setLoading(true, { message: '조회 중입니다' }),
);
button('추가 로딩', () => grid.setLoading(true, { append: true }));
button('로딩 종료', () => grid.setLoading(false));
button('오류 표시 · 1.5초', () =>
grid.attempt(() => grid.setColumnWidth('missing', 100)),
);
button('빈 데이터', () => grid.setRows([]));
button('데이터 복원', () => grid.setRows(options.rows));
message.value =
'로딩은 기존 행을 밀지 않는 오버레이이며 기본 위치는 center(중앙)입니다. 오류 오버레이는 errorDuration 기본 1500ms 뒤 자동 숨김이며 0은 자동 숨김 없음입니다. 새 오류마다 시간이 다시 시작됩니다. 숨겨도 검증 실패·재시도 상태는 유지하며 서버 오류에서는 retry()로 실패 요청을 재시도합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>Loading · Empty · Error · 재시도</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
화면 주변 행만 렌더하며 scrollToRow에 고유 행 키를 전달합니다.
행 번호 위치가 아닌 고유 행 키로 이동합니다. 이 예제의 마지막 행 키는 100000입니다. 생성 후 grid.scrollToRow(100000)를 호출합니다.
행 데이터: 100,000행 · large.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/large.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>10만 행 · 가상 스크롤</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>10만 행 · 가상 스크롤</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="large.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './large.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'id', title: 'ID', editable: false },
{ field: 'name', title: '내용', width: 280 },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('마지막 행 이동', () => grid.scrollToRow(100000));
button('첫 행 이동', () => grid.scrollToRow(1));
report.textContent =
'화면 주변 행만 렌더하며 scrollToRow에 고유 행 키를 전달합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './large.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'id', title: 'ID', editable: false },
{ field: 'name', title: '내용', width: 280 },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('마지막 행 이동', () => grid.scrollToRow(100000));
button('첫 행 이동', () => grid.scrollToRow(1));
message.value =
'화면 주변 행만 렌더하며 scrollToRow에 고유 행 키를 전달합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>10만 행 · 가상 스크롤</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 최소 100행. 대량 데이터는 배열 생성식을 사용합니다.
export const rows = Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
name: '행 ' + (i + 1),
}));
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
CSV 가져오기는 신규 키의 행을 추가합니다. XLSX Import는 지원하지 않습니다.
exportPlugin 설치 후 생성된 그리드에서 내보내기 API를 호출합니다. 생성 후 grid.exportCSV({downloadFile:true}); grid.exportXLSX({downloadFile:true})를 호출합니다.
행 데이터: 최소 100행 · export.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/export.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSV · XLSX · CSV Import</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>CSV · XLSX · CSV Import</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="export.js"></script>
</body>
</html>
import { ZixoGrid, exportPlugin } from '../../src/index.js';
import { rows } from './export.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [exportPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('CSV 내보내기', () => grid.exportCSV({ downloadFile: true }));
button('XLSX 내보내기', () => grid.exportXLSX({ downloadFile: true }));
const upload = document.createElement('input');
upload.type = 'file';
upload.accept = '.csv';
upload.setAttribute('aria-label', 'CSV 가져오기');
upload.addEventListener('change', async () => {
if (!upload.files[0]) return;
try {
report.textContent = grid.importCSV(await upload.files[0].text());
} catch (e) {
report.textContent = e.message;
}
});
toolbar.append(upload);
report.textContent =
'CSV 가져오기는 신규 키의 행을 추가합니다. XLSX Import는 지원하지 않습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, exportPlugin } from '../../src/index.js';
import { rows } from './export.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
async function importFile(event) {
const file = event.target.files?.[0];
if (!file) return;
const text = await file.text();
if (alive) await run(() => grid.importCSV(text));
event.target.value = '';
}
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [exportPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('CSV 내보내기', () => grid.exportCSV({ downloadFile: true }));
button('XLSX 내보내기', () => grid.exportXLSX({ downloadFile: true }));
message.value =
'CSV 가져오기는 신규 키의 행을 추가합니다. XLSX Import는 지원하지 않습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>CSV · XLSX · CSV Import</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
<input
type="file"
accept=".csv"
aria-label="CSV 가져오기"
@change="importFile"
/>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
현재 적재된 모든 행을 인쇄합니다. 서버에서 아직 조회하지 않은 페이지는 포함하지 않습니다.
exportPlugin 설치 후 현재 적재된 전체 행을 인쇄합니다. 생성 후 grid.print()를 호출합니다.
행 데이터: 최소 100행 · print.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/print.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>적재된 전체 행 인쇄</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>적재된 전체 행 인쇄</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="print.js"></script>
</body>
</html>
import { ZixoGrid, exportPlugin } from '../../src/index.js';
import { rows } from './print.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [exportPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('전체 행 인쇄', () => grid.print());
report.textContent =
'현재 적재된 모든 행을 인쇄합니다. 서버에서 아직 조회하지 않은 페이지는 포함하지 않습니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, exportPlugin } from '../../src/index.js';
import { rows } from './print.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [exportPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('전체 행 인쇄', () => grid.print());
message.value =
'현재 적재된 모든 행을 인쇄합니다. 서버에서 아직 조회하지 않은 페이지는 포함하지 않습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>적재된 전체 행 인쇄</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
표시·편집 권한을 변경합니다. 서버 권한은 서버에서 별도로 검증해야 합니다.
생성 후 표시·편집 권한을 변경합니다. 서버 권한 검사는 별도로 필요합니다. 생성 후 grid.setPermissions({edit:(row,column)=>column.field !== 'qty'})를 호출합니다.
행 데이터: 최소 100행 · permissions.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/permissions.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>동적 권한 · 표시 / 편집</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>동적 권한 · 표시 / 편집</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="permissions.js"></script>
</body>
</html>
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './permissions.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('수량 편집 금지', () =>
grid.setPermissions({ edit: (r, c) => c.field !== 'qty' }),
);
button('단가 숨김', () =>
grid.setPermissions({ view: (c) => c.field !== 'price' }),
);
button('권한 복원', () => grid.setPermissions({}));
report.textContent =
'표시·편집 권한을 변경합니다. 서버 권한은 서버에서 별도로 검증해야 합니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './permissions.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('수량 편집 금지', () =>
grid.setPermissions({ edit: (r, c) => c.field !== 'qty' }),
);
button('단가 숨김', () =>
grid.setPermissions({ view: (c) => c.field !== 'price' }),
);
button('권한 복원', () => grid.setPermissions({}));
message.value =
'표시·편집 권한을 변경합니다. 서버 권한은 서버에서 별도로 검증해야 합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>동적 권한 · 표시 / 편집</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
grid/row/gridcell과 행·열 위치 정보를 제공합니다. Tab 진입 후 방향키로 이동합니다. 실제 스크린리더는 별도 검증입니다.
그리드의 접근성 이름을 생성 전에 지정합니다. 실제 스크린리더 사용성은 별도 검증합니다. 초기 설정은 위 예제처럼 생성 옵션 객체 안에 직접 넣으며, 생성 전에 options.ariaLabel = '주문 접근성 예제'처럼 별도로 지정해도 됩니다. 생성 후 원래 options 객체만 변경하면 자동 반영되지 않으므로 해당 API를 사용하거나 재생성하세요.
행 데이터: 최소 100행 · accessibility.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/accessibility.js
이 기능의 소스 선택 소스 복사
HTML은 해당 JavaScript 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ARIA · 접근성</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>ARIA · 접근성</h1>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="accessibility.js"></script>
</body>
</html>
import { ZixoGrid } from '../../src/index.js';
import { rows } from './accessibility.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
ariaLabel: '주문 접근성 예제',
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'grid/row/gridcell과 행·열 위치 정보를 제공합니다. Tab 진입 후 방향키로 이동합니다. 실제 스크린리더는 별도 검증입니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid } from '../../src/index.js';
import { rows } from './accessibility.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
ariaLabel: '주문 접근성 예제',
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'grid/row/gridcell과 행·열 위치 정보를 제공합니다. Tab 진입 후 방향키로 이동합니다. 실제 스크린리더는 별도 검증입니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>ARIA · 접근성</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
name이 고유한 플러그인을 install하고 name으로 uninstall합니다. install은 정리 함수를 반환하며 destroy 시에도 실행됩니다.
이 예제의 필수값 사용자 플러그인의 name·install 필수값 — 직접 만든 플러그인 등록 시 고유 name과 install(grid) 함수를 제공합니다. 내장 플러그인은 이미 제공합니다.실행 중 플러그인을 설치·해제합니다. 처음부터 사용할 플러그인은 생성 옵션 plugins에 넣습니다. 생성 후 grid.install(columnToolsPlugin()); grid.uninstall('columnTools')를 호출합니다.
행 데이터: 최소 100행 · plugins.data.js · JavaScript와 Vue 3가 같은 데이터를 사용합니다.
실행: JavaScript · examples/features/plugins.js
이 기능의 소스 선택 소스 복사
HTML은 JavaScript·UI 도구 파일과 함께 사용합니다. 행 데이터 탭의 파일도 함께 사용하세요. Vue 3 탭은 해당 컴포넌트를 실행합니다. 실행 방식 전환 시 예제가 초기화됩니다. Vue 3 실행 ↗ · Vue 원본
HTML JavaScript Vue 3 행 데이터 JavaScript UI 도구
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>플러그인 설치 · 해제</title>
<link rel="stylesheet" href="../../src/zixo.css" />
<link rel="stylesheet" href="../demo.css" />
</head>
<body>
<main>
<h1>플러그인 설치 · 해제</h1>
<div id="toolbar" class="toolbar"></div>
<div id="grid"></div>
<div id="report" class="notes result" aria-live="polite"></div>
</main>
<script type="module" src="plugins.js"></script>
</body>
</html>
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './plugins.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('컬럼 도구 설치', () => grid.install(columnToolsPlugin()));
button('컬럼 도구 해제', () => grid.uninstall('columnTools'));
report.textContent =
'name이 고유한 플러그인을 install하고 name으로 uninstall합니다. install은 정리 함수를 반환하며 destroy 시에도 실행됩니다.';
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './plugins.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('컬럼 도구 설치', () => grid.install(columnToolsPlugin()));
button('컬럼 도구 해제', () => grid.uninstall('columnTools'));
message.value =
'name이 고유한 플러그인을 install하고 name으로 uninstall합니다. install은 정리 함수를 반환하며 destroy 시에도 실행됩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>플러그인 설치 · 해제</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '서울 주문',
qty: 2,
price: 1500,
},
{
id: 2,
name: '부산 주문',
qty: 3,
price: 2000,
},
{
id: 3,
name: '서울 주문 3',
qty: 2,
price: 1500,
},
{
id: 4,
name: '부산 주문 4',
qty: 3,
price: 2000,
},
{
id: 5,
name: '서울 주문 5',
qty: 2,
price: 1500,
},
{
id: 6,
name: '부산 주문 6',
qty: 3,
price: 2000,
},
{
id: 7,
name: '서울 주문 7',
qty: 2,
price: 1500,
},
{
id: 8,
name: '부산 주문 8',
qty: 3,
price: 2000,
},
{
id: 9,
name: '서울 주문 9',
qty: 2,
price: 1500,
},
{
id: 10,
name: '부산 주문 10',
qty: 3,
price: 2000,
},
{
id: 11,
name: '서울 주문 11',
qty: 2,
price: 1500,
},
{
id: 12,
name: '부산 주문 12',
qty: 3,
price: 2000,
},
{
id: 13,
name: '서울 주문 13',
qty: 2,
price: 1500,
},
{
id: 14,
name: '부산 주문 14',
qty: 3,
price: 2000,
},
{
id: 15,
name: '서울 주문 15',
qty: 2,
price: 1500,
},
{
id: 16,
name: '부산 주문 16',
qty: 3,
price: 2000,
},
{
id: 17,
name: '서울 주문 17',
qty: 2,
price: 1500,
},
{
id: 18,
name: '부산 주문 18',
qty: 3,
price: 2000,
},
{
id: 19,
name: '서울 주문 19',
qty: 2,
price: 1500,
},
{
id: 20,
name: '부산 주문 20',
qty: 3,
price: 2000,
},
{
id: 21,
name: '서울 주문 21',
qty: 2,
price: 1500,
},
{
id: 22,
name: '부산 주문 22',
qty: 3,
price: 2000,
},
{
id: 23,
name: '서울 주문 23',
qty: 2,
price: 1500,
},
{
id: 24,
name: '부산 주문 24',
qty: 3,
price: 2000,
},
{
id: 25,
name: '서울 주문 25',
qty: 2,
price: 1500,
},
{
id: 26,
name: '부산 주문 26',
qty: 3,
price: 2000,
},
{
id: 27,
name: '서울 주문 27',
qty: 2,
price: 1500,
},
{
id: 28,
name: '부산 주문 28',
qty: 3,
price: 2000,
},
{
id: 29,
name: '서울 주문 29',
qty: 2,
price: 1500,
},
{
id: 30,
name: '부산 주문 30',
qty: 3,
price: 2000,
},
{
id: 31,
name: '서울 주문 31',
qty: 2,
price: 1500,
},
{
id: 32,
name: '부산 주문 32',
qty: 3,
price: 2000,
},
{
id: 33,
name: '서울 주문 33',
qty: 2,
price: 1500,
},
{
id: 34,
name: '부산 주문 34',
qty: 3,
price: 2000,
},
{
id: 35,
name: '서울 주문 35',
qty: 2,
price: 1500,
},
{
id: 36,
name: '부산 주문 36',
qty: 3,
price: 2000,
},
{
id: 37,
name: '서울 주문 37',
qty: 2,
price: 1500,
},
{
id: 38,
name: '부산 주문 38',
qty: 3,
price: 2000,
},
{
id: 39,
name: '서울 주문 39',
qty: 2,
price: 1500,
},
{
id: 40,
name: '부산 주문 40',
qty: 3,
price: 2000,
},
{
id: 41,
name: '서울 주문 41',
qty: 2,
price: 1500,
},
{
id: 42,
name: '부산 주문 42',
qty: 3,
price: 2000,
},
{
id: 43,
name: '서울 주문 43',
qty: 2,
price: 1500,
},
{
id: 44,
name: '부산 주문 44',
qty: 3,
price: 2000,
},
{
id: 45,
name: '서울 주문 45',
qty: 2,
price: 1500,
},
{
id: 46,
name: '부산 주문 46',
qty: 3,
price: 2000,
},
{
id: 47,
name: '서울 주문 47',
qty: 2,
price: 1500,
},
{
id: 48,
name: '부산 주문 48',
qty: 3,
price: 2000,
},
{
id: 49,
name: '서울 주문 49',
qty: 2,
price: 1500,
},
{
id: 50,
name: '부산 주문 50',
qty: 3,
price: 2000,
},
{
id: 51,
name: '서울 주문 51',
qty: 2,
price: 1500,
},
{
id: 52,
name: '부산 주문 52',
qty: 3,
price: 2000,
},
{
id: 53,
name: '서울 주문 53',
qty: 2,
price: 1500,
},
{
id: 54,
name: '부산 주문 54',
qty: 3,
price: 2000,
},
{
id: 55,
name: '서울 주문 55',
qty: 2,
price: 1500,
},
{
id: 56,
name: '부산 주문 56',
qty: 3,
price: 2000,
},
{
id: 57,
name: '서울 주문 57',
qty: 2,
price: 1500,
},
{
id: 58,
name: '부산 주문 58',
qty: 3,
price: 2000,
},
{
id: 59,
name: '서울 주문 59',
qty: 2,
price: 1500,
},
{
id: 60,
name: '부산 주문 60',
qty: 3,
price: 2000,
},
{
id: 61,
name: '서울 주문 61',
qty: 2,
price: 1500,
},
{
id: 62,
name: '부산 주문 62',
qty: 3,
price: 2000,
},
{
id: 63,
name: '서울 주문 63',
qty: 2,
price: 1500,
},
{
id: 64,
name: '부산 주문 64',
qty: 3,
price: 2000,
},
{
id: 65,
name: '서울 주문 65',
qty: 2,
price: 1500,
},
{
id: 66,
name: '부산 주문 66',
qty: 3,
price: 2000,
},
{
id: 67,
name: '서울 주문 67',
qty: 2,
price: 1500,
},
{
id: 68,
name: '부산 주문 68',
qty: 3,
price: 2000,
},
{
id: 69,
name: '서울 주문 69',
qty: 2,
price: 1500,
},
{
id: 70,
name: '부산 주문 70',
qty: 3,
price: 2000,
},
{
id: 71,
name: '서울 주문 71',
qty: 2,
price: 1500,
},
{
id: 72,
name: '부산 주문 72',
qty: 3,
price: 2000,
},
{
id: 73,
name: '서울 주문 73',
qty: 2,
price: 1500,
},
{
id: 74,
name: '부산 주문 74',
qty: 3,
price: 2000,
},
{
id: 75,
name: '서울 주문 75',
qty: 2,
price: 1500,
},
{
id: 76,
name: '부산 주문 76',
qty: 3,
price: 2000,
},
{
id: 77,
name: '서울 주문 77',
qty: 2,
price: 1500,
},
{
id: 78,
name: '부산 주문 78',
qty: 3,
price: 2000,
},
{
id: 79,
name: '서울 주문 79',
qty: 2,
price: 1500,
},
{
id: 80,
name: '부산 주문 80',
qty: 3,
price: 2000,
},
{
id: 81,
name: '서울 주문 81',
qty: 2,
price: 1500,
},
{
id: 82,
name: '부산 주문 82',
qty: 3,
price: 2000,
},
{
id: 83,
name: '서울 주문 83',
qty: 2,
price: 1500,
},
{
id: 84,
name: '부산 주문 84',
qty: 3,
price: 2000,
},
{
id: 85,
name: '서울 주문 85',
qty: 2,
price: 1500,
},
{
id: 86,
name: '부산 주문 86',
qty: 3,
price: 2000,
},
{
id: 87,
name: '서울 주문 87',
qty: 2,
price: 1500,
},
{
id: 88,
name: '부산 주문 88',
qty: 3,
price: 2000,
},
{
id: 89,
name: '서울 주문 89',
qty: 2,
price: 1500,
},
{
id: 90,
name: '부산 주문 90',
qty: 3,
price: 2000,
},
{
id: 91,
name: '서울 주문 91',
qty: 2,
price: 1500,
},
{
id: 92,
name: '부산 주문 92',
qty: 3,
price: 2000,
},
{
id: 93,
name: '서울 주문 93',
qty: 2,
price: 1500,
},
{
id: 94,
name: '부산 주문 94',
qty: 3,
price: 2000,
},
{
id: 95,
name: '서울 주문 95',
qty: 2,
price: 1500,
},
{
id: 96,
name: '부산 주문 96',
qty: 3,
price: 2000,
},
{
id: 97,
name: '서울 주문 97',
qty: 2,
price: 1500,
},
{
id: 98,
name: '부산 주문 98',
qty: 3,
price: 2000,
},
{
id: 99,
name: '서울 주문 99',
qty: 2,
price: 1500,
},
{
id: 100,
name: '부산 주문 100',
qty: 3,
price: 2000,
},
];
export function createToolbar(toolbar, report) {
const button = (label, fn) => {
const b = document.createElement("button");
b.textContent = label;
b.addEventListener("click", async () => {
try {
const value = await fn();
if (value !== undefined)
report.textContent =
typeof value === "string" ? value : JSON.stringify(value, null, 2);
} catch (error) {
report.textContent = error.message;
}
});
toolbar.append(b);
return b;
};
const select = (label, values, fn) => {
const l = document.createElement("label");
l.textContent = label + " ";
const s = document.createElement("select");
s.setAttribute("aria-label", label);
for (const value of values) {
const o = document.createElement("option");
o.value = o.textContent = value;
s.append(o);
}
s.addEventListener("change", () => fn(s.value));
l.append(s);
toolbar.append(l);
return s;
};
return { button, select };
}
테마 빌더 사용법
라이트·다크·포레스트 테마를 시작점으로 셀·헤더·선택·마우스 오버·교차 행·체크박스 색상과 서체, 글자 크기, 행·헤더 높이, 모서리를 조절합니다. 120행 미리보기에서 선택·정렬·편집을 직접 확인할 수 있습니다.
테마 빌더 열기 ↗
시작 테마를 고른 뒤 색상과 크기를 조절합니다. 프리셋 변경·초기화는 디자인 설정을 기본값으로 돌리며 미리보기의 편집한 행은 유지합니다. CSS 탭의 코드를 복사하거나 CSS 내려받기로 zixo-custom-theme.css를 저장합니다. zixo.css 다음에 테마 CSS를 불러오고 생성한 grid.root에 zixo-custom-theme 클래스를 추가합니다. JavaScript / Vue 3 탭에 생성된 rowHeight·headerHeight 옵션을 그리드 생성에 함께 지정합니다. 행 높이를 CSS만으로 변경하면 가상 스크롤 좌표와 어긋날 수 있습니다.
Vue 3 코드는 ref·onMounted에서 생성하고 onBeforeUnmount에서 정리합니다. import 경로는 프로젝트 위치에 맞게 수정하세요. 행 데이터 탭은 미리보기에서 사용하는 별도 데이터 파일 원문입니다. 글꼴은 설치된 로컬 폰트를 사용하며 외부 폰트를 내려받지 않습니다.
내보낸 CSS는 해당 클래스를 가진 그리드 내부에만 적용됩니다. 별도 팝업·날짜 달력·헤더 컨텍스트 메뉴·툴팁은 이 테마의 범위에 포함하지 않습니다. 원하는 색상 조합의 명도 대비는 직접 확인하세요. 설정은 새로고침하면 초기화되므로 완성한 코드를 저장하세요.
전체 함수 API Core의 공개 호출과 플러그인 설치 후 제공되는 호출을 구분합니다. 행 키는 별도 설명이 없는 한 실제 rowKey 값이며 숫자 1과 문자열 '1'은 다릅니다. 아래 예시는 이미 생성된 grid와 columns/rows 등 애플리케이션 변수를 전제로 합니다.
데이터 설정·변경 · 선택·복사·스크롤 · 컬럼·표시·조회 조건 · 로딩·서버·수명주기
데이터 설정·변경 grid.setRows(rows)반환: undefined
행 배열로 전체 교체하고 기준 상태·변경 목록·Undo/Redo를 초기화합니다. ID 누락/중복·계산 실패 시 기존 데이터는 유지합니다. 데이터 조회 응답에는 insertRows 대신 사용하세요. 선택은 새 화면에 유효한 셀만 유지되며 명시적 전체 해제는 clearSelection입니다. 진행 중인 조회 요청을 취소하지 않습니다.
grid.setRows(response.rows);관련 실행 예제
grid.clearRows()반환: undefined
setRows([])와 동일합니다. 삭제 변경으로 기록하지 않습니다. 서버에 삭제를 저장하려면 deleteRows를 사용하세요.
grid.clearRows();관련 실행 예제
grid.getRows()반환: object[]
필터/가상 스크롤과 관계없이 적재한 전체 행의 복사본을 반환합니다. 행 이동 순서를 반영하며 아직 조회하지 않은 서버 페이지는 포함하지 않습니다.
const rows = grid.getRows();관련 실행 예제
grid.insert(rowOrRows)반환: undefined
한 행 또는 행 배열을 신규 추가합니다. 변경 목록의 insert에 기록하고 하나의 Undo 트랜잭션으로 처리합니다.
grid.insert({id:101,name:"신규"});관련 실행 예제
grid.insertRows(rows)반환: undefined
배열 전체를 신규 추가합니다. 키 중복/누락·계산 실패 시 전체 취소합니다. insert(row)와 목적이 같고 조회 결과 교체가 아닙니다.
grid.insertRows(newRows);관련 실행 예제
grid.deleteRows(ids)반환: undefined
행 키 배열을 삭제하고 기준 행은 delete 변경으로 기록합니다. 신규 행 추가 후 삭제는 최종 변경 목록에서 제외됩니다. permissions.delete를 검사합니다.
grid.deleteRows([1,2]);관련 실행 예제
grid.updateCells(changes)반환: undefined
{id,field,value} 배열을 parse→validate→계산 순으로 원자 반영합니다. 읽기 전용/키/계산 컬럼 수정은 거부합니다. 하나의 Undo로 처리합니다.
grid.updateCells([{id:1,field:"qty",value:3}]);관련 실행 예제
grid.getChangedRows()반환: object
insert/update/delete 배열 복사본을 반환합니다. rowReorder:true이고 현재 행 키 배열이 기준과 다를 때 전체 키 순서인 order도 포함합니다. update는 변경 필드만이 아닌 행 전체입니다.
const changes = grid.getChangedRows();관련 실행 예제
grid.acceptChanges()반환: undefined
현재 상태를 새 기준으로 확정하고 Undo/Redo를 초기화합니다. 서버 저장을 수행하지 않으므로 실제 저장 성공 후 호출하세요.
grid.acceptChanges();관련 실행 예제
grid.revertChanges()반환: undefined
마지막 기준 상태로 원복하고 Undo/Redo를 초기화합니다.
grid.revertChanges();관련 실행 예제
grid.undo()반환: boolean
직전 변경 트랜잭션을 취소합니다. 취소할 기록이 없으면 false입니다. 최대100개 스냅샷을 보관합니다.
const changed = grid.undo();관련 실행 예제
grid.redo()반환: boolean
Undo한 변경을 다시 적용합니다. 기록이 없으면 false입니다.
const changed = grid.redo();관련 실행 예제
grid.moveRow(id, target, position = "before")반환: boolean
행을 target 앞/뒤로 이동하고 getRows 및 저장 order에 반영합니다. rowReorder:true가 필요하며 정렬·필터·트리·그룹·고정행·저장 중에는 제한됩니다.
grid.moveRow(2,1,"before");관련 실행 예제
선택·복사·스크롤 grid.selectCell(id, field, {extend=false, add=false} = {})반환: undefined
보이는 행/컬럼의 셀을 선택합니다. extend는 범위 확장, add는 기존 선택에 추가입니다. 화면에 없는 대상은 오류입니다.
grid.selectCell(1,"name");관련 실행 예제
grid.selectedCells()반환: {id,field}[]
선택 셀의 행 키/필드 배열을 반환합니다. 선택 행 배열이 아닙니다.
const cells = grid.selectedCells();관련 실행 예제
grid.clearSelection()반환: undefined
선택·활성 셀·선택 기준을 해제하고 빈 selectionchange를 발생시킵니다.
grid.clearSelection();관련 실행 예제
grid.selectAll()반환: undefined
현재 view의 데이터 행과 표시 데이터 컬럼을 모두 선택합니다. 행번호 같은 유틸리티 컬럼은 제외합니다.
grid.selectAll();관련 실행 예제
grid.copy()반환: string | undefined
선택 범위를 TSV 문자열로 반환합니다. 선택 없음/편집 중이면 undefined입니다. API 호출만으로 시스템 클립보드에 쓰지 않습니다.
const tsv = grid.copy();관련 실행 예제
grid.pasteText(tsv, start?)반환: number
start={id,field} 또는 활성 셀부터 TSV를 붙여넣고 반영 셀 수를 반환합니다. 읽기 전용 시작/혼합 범위는0으로 무시하며 다른 검증/범위 오류는 throw합니다.
grid.pasteText("수정 3",{id:1,field:"name"});관련 실행 예제
grid.scrollToRow(id)반환: undefined
현재 스크롤 본문에 적재된 행이 보이도록 이동합니다. 해당 행이 없거나 고정/접힌 행이면 이동하지 않습니다. 서버 페이지를 자동 조회하지 않습니다.
grid.scrollToRow(80);관련 실행 예제
grid.scrollToColumn(field)반환: undefined
표시 컬럼을 가로 뷰포트 안으로 이동합니다. 숨김/없는/고정 컬럼이면 이동하지 않습니다.
grid.scrollToColumn("price");관련 실행 예제
컬럼·표시·조회 조건 grid.setColumnWidth(field, width)반환: undefined
minWidth/maxWidth 범위로 너비를 제한하고 해당 컬럼의 flex를0으로 바꿉니다. 세 번째 refresh 인수는 내부 드래그용입니다.
grid.setColumnWidth("name",220);관련 실행 예제
grid.setColumnVisible(field, visible)반환: undefined
컬럼 표시 여부를 바꿉니다. 마지막 표시 데이터 컬럼 숨김은 거부하며 유틸리티 컬럼은 변경하지 않습니다.
grid.setColumnVisible("price",false);관련 실행 예제
grid.setColumnPinned(field, side)반환: undefined
side는 left/right/null입니다. null은 고정 해제이고 유틸리티 컬럼은 변경하지 않습니다.
grid.setColumnPinned("name","left");관련 실행 예제
grid.autoSizeColumn(field)반환: undefined
제목과 view 앞500개 항목의 표시 텍스트를 측정해 너비를 맞춥니다. setColumnWidth처럼 flex를 해제합니다.
grid.autoSizeColumn("name");관련 실행 예제
grid.moveColumn(field, targetField, position = "before")반환: undefined
컬럼을 대상 앞/뒤로 이동합니다. columnReorder와 양쪽 컬럼의 reorderable 정책을 검사합니다.
grid.moveColumn("price","qty","before");관련 실행 예제
grid.setTheme(theme)반환: undefined
light/dark/compact 테마를 적용합니다.
grid.setTheme("dark");관련 실행 예제
grid.setLocale(locale)반환: undefined
언어와 내부 UI/표시를 갱신하고 열린 컬럼 메뉴를 닫습니다. ko/en을 사용하며 업무 제목/서버 메시지는 원문을 유지합니다.
grid.setLocale("en");관련 실행 예제
grid.setPermissions(permissions)반환: undefined
권한 콜백 객체를 교체하고 모델과 화면에 반영합니다. 서버 권한 검증을 대체하지 않습니다.
grid.setPermissions({edit:row=>!row.locked});관련 실행 예제
grid.setSearch(search)반환: undefined | Promise
로컬 검색 또는 서버 첫 페이지 재조회를 수행합니다. 미저장 변경이 있는 서버 모드는 거부합니다.
await grid.setSearch("서울");관련 실행 예제
grid.setFilter(filterTree)반환: undefined | Promise
중첩 조건 트리를 지정합니다. null은 트리 조건만 해제하며 filters는 유지합니다.
await grid.setFilter(null);관련 실행 예제
grid.setFilters(filters)반환: undefined | Promise
컬럼 조건 배열(최대50개)을 교체합니다. []는 배열 조건만 해제하고 filterTree는 유지합니다.
await grid.setFilters([{field:"qty",op:"gte",value:2}]);관련 실행 예제
grid.setSort(sort)반환: undefined | Promise
{field,dir} 배열로 정렬합니다. []면 정렬 해제입니다. 로컬 갱신 또는 서버 첫 페이지 재조회입니다.
await grid.setSort([{field:"qty",dir:"desc"}]);관련 실행 예제
grid.toggleSort(field, append = false)반환: undefined | Promise
정렬 가능 컬럼을 asc→desc→해제 순으로 전환합니다. append:true는 다른 정렬을 유지합니다.
await grid.toggleSort("qty",true);관련 실행 예제
grid.openSearch()반환: undefined
화면 내 결과를 이전/다음으로 탐색하는 Ctrl+F 패널을 엽니다. setSearch의 행 필터와 다릅니다.
grid.openSearch();관련 실행 예제
grid.saveLayout()반환: object
version:1의 컬럼 순서·폭·숨김·flex·고정·조회 조건 상태 객체를 반환합니다. 파일 저장은 호출자가 수행합니다.
const layout = grid.saveLayout();관련 실행 예제
grid.restoreLayout(layout)반환: undefined | Promise
saveLayout 결과를 복원합니다. 기존 컬럼 콜백은 유지하고 조회 조건도 반영합니다.
await grid.restoreLayout(layout);관련 실행 예제
grid.refresh()반환: undefined
현재 옵션/모델로 컬럼과 view를 재계산하고 렌더링합니다. 서버를 재조회하거나 원래 생성 options 객체의 변경을 자동 동기화하지 않습니다.
grid.refresh();관련 실행 예제
로딩·서버·수명주기 grid.setLoading(loading, {append=false, message} = {})반환: undefined
로딩 오버레이를 표시/해제합니다. append는 추가 조회 문구, message는 직접 지정한 문자열입니다. 요청 실행 API가 아닙니다.
grid.setLoading(true,{message:"조회 중"});관련 실행 예제
grid.setLoadingPosition(position)반환: undefined
top-left/top-center/top-right/center/bottom-left/bottom-center/bottom-right 중 하나입니다.
grid.setLoadingPosition("center");관련 실행 예제
grid.connectServer(config)반환: Promise
{size=100,fetch}로 서버를 연결하고 첫 페이지를 조회합니다. fetch(request,{signal}) 응답은 rows/total/hasMore가 필요합니다.
await grid.connectServer({size:100,fetch:fetchRows});관련 실행 예제
grid.loadNext()반환: Promise | undefined
hasMore이며 조회 중이 아닐 때 다음 페이지를 조회합니다. 추가 조회 행은 신규 insert 변경으로 기록하지 않습니다.
await grid.loadNext();관련 실행 예제
grid.retry()반환: Promise | undefined
저장/조회 실패 후 등록된 재시도를 수행합니다. 재시도 작업이 없으면 undefined이고 실패 후 데이터 변경 시 이전 저장 재시도를 거부합니다.
await grid.retry();관련 실행 예제
grid.save(saveFn, {failure="keep", optimistic=false} = {})반환: Promise
saveFn(changes,{signal})을 호출합니다. {success:true}만 성공이며 응답을 반환합니다. 실패는 throw하고 failure:rollback이면 기준 상태로 원복합니다.
await grid.save(saveRows,{failure:"keep"});관련 실행 예제
grid.install(plugin)반환: grid
고유 name과 install(grid)을 가진 플러그인을 설치합니다. 중복 이름과 tree/grouping 동시 설치는 거부합니다.
grid.install(editorsPlugin());관련 실행 예제
grid.uninstall(name)반환: undefined
플러그인을 이름으로 해제하고 정리 함수를 호출합니다.
grid.uninstall("editors");관련 실행 예제
grid.on(eventName, handler)반환: () => void
handler(detail,grid)를 구독하고 해제 함수를 반환합니다. DOM 리스너 등록 오버로드는 내부 확장용입니다.
const stop = grid.on("datachange",detail=>console.log(detail));관련 실행 예제
grid.off(eventName, handler)반환: undefined
on에 등록했던 같은 함수 참조를 해제합니다.
grid.off("datachange",handler);관련 실행 예제
grid.edit(JSON.stringify(id), field, initialValue?)반환: undefined
편집 요청 이벤트를 발생시킵니다. 행 키는 현재 구현에서 JSON 문자열로 전달하며 editorsPlugin이 UI를 제공합니다.
grid.edit(JSON.stringify(1),"name");관련 실행 예제
grid.closeEditor()반환: undefined
편집 UI/요청을 정리합니다. 값을 확정하는 API가 아니며 미확정 입력은 반영하지 않습니다.
grid.closeEditor();관련 실행 예제
grid.showError(error, prefix = false)반환: undefined
오류 오버레이를 표시합니다. errorDuration 후 숨기며 prefix:true면 오류 접두사를 표시합니다. 자체로 error 이벤트나 retry 작업을 만들지 않습니다.
grid.showError(new Error("조회 실패"));관련 실행 예제
grid.attempt(fn)반환: fn 결과 | false
동기 콜백의 throw를 오류 오버레이와 validationerror로 처리하고 false를 반환합니다. Promise rejection을 처리하는 래퍼가 아닙니다.
grid.attempt(()=>grid.updateCells(changes));관련 실행 예제
grid.destroy()반환: undefined
요청·편집 UI·플러그인·Observer·이벤트를 정리합니다. Vue onBeforeUnmount에서 호출하며 종료 후 인스턴스를 재사용하지 않습니다.
onBeforeUnmount(()=>grid.destroy());관련 실행 예제
플러그인 설치 후 제공되는 함수 아래 메서드는 해당 플러그인 설치 후 사용합니다. 상세 인수·기본값·해제 시 동작은 각 플러그인 메뉴를 확인하세요.
grid.edit(JSON.stringify(id), field, initialValue?) — 편집 요청. id 인수는 현재 API에서 JSON으로 인코딩한 행 키입니다.grid.closeEditor() — 편집 UI와 연결된 요청·팝업을 정리합니다. 값 확정 용도가 아닙니다.grid.openColumnMenu(field, anchor?) — 지정 헤더 컬럼의 메뉴를 엽니다.grid.openColumns(anchor?) — 표시 컬럼 관리 창을 엽니다.grid.openFilterBuilder(anchor?) — 평면 AND/OR 조건 빌더를 엽니다.grid.setFilters(filters) / grid.setFilter(tree) — 조건을 API로 설정합니다. Core API이며 플러그인 해제 뒤에도 사용할 수 있습니다.grid.toggleTree(id) — 해당 행의 펼침 상태를 전환합니다. lazy 조회를 포함하며 Promise를 반환합니다.grid.setTreeLines(boolean) — 연결선을 켜거나 끄고 다시 표시합니다.grid.toggleGroup(key) — 그룹을 접거나 펼칩니다. key는 해당 그룹 값 경로를 JSON.stringify한 문자열입니다. 예: JSON.stringify(["제품","서울"]).grid.toggleDetail(id) — 부모 행의 상세를 접거나 펼칩니다. 비동기 조회를 포함하며 Promise를 반환합니다.grid.install(contextMenuPlugin({items})) — 사용자 action을 포함한 메뉴를 설치합니다.grid.uninstall("contextMenu") — 열린 메뉴를 닫고 플러그인을 해제합니다. 별도 공개 open API는 없습니다.grid.install(columnGroupsPlugin({groups})) — 컬럼 그룹 구성을 설치합니다. 그룹 설정 전용 변경 API는 없습니다.grid.uninstall("columnGroups") — 설치 전 renderHeader 동작을 복원합니다. 구성을 바꿀 때 해제 후 새 설정으로 다시 설치할 수 있습니다.grid.exportCSV({downloadFile?}) — CSV 문자열 생성 및 선택적 다운로드. 수식으로 해석될 수 있는 값에는 보호 접두사를 사용합니다.grid.exportXLSX({downloadFile?}) — XLSX 바이트 생성 및 선택적 다운로드. 단일 시트입니다.grid.importCSV(text) — 신규 행을 추가하고 추가 행 수를 반환합니다. 중복 키·잘못된 CSV는 거부합니다.grid.getPrintHTML() — 현재 표시 컬럼·전체 적재 행의 인쇄 HTML을 반환합니다.grid.print() — 새 인쇄 창을 열고 인쇄합니다. 팝업이 차단되면 오류를 냅니다.공개 설정·API가 아닌 내부 값 render/renderHeader/makeView/loadPage와 DOM 이벤트 처리기는 내부 구현입니다. columns의 actualWidth/utility, model.options.saving도 사용자 설정이 아닙니다. setColumns/getSelectedRows/getRowById/appendRows라는 Core 메서드는 제공하지 않습니다. 컬럼 전체 정의를 바꿀 때는 종료 후 생성하고, 행 전체 교체는 setRows를 사용하세요.
패키지에서 import할 수 있는 항목 src/index.js와 npm 진입점의 export 목록입니다. 아래 모델 메서드와 그리드 메서드를 혼동하지 마세요.
export 호출 설명 ZixoGrid new ZixoGrid(host,options)화면 그리드 생성. host와 표시 컬럼/행의 키 계약은 생성 예제를 참고하세요. GridModel new GridModel(options)DOM 없는 독립 데이터 모델. 행/컬럼/권한/rowKey 설정을 사용합니다. UI 업무 코드는 grid의 공개 API를 사용하세요. normalizeDate normalizeDate(value, year?, pattern?)날짜/단축 입력을 YYYY-MM-DD 문자열로 정규화합니다. 기본 year는 현재 연도. pattern은 컬럼 dateFormat과 같은 토큰 형식입니다. 유효하지 않은 날짜는 오류입니다. matches matches(row,tree)행이 조건에 맞으면 boolean을 반환합니다. null 조건은 true. eq/ne/contains/gt/gte/lt/lte/in/range와 and/or 그룹, 깊이/개수 제한이 적용됩니다. editorsPlugin editorsPlugin()셀 안에서 값을 편집하고 확정·취소·입력 검증을 처리합니다. 날짜·선택·자동완성은 컬럼 설정으로 선택합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. columnToolsPlugin columnToolsPlugin()헤더 메뉴의 컬럼 표시·필터와 조건 빌더를 제공합니다. 정렬·고정·너비 맞춤은 연결된 Core API에서 설정합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. treePlugin treePlugin(config?)부모 키를 가진 평탄한 행 배열을 계층으로 표시합니다. 5단계 예제에서 연결선·초기 펼침·접기·말단 검색 시 조상 유지를 확인합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. groupingPlugin groupingPlugin(config?)표시 중인 행을 지정한 필드 순서로 묶고 각 그룹의 행 수와 합계·평균·최소·최대를 표시합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. detailPlugin detailPlugin(config)상세 버튼 아래에 별도 ZixoGrid 인스턴스를 만들고 해당 부모의 하위 데이터를 표시합니다. 예제는 부모·상세 각각 100행을 사용합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. contextMenuPlugin contextMenuPlugin(config?)셀 우클릭으로 업무 메뉴를 열고 클릭한 셀의 행 키·필드와 현재 그리드를 사용자 action에 전달합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. columnGroupsPlugin columnGroupsPlugin(config?)일반 컬럼 헤더 위에 상위·하위 그룹 제목을 표시합니다. 데이터 행을 그룹화하는 groupingPlugin과 용도가 다릅니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다. exportPlugin exportPlugin()CSV/XLSX 생성, CSV의 신규 행 가져오기, 현재 적재된 행의 인쇄 HTML을 제공합니다. 파일 형식별 범위와 데이터 기준을 구분해서 사용합니다. 실제 인수는 해당 플러그인 상세 페이지를 따릅니다.
import { normalizeDate, matches } from '@kloz/zixo-grid';
normalizeDate('0918', 2026); // '2026-09-18'
matches({qty:3},{field:'qty',op:'gte',value:2}); // true독립 GridModel의 메서드 모델은 화면·선택·서버 요청·플러그인 수명주기를 관리하지 않습니다. 아래는 new GridModel(options)로 만든 model의 API이며 grid.model 내부 Map을 직접 수정하는 사용법은 공개 계약이 아닙니다.
호출 반환 동작 model.setRows(rows)undefined 전체 교체·기준/히스토리 초기화 model.getRows()object[] 전체 행 복사본 model.getRow(id)object | undefined 행 키 한 개의 복사본. grid.getRow는 제공하지 않음 model.insert(rowOrRows)undefined 신규 추가 model.insertRows(rows)undefined 여러 행 원자 추가 model.delete(ids)undefined 행 키 배열 삭제. 그리드 함수 이름은 deleteRows model.applyCells(changes)undefined 셀 변경 원자 반영. 그리드 함수 이름은 updateCells model.moveRow(id,target,position="before")boolean rowReorder:true 필요. 화면 정렬/필터 정책과 rowreorder 이벤트는 grid.moveRow가 담당 model.getChangedRows()object 변경 복사본과 선택적 order model.isDirty(id,field)boolean 셀 값의 기준 대비 변경 여부 model.acceptChanges()undefined 현재 상태 확정 model.revertChanges()undefined 기준으로 원복 model.undo()boolean 직전 트랜잭션 취소 model.redo()boolean 취소한 변경 다시 적용 model.append(rows)undefined 조회 페이지를 기준 상태에 추가. 신규 insert 변경으로 기록하지 않음. grid.appendRows는 제공하지 않음 model.summary(field,type="sum",rows?)number | null sum/avg/min/max. 기본 대상은 전체 행이며 숫자 값이 없으면 null model.subscribe(fn)() => void 모델 변경 구독. 해제 함수 반환. 그리드 업무 이벤트는 grid.on 사용
데이터 조회 API 조회 결과를 넣는 함수는 전체 함수 API의 setRows 에서 확인하세요. 데이터 설정 · 교체 · 추가 실행 예제 에서 setRows/insert/insertRows/clearRows 차이를 직접 확인할 수 있습니다.
아래 getRows/getChangedRows/selectedCells/copy는 인수가 없는 함수로, 호출할 때 전달해야 하는 필수값은 없습니다.
데이터 조회 API와 전체 비우기 API를 설명합니다. clearRows()는 npm 0.1.2부터 제공합니다. 데이터 조회는 저장된 값과 선택 위치를 읽는 기능이며, 서버에서 데이터를 받아 그리드에 넣는 setRows(rows)와 구분합니다.
함수 대상 반환
grid.getRows() 전체 적재 데이터 행 배열 복사본
grid.getChangedRows() 기준 상태 이후 변경 { insert: [], update: [], delete: [] }
grid.selectedCells() 선택 셀 위치 [{ id, field }]
grid.copy() 선택 데이터 TSV 문자열 또는 undefined
getRows() — 보관 중인 전체 행 조회 행 데이터의 복사본을 반환합니다. 검색·필터로 숨긴 행도 포함하며 화면 정렬 순서가 아닌 모델의 보관 순서입니다. 화면 밖으로 가상화된 행도 포함합니다. 서버 페이징/추가 조회에서는 현재까지 적재한 행만 반환하고 서버의 미조회 행은 포함하지 않습니다. 빈 데이터는 []입니다.
const rows = grid.getRows();
console.log(rows); // [{ id: 1, name: '주문 001' }, ...]
getChangedRows() — 추가·수정·삭제 데이터 조회 기준 상태 이후의 변경을 insert/update/delete 배열로 나눈 복사본입니다. update는 변경된 필드만이 아니라 해당 행 전체 데이터입니다. delete에는 삭제된 기준 행 데이터가 들어갑니다. 신규 추가 후 삭제한 행은 최종 변경 목록에 남지 않습니다. acceptChanges() 또는 setRows() 이후에는 새 기준으로 변경 목록을 계산하며, undo()/redo() 결과도 반영합니다.
const changes = grid.getChangedRows();
const insertedRows = changes.insert;
const updatedRows = changes.update;
const deletedRows = changes.delete;
// 변경이 없으면 { insert: [], update: [], delete: [] }
selectedCells() — 선택된 셀의 위치 조회 행 키 id와 컬럼 field만 반환하며 셀 값이나 행 데이터는 포함하지 않습니다. id는 rowKey로 지정한 필드의 값입니다. 반환 순서를 화면 순서로 가정하지 마세요. 일반 checkbox 컬럼의 true/false 값과 그리드의 셀 선택 상태는 별개입니다.
const cells = grid.selectedCells();
// [{ id: 100, field: 'name' }, { id: 100, field: 'qty' }]
// 선택이 없으면 []
copy() — 선택 데이터의 TSV 문자열 조회 선택된 행과 컬럼의 조합을 현재 화면 순서로 출력합니다. 열은 탭, 행은 줄바꿈으로 구분하며 셀 안의 탭/줄바꿈은 공백으로 바꿉니다. format()으로 꾸민 표시값이 아니라 보관된 값을 문자열로 바꿉니다. 선택이 없거나 편집기가 열려 있으면 undefined입니다. grid.copy()만 호출하면 시스템 클립보드는 변경하지 않고 문자열만 반환합니다. 다중 선택의 개별 셀 모양은 보존하지 않습니다.
const text = grid.copy();
// 예: '주문 001\t10\n주문 002\t20'
if (text !== undefined) console.log(text);
특정 행·셀·선택 행을 조회하는 대체 코드
grid.getRow(id), grid.getValue(id, field), grid.getSelectedRows()는 현재 ZixoGrid의 공개 메서드가 아닙니다. 아래 코드는 제공 중인 API를 조합하는 애플리케이션 코드입니다. rowKey가 다른 이름이면 row.id를 해당 키 필드로 바꾸세요. 내부 grid.model.getRow()와 그리드의 공개 API를 혼동하지 마세요.
// 아래 예시는 rowKey: 'id'인 그리드를 가정합니다.
// 한 번 조회한 배열을 재사용해 전체 데이터 복사를 반복하지 않습니다.
const rows = grid.getRows();
const byId = new Map(rows.map(row => [row.id, row]));
// 특정 행과 셀 값: 현재는 애플리케이션에서 조회합니다.
const row = byId.get(100);
const value = row?.name;
// 선택된 셀의 위치와 실제 값
const selectedValues = grid.selectedCells().map(({ id, field }) => ({
id, field, value: byId.get(id)?.[field]
}));
// 선택된 셀이 하나라도 있는 행을 중복 없이 조회합니다.
const ids = new Set(grid.selectedCells().map(cell => cell.id));
const selectedRows = rows.filter(row => ids.has(row.id));
// 이 대체 코드는 화면 정렬 순서가 아니라 getRows()의 보관 순서입니다.
조회 결과 수정과 편집 확정 getRows()/getChangedRows()의 복사본을 바꿔도 원본은 수정되지 않습니다. updateCells()는 parse/validate/readonly 검사를 거쳐 원자적으로 수정합니다. 입력 중인 편집기의 미확정 값은 조회에 포함되지 않으므로 편집을 확정한 뒤 데이터를 조회하세요.
// 조회 복사본 변경은 그리드에 반영되지 않습니다.
// grid의 실제 수정에는 updateCells()를 사용합니다.
grid.updateCells([{ id: 100, field: 'name', value: '변경된 주문' }]);
getSelectedRows() 제안 — 현재 미제공 선택 셀이 하나라도 있는 행을 중복 없이 화면 표시 순서의 복사본으로 반환하고, 선택이 없으면 []를 반환하는 API를 검토한 상태입니다. 일반 checkbox 컬럼 값과는 구분합니다. 이 동작은 향후 추가 시의 제안이며 현재 버전에서 호출할 수 없습니다. 필터·트리/그룹·고정행·선택 해제 조합의 계약과 검증이 필요합니다.
범위 선택·클립보드·Fill 예제 · 변경 데이터 예제 · 데이터 설정·행 추가 예제 · 이벤트 설명
clearRows() — 전체 데이터 비우기 (0.1.2부터) grid.setRows([])와 같은 동작을 명확한 이름으로 제공합니다. 모든 행과 변경 목록·Undo/Redo를 초기화하고 선택을 해제하며 빈 데이터 화면을 표시합니다. 반환값은 undefined이고 datachange는 호출당 한 번 발생합니다.
grid.clearRows(); // grid.setRows([])와 동일
// 새로운 조회 결과로 다시 채우기
grid.setRows(newRows);clearRows()는 새 빈 기준 상태를 설정하므로 삭제 변경으로 기록하지 않고 Undo로 되돌릴 수 없습니다. 서버에 삭제 내용을 저장하려면 deleteRows(ids)를 사용하세요. 진행 중인 조회 요청을 취소하는 함수는 아니므로 늦게 도착한 조회 결과는 별도로 제어해야 합니다.
행 드래그 후 데이터 순서 rowReorder:true이면 getRows()는 드래그 후의 순서를 반환합니다. getChangedRows()는 순서 변경 시에만 order 배열을 추가하며, 현재 적재된 모든 행의 rowKey 값이 변경된 순서대로 들어갑니다. insert/update/delete 배열도 현재 보관 순서대로 반환하지만 order와 함께 처리해야 전체 순서를 복원할 수 있습니다. order는 행 데이터 필드가 아니며 서버가 별도로 저장해야 합니다. acceptChanges·성공 저장 후에는 order를 생략하며 Undo/Redo·원복도 순서를 복원합니다.
이벤트 설명
grid.on(name, handler) 방식으로 구독하면 name과 handler 함수는 필수값 입니다. 생성 옵션의 onclick 등 이벤트 콜백은 선택입니다. handler가 받는 detail·grid는 그리드가 전달하므로 호출자가 직접 넣지 않습니다.
모든 그리드 이벤트는 DOM을 직접 찾지 않고 구독합니다. 생성 옵션의 콜백 또는 grid.on('이벤트명', handler) 중 하나를 사용하세요. 두 방식의 핸들러 인수는 모두 (detail, grid)이며, 데이터도 같습니다. event.detail을 따로 꺼낼 필요가 없습니다.
방식 1 — 생성 옵션 콜백
const grid = new ZixoGrid(host, {
rows: [{ id: 1, name: '첫 주문' }],
columns: [{ field: 'name', title: '주문' }],
onclick: (detail, grid) => console.log(detail.id, detail.field),
onselectionchange: (detail, grid) => console.log(detail.cells),
ondatachange: (detail, grid) => console.log(detail.changes)
});
방식 2 — 생성 후 구독
const handleSelection = (detail, grid) => console.log(detail.cells);
const unsubscribe = grid.on('selectionchange', handleSelection);
// 이 구독이 필요 없어졌을 때 호출합니다.
unsubscribe();
// 같은 함수로 해제할 수도 있습니다.
grid.off('selectionchange', handleSelection);
위 해제 코드는 수명주기 사용법입니다. 구독한 뒤 즉시 해제하면 이후 알림을 받지 않습니다. 실제 실행 예제는 구독을 유지합니다. 여러 리스너를 등록할 수 있으며 같은 이벤트에 같은 함수의 중복 등록은 한 번만 처리합니다. destroy()는 모든 구독을 정리합니다.
일반적으로 한 가지 방식만 선택하세요. 비교 예제는 의도적으로 두 방식을 함께 등록합니다. 현재 호출 순서는 grid.on 리스너 → 생성 옵션 콜백 입니다. 둘 다 같은 업무 로직을 실행하면 중복 처리됩니다. 기존 DOM 리스너가 있으면 그 알림이 먼저 전달됩니다.
이벤트별 실행 예제 확인할 이벤트를 선택한 뒤 그리드 또는 버튼을 조작하세요. 아래 로그에 실제 전달 데이터가 표시됩니다.
행 손잡이를 드래그하거나 버튼으로 첫 행을 둘째 행 뒤로 옮기면 id/target/position/order를 기록합니다. Undo/Redo는 datachange로 확인합니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onrowreorder = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('rowreorder', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · rowreorder.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/rowreorder.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './rowreorder.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rowReorder: true,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('rowreorder', (detail, grid) => log('rowreorder', detail));
button('첫 행 뒤로 이동', () => {
grid.moveRow(1, 2, 'after');
});
report.textContent =
'행 손잡이를 드래그하거나 버튼으로 첫 행을 둘째 행 뒤로 옮기면 id/target/position/order를 기록합니다. Undo/Redo는 datachange로 확인합니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './rowreorder.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rowReorder: true,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('rowreorder', (detail, grid) => log('rowreorder', detail));
button('첫 행 뒤로 이동', () => {
grid.moveRow(1, 2, 'after');
});
message.value =
'행 손잡이를 드래그하거나 버튼으로 첫 행을 둘째 행 뒤로 옮기면 id/target/position/order를 기록합니다. Undo/Redo는 datachange로 확인합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>rowreorder · 행 순서 변경</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
그리드 셀을 한 번 클릭하세요. 비교를 위해 두 방식을 모두 등록했습니다. 클릭 한 번에 [1] grid.on("cellclick"), [2] onclick(생성 옵션) 순서로 같은 셀 데이터를 기록합니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onclick = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('cellclick', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · cellclick.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/cellclick.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './cellclick.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
onclick: (detail, grid) => log('onclick', detail),
};
const grid = new ZixoGrid(host, options);
grid.on('cellclick', (detail, grid) => log('cellclick', detail));
report.textContent =
'그리드 셀을 한 번 클릭하세요. 비교를 위해 두 방식을 모두 등록했습니다. 클릭 한 번에 [1] grid.on("cellclick"), [2] onclick(생성 옵션) 순서로 같은 셀 데이터를 기록합니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './cellclick.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
onclick: (detail, grid) => log('onclick', detail),
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('cellclick', (detail, grid) => log('cellclick', detail));
message.value =
'그리드 셀을 한 번 클릭하세요. 비교를 위해 두 방식을 모두 등록했습니다. 클릭 한 번에 [1] grid.on("cellclick"), [2] onclick(생성 옵션) 순서로 같은 셀 데이터를 기록합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>셀 클릭: onclick 옵션 / grid.on</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
셀 더블클릭: ondblclick 옵션 / grid.on 독립 실행 ↗ 그리드 셀을 더블클릭하세요. 비교를 위해 두 방식을 모두 등록했습니다. grid.on("celldblclick") 다음 ondblclick(생성 옵션)을 기록한 뒤 편집을 시작합니다. 브라우저의 두 번 click도 별도로 발생합니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.ondblclick = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('celldblclick', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · celldblclick.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/celldblclick.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './celldblclick.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
ondblclick: (detail, grid) => log('ondblclick', detail),
};
const grid = new ZixoGrid(host, options);
grid.on('celldblclick', (detail, grid) => log('celldblclick', detail));
report.textContent =
'그리드 셀을 더블클릭하세요. 비교를 위해 두 방식을 모두 등록했습니다. grid.on("celldblclick") 다음 ondblclick(생성 옵션)을 기록한 뒤 편집을 시작합니다. 브라우저의 두 번 click도 별도로 발생합니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './celldblclick.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
ondblclick: (detail, grid) => log('ondblclick', detail),
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('celldblclick', (detail, grid) => log('celldblclick', detail));
message.value =
'그리드 셀을 더블클릭하세요. 비교를 위해 두 방식을 모두 등록했습니다. grid.on("celldblclick") 다음 ondblclick(생성 옵션)을 기록한 뒤 편집을 시작합니다. 브라우저의 두 번 click도 별도로 발생합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>셀 더블클릭: ondblclick 옵션 / grid.on</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
셀을 선택하거나 아래 버튼으로 선택 범위를 변경하세요. 해제하면 cells는 빈 배열입니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onselectionchange = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('selectionchange', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · selectionchange.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/selectionchange.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './selectionchange.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('selectionchange', (detail, grid) => log('selectionchange', detail));
button('첫 셀 선택', () => grid.selectCell(1, 'qty'));
button('전체 선택', () => grid.selectAll());
button('선택 해제', () => grid.clearSelection());
report.textContent =
'셀을 선택하거나 아래 버튼으로 선택 범위를 변경하세요. 해제하면 cells는 빈 배열입니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './selectionchange.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('selectionchange', (detail, grid) => log('selectionchange', detail));
button('첫 셀 선택', () => grid.selectCell(1, 'qty'));
button('전체 선택', () => grid.selectAll());
button('선택 해제', () => grid.clearSelection());
message.value =
'셀을 선택하거나 아래 버튼으로 선택 범위를 변경하세요. 해제하면 cells는 빈 배열입니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>selectionchange · 선택 변경</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
셀 더블클릭 또는 Enter/F2로 편집을 요청하세요. 버튼은 첫 수량 셀을 편집합니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.oneditrequest = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('editrequest', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · editrequest.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/editrequest.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './editrequest.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('editrequest', (detail, grid) => log('editrequest', detail));
button('수량 편집 요청', () => grid.edit(JSON.stringify(1), 'qty'));
report.textContent =
'셀 더블클릭 또는 Enter/F2로 편집을 요청하세요. 버튼은 첫 수량 셀을 편집합니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './editrequest.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('editrequest', (detail, grid) => log('editrequest', detail));
button('수량 편집 요청', () => grid.edit(JSON.stringify(1), 'qty'));
message.value =
'셀 더블클릭 또는 Enter/F2로 편집을 요청하세요. 버튼은 첫 수량 셀을 편집합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>editrequest · 편집 요청</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
수량을 음수로 입력해 확정하거나 검증 실패 버튼을 누르세요. 실패 후 수량은 기존 값으로 유지됩니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onvalidationerror = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('validationerror', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · validationerror.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/validationerror.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './validationerror.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('validationerror', (detail, grid) => log('validationerror', detail));
button('음수 입력 검증 실패', () => {
grid.attempt(() => grid.updateCells([{ id: 1, field: 'qty', value: -1 }]));
});
report.textContent =
'수량을 음수로 입력해 확정하거나 검증 실패 버튼을 누르세요. 실패 후 수량은 기존 값으로 유지됩니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './validationerror.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('validationerror', (detail, grid) => log('validationerror', detail));
button('음수 입력 검증 실패', () => {
grid.attempt(() => grid.updateCells([{ id: 1, field: 'qty', value: -1 }]));
});
message.value =
'수량을 음수로 입력해 확정하거나 검증 실패 버튼을 누르세요. 실패 후 수량은 기존 값으로 유지됩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>validationerror · 입력 검증 실패</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
렌더 실행 또는 마지막 행 이동 버튼을 눌러 가상화된 렌더 범위를 확인하세요.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onrender = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('render', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · render.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/render.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './render.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('render', (detail, grid) => log('render', detail));
button('렌더 실행', () => grid.refresh());
button('마지막 행 이동', () => grid.scrollToRow(100));
report.textContent =
'렌더 실행 또는 마지막 행 이동 버튼을 눌러 가상화된 렌더 범위를 확인하세요.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './render.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('render', (detail, grid) => log('render', detail));
button('렌더 실행', () => grid.refresh());
button('마지막 행 이동', () => grid.scrollToRow(100));
message.value =
'렌더 실행 또는 마지막 행 이동 버튼을 눌러 가상화된 렌더 범위를 확인하세요.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>render · 렌더 완료</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 최소 100행. 대량 데이터는 배열 생성식을 사용합니다.
export const rows = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
name: '행 ' + (i + 1),
qty: i + 1,
}));
조회 실패 버튼은 실제 server.fetch 경로에서 오류를 냅니다. 재시도를 누르면 같은 요청을 성공시킵니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onerror = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('error', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · error.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/error.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './error.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('error', (detail, grid) => log('error', detail));
let fail = true;
button('조회 실패 발생', () => {
fail = true;
return grid.connectServer({
size: 100,
fetch: async (request, { signal }) => {
if (signal.aborted) throw Error('취소됨');
if (fail) {
fail = false;
throw Error('예제 조회 실패');
}
return {
rows: rows.map((row) => ({ ...row, name: '재시도 성공 ' + row.id })),
total: rows.length,
hasMore: false,
};
},
});
});
button('조회 재시도', () => grid.retry());
report.textContent =
'조회 실패 버튼은 실제 server.fetch 경로에서 오류를 냅니다. 재시도를 누르면 같은 요청을 성공시킵니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './error.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('error', (detail, grid) => log('error', detail));
let fail = true;
button('조회 실패 발생', () => {
fail = true;
return grid.connectServer({
size: 100,
fetch: async (request, { signal }) => {
if (signal.aborted) throw Error('취소됨');
if (fail) {
fail = false;
throw Error('예제 조회 실패');
}
return {
rows: rows.map((row) => ({ ...row, name: '재시도 성공 ' + row.id })),
total: rows.length,
hasMore: false,
};
},
});
});
button('조회 재시도', () => grid.retry());
message.value =
'조회 실패 버튼은 실제 server.fetch 경로에서 오류를 냅니다. 재시도를 누르면 같은 요청을 성공시킵니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>error · 조회 실패와 재시도</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
수량을 바꾼 뒤 실패 응답을 반환하는 실제 save 경로를 실행합니다. 이벤트와 Promise 예외를 함께 처리합니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.onsaveerror = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('saveerror', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · saveerror.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/saveerror.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './saveerror.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('saveerror', (detail, grid) => log('saveerror', detail));
button('저장 실패 발생', async () => {
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]);
try {
await grid.save(async () => ({
success: false,
message: '예제 저장 실패',
}));
} catch (error) {
log('save Promise catch', { message: error.message });
}
});
report.textContent =
'수량을 바꾼 뒤 실패 응답을 반환하는 실제 save 경로를 실행합니다. 이벤트와 Promise 예외를 함께 처리합니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './saveerror.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('saveerror', (detail, grid) => log('saveerror', detail));
button('저장 실패 발생', async () => {
grid.updateCells([{ id: 1, field: 'qty', value: 9 }]);
try {
await grid.save(async () => ({
success: false,
message: '예제 저장 실패',
}));
} catch (error) {
log('save Promise catch', { message: error.message });
}
});
message.value =
'수량을 바꾼 뒤 실패 응답을 반환하는 실제 save 경로를 실행합니다. 이벤트와 Promise 예외를 함께 처리합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>saveerror · 저장 실패</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
수정·Undo·Redo·원복 버튼으로 모델 변경 알림과 변경 목록을 확인하세요. grid.on("datachange")로 다른 이벤트와 같은 방식으로 구독합니다.
grid.on 방식으로 이벤트를 구독할 때 이벤트명과 handler 함수는 필수값 입니다. 생성 옵션 콜백은 선택값이며 두 방식을 모두 등록할 필요는 없습니다. 이 이벤트는 생성 전에 options.ondatachange = (detail, grid) => console.log(detail)로 콜백을 지정하거나 생성 후 grid.on('datachange', handler)로 구독합니다. 두 방식은 동일한 detail과 grid를 전달하며, 둘 다 등록하면 각각 호출됩니다.
행 데이터: 100행 · modelchange.data.js · JavaScript와 Vue 3 공통 데이터
실행: JavaScript · examples/events/modelchange.js
실행 중인 예제 소스 선택 소스 복사 JavaScript Vue 3 행 데이터
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './modelchange.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button } = createToolbar(toolbar, report);
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
report.textContent = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
grid.on('datachange', (detail, grid) => log('datachange', detail));
button('수량 수정', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 7 }]),
);
button('Undo', () => {
grid.undo();
});
button('Redo', () => {
grid.redo();
});
button('원복', () => grid.revertChanges());
report.textContent =
'수정·Undo·Redo·원복 버튼으로 모델 변경 알림과 변경 목록을 확인하세요. grid.on("datachange")로 다른 이벤트와 같은 방식으로 구독합니다.';
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './modelchange.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
onMounted(() => {
let sequence = 0;
const records = [];
function log(name, detail) {
const text = JSON.stringify(
detail,
(key, value) =>
key === 'originalEvent'
? { type: value.type, button: value.button }
: value instanceof Error
? { message: value.message }
: value,
2,
);
records.push('[' + ++sequence + '] ' + name + '\n' + text);
message.value = records.slice(-8).join('\n\n');
}
const options = {
rowKey: 'id',
height: 320,
rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (value) =>
(Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on('datachange', (detail, grid) => log('datachange', detail));
button('수량 수정', () =>
grid.updateCells([{ id: 1, field: 'qty', value: 7 }]),
);
button('Undo', () => {
grid.undo();
});
button('Redo', () => {
grid.redo();
});
button('원복', () => grid.revertChanges());
message.value =
'수정·Undo·Redo·원복 버튼으로 모델 변경 알림과 변경 목록을 확인하세요. grid.on("datachange")로 다른 이벤트와 같은 방식으로 구독합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>datachange · 데이터 변경</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
// 이 예제의 행 데이터 100개. 행 키는 고유하며 원본 예시 값을 유지합니다.
export const rows = [
{
id: 1,
name: '첫 주문',
qty: 2,
},
{
id: 2,
name: '둘째 주문',
qty: 3,
},
{
id: 3,
name: '첫 주문 3',
qty: 2,
},
{
id: 4,
name: '둘째 주문 4',
qty: 3,
},
{
id: 5,
name: '첫 주문 5',
qty: 2,
},
{
id: 6,
name: '둘째 주문 6',
qty: 3,
},
{
id: 7,
name: '첫 주문 7',
qty: 2,
},
{
id: 8,
name: '둘째 주문 8',
qty: 3,
},
{
id: 9,
name: '첫 주문 9',
qty: 2,
},
{
id: 10,
name: '둘째 주문 10',
qty: 3,
},
{
id: 11,
name: '첫 주문 11',
qty: 2,
},
{
id: 12,
name: '둘째 주문 12',
qty: 3,
},
{
id: 13,
name: '첫 주문 13',
qty: 2,
},
{
id: 14,
name: '둘째 주문 14',
qty: 3,
},
{
id: 15,
name: '첫 주문 15',
qty: 2,
},
{
id: 16,
name: '둘째 주문 16',
qty: 3,
},
{
id: 17,
name: '첫 주문 17',
qty: 2,
},
{
id: 18,
name: '둘째 주문 18',
qty: 3,
},
{
id: 19,
name: '첫 주문 19',
qty: 2,
},
{
id: 20,
name: '둘째 주문 20',
qty: 3,
},
{
id: 21,
name: '첫 주문 21',
qty: 2,
},
{
id: 22,
name: '둘째 주문 22',
qty: 3,
},
{
id: 23,
name: '첫 주문 23',
qty: 2,
},
{
id: 24,
name: '둘째 주문 24',
qty: 3,
},
{
id: 25,
name: '첫 주문 25',
qty: 2,
},
{
id: 26,
name: '둘째 주문 26',
qty: 3,
},
{
id: 27,
name: '첫 주문 27',
qty: 2,
},
{
id: 28,
name: '둘째 주문 28',
qty: 3,
},
{
id: 29,
name: '첫 주문 29',
qty: 2,
},
{
id: 30,
name: '둘째 주문 30',
qty: 3,
},
{
id: 31,
name: '첫 주문 31',
qty: 2,
},
{
id: 32,
name: '둘째 주문 32',
qty: 3,
},
{
id: 33,
name: '첫 주문 33',
qty: 2,
},
{
id: 34,
name: '둘째 주문 34',
qty: 3,
},
{
id: 35,
name: '첫 주문 35',
qty: 2,
},
{
id: 36,
name: '둘째 주문 36',
qty: 3,
},
{
id: 37,
name: '첫 주문 37',
qty: 2,
},
{
id: 38,
name: '둘째 주문 38',
qty: 3,
},
{
id: 39,
name: '첫 주문 39',
qty: 2,
},
{
id: 40,
name: '둘째 주문 40',
qty: 3,
},
{
id: 41,
name: '첫 주문 41',
qty: 2,
},
{
id: 42,
name: '둘째 주문 42',
qty: 3,
},
{
id: 43,
name: '첫 주문 43',
qty: 2,
},
{
id: 44,
name: '둘째 주문 44',
qty: 3,
},
{
id: 45,
name: '첫 주문 45',
qty: 2,
},
{
id: 46,
name: '둘째 주문 46',
qty: 3,
},
{
id: 47,
name: '첫 주문 47',
qty: 2,
},
{
id: 48,
name: '둘째 주문 48',
qty: 3,
},
{
id: 49,
name: '첫 주문 49',
qty: 2,
},
{
id: 50,
name: '둘째 주문 50',
qty: 3,
},
{
id: 51,
name: '첫 주문 51',
qty: 2,
},
{
id: 52,
name: '둘째 주문 52',
qty: 3,
},
{
id: 53,
name: '첫 주문 53',
qty: 2,
},
{
id: 54,
name: '둘째 주문 54',
qty: 3,
},
{
id: 55,
name: '첫 주문 55',
qty: 2,
},
{
id: 56,
name: '둘째 주문 56',
qty: 3,
},
{
id: 57,
name: '첫 주문 57',
qty: 2,
},
{
id: 58,
name: '둘째 주문 58',
qty: 3,
},
{
id: 59,
name: '첫 주문 59',
qty: 2,
},
{
id: 60,
name: '둘째 주문 60',
qty: 3,
},
{
id: 61,
name: '첫 주문 61',
qty: 2,
},
{
id: 62,
name: '둘째 주문 62',
qty: 3,
},
{
id: 63,
name: '첫 주문 63',
qty: 2,
},
{
id: 64,
name: '둘째 주문 64',
qty: 3,
},
{
id: 65,
name: '첫 주문 65',
qty: 2,
},
{
id: 66,
name: '둘째 주문 66',
qty: 3,
},
{
id: 67,
name: '첫 주문 67',
qty: 2,
},
{
id: 68,
name: '둘째 주문 68',
qty: 3,
},
{
id: 69,
name: '첫 주문 69',
qty: 2,
},
{
id: 70,
name: '둘째 주문 70',
qty: 3,
},
{
id: 71,
name: '첫 주문 71',
qty: 2,
},
{
id: 72,
name: '둘째 주문 72',
qty: 3,
},
{
id: 73,
name: '첫 주문 73',
qty: 2,
},
{
id: 74,
name: '둘째 주문 74',
qty: 3,
},
{
id: 75,
name: '첫 주문 75',
qty: 2,
},
{
id: 76,
name: '둘째 주문 76',
qty: 3,
},
{
id: 77,
name: '첫 주문 77',
qty: 2,
},
{
id: 78,
name: '둘째 주문 78',
qty: 3,
},
{
id: 79,
name: '첫 주문 79',
qty: 2,
},
{
id: 80,
name: '둘째 주문 80',
qty: 3,
},
{
id: 81,
name: '첫 주문 81',
qty: 2,
},
{
id: 82,
name: '둘째 주문 82',
qty: 3,
},
{
id: 83,
name: '첫 주문 83',
qty: 2,
},
{
id: 84,
name: '둘째 주문 84',
qty: 3,
},
{
id: 85,
name: '첫 주문 85',
qty: 2,
},
{
id: 86,
name: '둘째 주문 86',
qty: 3,
},
{
id: 87,
name: '첫 주문 87',
qty: 2,
},
{
id: 88,
name: '둘째 주문 88',
qty: 3,
},
{
id: 89,
name: '첫 주문 89',
qty: 2,
},
{
id: 90,
name: '둘째 주문 90',
qty: 3,
},
{
id: 91,
name: '첫 주문 91',
qty: 2,
},
{
id: 92,
name: '둘째 주문 92',
qty: 3,
},
{
id: 93,
name: '첫 주문 93',
qty: 2,
},
{
id: 94,
name: '둘째 주문 94',
qty: 3,
},
{
id: 95,
name: '첫 주문 95',
qty: 2,
},
{
id: 96,
name: '둘째 주문 96',
qty: 3,
},
{
id: 97,
name: '첫 주문 97',
qty: 2,
},
{
id: 98,
name: '둘째 주문 98',
qty: 3,
},
{
id: 99,
name: '첫 주문 99',
qty: 2,
},
{
id: 100,
name: '둘째 주문 100',
qty: 3,
},
];
공통 이벤트 API 10개 grid.on 이벤트명 생성 옵션 콜백 detail 발생 시점 · 주의
rowreorder onrowreorder {id,target,position,order} 드래그·Alt+방향키·moveRow로 순서를 바꾼 직후입니다. order는 변경 후 적재된 행 키 순서의 복사본입니다. Undo/Redo는 datachange로 확인합니다.
cellclick onclick {id,field,value,row,originalEvent} 데이터 셀 클릭. 컨트롤 클릭 전 모델 값을 전달합니다. 헤더/빈 영역/다른 하위 그리드 제외. row는 복사본, originalEvent는 원본 MouseEvent입니다.
celldblclick ondblclick {id,field,value,row,originalEvent} 셀 더블클릭 알림 후 기본 편집 요청을 처리합니다. 컨트롤 자체는 알림만 발생합니다. 브라우저의 click 두 번도 별도로 발생할 수 있습니다.
selectionchange onselectionchange {cells:[{id,field},...]} 셀/범위/전체 선택·선택 해제. 해제 시 빈 배열. 통합 grid.on은 브라우저 기본 selectionchange를 받지 않습니다.
editrequest oneditrequest {id,field,initialValue?} Enter/F2/더블클릭/문자 입력 또는 edit 호출. 요청 알림이며 편집 성공/확정을 의미하지 않습니다. 실제 편집은 editorsPlugin이 담당합니다.
validationerror onvalidationerror {error} 셀 확정 검증 실패 및 attempt를 통한 UI 실패. error.message를 읽습니다. 직접 updateCells 호출에서 난 예외는 호출자가 try/catch합니다.
render onrender {start,end,rendered} 렌더 완료. end는 제외 경계이며 rendered는 실제 canvas 노드 수입니다. 생성 중 초기 렌더는 onrender 옵션으로 받을 수 있습니다. 후등록 grid.on은 등록 이후만 받습니다.
error onerror {error,request?} 서버 조회 및 Detail lazy 실패. 서버 request는 {page,size,sort,filters,filterTree,search}. Tree lazy 실패는 현재 상태 표시로 처리합니다. 취소/종료로 폐기한 서버 요청 제외.
saveerror onsaveerror {error,changes} 저장 실패. changes는 {insert,update,delete,order?}. save Promise도 reject되므로 await에 catch가 필요합니다. 저장 중 재호출 거부와 종료/취소는 이 알림 대상이 아닙니다.
datachange ondatachange {revision,changes} 모델 변경 후 알림. 수정·CRUD·붙여넣기·Undo/Redo·setRows·append·원복·변경 확정·실패 상태 복원도 포함합니다. 개별 셀 편집 완료 전용이 아닙니다. 전체 행은 두 번째 인수 grid.getRows()로 읽습니다.
핸들러 반환값으로 편집/저장을 취소하지 않습니다. render 안에서 무조건 refresh를 호출하면 반복 렌더가 생깁니다. 초기 생성 옵션 콜백에서 외부 변수 grid를 참조하면 아직 대입되기 전일 수 있으므로 두 번째 인수의 인스턴스를 사용하세요. 구독/옵션 핸들러가 던진 예외는 브라우저 오류로 보고하며 그리드의 변경 작업을 중단시키지 않습니다.
JavaScript — 등록부터 해제까지
#grid 요소와 src/zixo.css가 필요합니다. 아래 소스는 DOM 이벤트 대신 통합 API로 구독합니다.
import { ZixoGrid, editorsPlugin } from '../src/index.js';
const grid = new ZixoGrid(document.querySelector('#grid'), {
rows: [{id: 1, qty: 2}],
columns: [{field: 'qty', title: '수량', parse: Number,
validate: value => (Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요'}],
plugins: [editorsPlugin()]
});
const subscriptions = [
grid.on('cellclick', detail => console.log('셀 클릭', detail)),
grid.on('celldblclick', detail => console.log('셀 더블클릭', detail)),
grid.on('selectionchange', detail => console.log('선택', detail.cells)),
grid.on('editrequest', detail => console.log('편집 요청', detail)),
grid.on('validationerror', detail => console.log('입력 오류', detail.error.message)),
grid.on('render', detail => console.log('렌더 범위', detail)),
grid.on('error', detail => console.log('조회 오류', detail.error.message, detail.request)),
grid.on('saveerror', detail => console.log('저장 오류', detail.error.message, detail.changes)),
grid.on('datachange', detail => console.log('변경 목록', detail.changes))
];
function dispose() {
subscriptions.forEach(unsubscribe => unsubscribe());
grid.destroy();
}
window.addEventListener('pagehide', dispose, {once: true});
Vue 3 — 부모 컴포넌트로 전달
Vue defineEmits로 부모에게 전달할 이름을 지정합니다. DOM 객체·CustomEvent 변환이나 AbortController가 필요하지 않습니다.
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../src/index.js';
const host = ref(null);
const status = ref('셀을 선택하거나 수량을 편집하세요.');
const emit = defineEmits(['selectionchange', 'datachange']);
let grid;
onMounted(() => {
grid = new ZixoGrid(host.value, {
rows: [{id: 1, qty: 2}],
columns: [{field: 'qty', title: '수량', parse: Number,
validate: value => (Number.isFinite(value) && value >= 0) || '0 이상 숫자를 입력하세요'}],
plugins: [editorsPlugin()],
onselectionchange: (detail, instance) => {
emit('selectionchange', detail.cells);
status.value = `${detail.cells.length}개 셀 선택`;
},
onvalidationerror: detail => { status.value = detail.error.message; },
ondatachange: detail => emit('datachange', detail.changes)
});
});
onBeforeUnmount(() => grid?.destroy());
</script>
<template>
<div ref="host"></div>
<p role="status">{{ status }}</p>
</template>
부모에서 <OrderGrid @selectionchange="handleSelection" @datachange="handleChanges" />로 수신합니다.
기존 코드 호환
기존8개 CustomEvent와 grid.on(node, name, handler, options) 형태는 계속 지원합니다. 기존 model.subscribe도 사용할 수 있습니다. 새 코드는 통합 API를 사용하세요. datachange는 통합 구독/옵션 콜백으로 제공합니다. DOM 클릭 속성 grid.root.onclick과 생성 옵션 onclick은 다른 것입니다.
플러그인 설명
Core에 필요한 기능만 추가하는 모듈입니다. src/index.js에서 import한 뒤 options.plugins 배열 또는 grid.install(plugin)으로 등록합니다. 옵션에 tree 등의 설정을 지정하는 것만으로 플러그인이 설치되지는 않습니다.
설치 · 해제 · 수명주기
import { ZixoGrid, editorsPlugin, columnToolsPlugin } from '../src/index.js';
const grid = new ZixoGrid(host, {
columns, rows,
plugins: [editorsPlugin()]
});
grid.install(columnToolsPlugin());
grid.uninstall('columnTools');
grid.destroy(); // 남은 플러그인 정리, 이벤트/Observer/요청 해제
같은 name의 중복 설치는 거부됩니다. tree와 grouping은 동일 인스턴스에 함께 설치할 수 없습니다. Vue 3에서는 onMounted에서 생성하고 onBeforeUnmount에서 destroy를 호출합니다.
공개 플러그인 8개 생성 함수 / name 역할 설정 · 사용 조건
editorsPlugin() / editors 셀 내부 text/date/select/combo 입력과 명시적 popup 편집 함수 인수 없음. 컬럼의 editor, options, parse, validate, loadOptions, debounce, popup으로 구성합니다. date는 referenceYear를 사용합니다. Enter/Tab 확정·Escape 취소·검증 오류를 처리합니다.
columnToolsPlugin() / columnTools 헤더 ⋮ 메뉴, 컬럼 표시/고정/너비 맞춤, 필터 조건 빌더 함수 인수 없음. sortable/filterable 및 표시 권한을 검사합니다. openColumns(), openFilterBuilder()를 제공합니다. GUI 조건은 평면 AND/OR입니다.
treePlugin(config) / tree 부모·자식 트리, 연결선, 조상 포함 검색 parentField 기본 parentId, field 기본 첫 컬럼, showLines 기본 false. defaultExpanded 기본 false: true는 적재된 부모 전체 펼침, false는 접힘. expanded 키 배열을 지정하면 defaultExpanded보다 우선하며 []는 모두 접힘. 미조회 lazy 자식은 자동 조회하지 않습니다. hasChildrenField 기본 hasChildren. loadChildren(row,{signal})은 자식 배열을 반환합니다. config가 options.tree보다 우선합니다.
groupingPlugin(config) / grouping 다단 그룹과 그룹 집계 fields: 그룹 필드 배열(기본 []), aggregates: {field,type?,label?} 배열(기본 []). type은 sum/avg/min/max(기본 sum). 그룹 버튼으로 접기/펼치기 합니다.
detailPlugin(config) / detail Master/Detail 하위 그리드 field: 상세 버튼 컬럼, columns: 하위 컬럼, rows(row): 로컬 하위 배열 또는 loadRows(row,{signal}): 비동기 하위 배열. 상세를 접거나 플러그인을 해제하면 하위 인스턴스를 정리합니다.
contextMenuPlugin(config) / contextMenu 셀 우클릭 메뉴 items: {label,action(grid,active)} 배열. 생략 시 원복·Undo 메뉴. active는 {id,field}입니다. 방향키·Escape와 화면 경계 처리를 제공합니다.
columnGroupsPlugin(config) / columnGroups 다단 컬럼 그룹 헤더 groups: {title,fields} 또는 {title,children} 배열. 생략 시 []. fields는 연속 컬럼 범위를 지정합니다. 그룹 경계를 넘는 이동·고정 조합에는 제한이 있습니다.
exportPlugin() / export CSV/XLSX 내보내기, CSV 신규 행 가져오기, 인쇄 함수 인수 없음. exportCSV({downloadFile}), exportXLSX({downloadFile}), importCSV(text), print(), getPrintHTML()을 제공합니다. XLSX Import·복수 시트·고급 서식은 지원하지 않습니다. 현재 적재된 행을 사용합니다.
설정 예
// 트리와 lazy 자식 조회
treePlugin({
field: 'name', parentField: 'parentId', showLines: true, defaultExpanded: true,
loadChildren: async (row, {signal}) => {
const response = await fetch(`/api/tree/${row.id}`, {signal});
if (!response.ok) throw Error('자식 조회 실패');
return response.json();
}
});
// 그룹 집계
groupingPlugin({fields: ['region'], aggregates: [{field: 'amount', type: 'sum', label: '합계'}]});
// 상세 조회
detailPlugin({field: 'detail', columns: [{field: 'name', title: '항목'}], rows: row => row.items});
// 사용자 우클릭 메뉴
contextMenuPlugin({items: [{label: '변경 원복', action: grid => grid.revertChanges()}]});
// 다단 헤더
columnGroupsPlugin({groups: [{title: '주문', fields: ['name', 'amount']}]});
사용자 플러그인 계약
const plugin = {
name: 'selectionLogger',
install(grid) {
const handler = detail => console.log(detail.cells);
return grid.on('selectionchange', handler); // 반환 함수가 플러그인 해제 시 구독을 정리합니다.
}
};
grid.install(plugin);
grid.uninstall('selectionLogger');
install(grid)은 설치 로직과 정리 함수 반환을 담당합니다. 이벤트·Observer·타이머·진행 중 요청은 정리 함수에서 해제하세요. API 상세 동작은 해당 기능 메뉴에서 확인할 수 있습니다.
셀 편집 · editorsPlugin 셀 안에서 값을 편집하고 확정·취소·입력 검증을 처리합니다. 날짜·선택·자동완성은 컬럼 설정으로 선택합니다.
설치 함수: editorsPlugin() · 해제 이름: editors
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 함수 인수없음 editorsPlugin()에는 별도 설정 객체를 전달하지 않습니다. column.editortext text/date/select/combo를 선택합니다. radio·checkbox·button의 상시 셀 컨트롤과 type:image/type:button 표시는 Core에서 제공합니다. column.parse / validate없음 parse 후 validate를 실행합니다. validate는 true/undefined 성공, false/문자열 실패이며 동기 함수입니다. column.options[] select 선택지 배열 또는 row를 받는 함수. 원시 값 또는 {value,label} 항목이며 원래 값 타입을 유지합니다. column.dateFormatlocale 표시 날짜 표시·입력 형식. YYYY/yyyy, MM/M, DD/dd/D/d를 각각 한 번 사용합니다. 저장은 YYYY-MM-DD입니다. referenceYear편집 시 현재 연도 그리드 옵션입니다. 918/0918 같은 날짜 단축 입력의 기준 연도를 지정합니다. column.loadOptions없음 loadOptions(query,row,{signal})이 선택지 배열을 반환합니다. debounce 기본 180ms이며 이전 요청을 취소합니다.
사용 순서 editorsPlugin()을 plugins에 등록하고 편집 가능한 컬럼을 선언합니다. Enter/F2/더블클릭 또는 문자 입력으로 편집을 시작합니다. select는 셀에서 직접 선택합니다. Enter/Tab 또는 다른 셀 클릭으로 확정하고 Escape로 취소합니다. 검증에 실패하면 이전 값을 유지합니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.edit(JSON.stringify(id), field, initialValue?)편집 요청. id 인수는 현재 API에서 JSON으로 인코딩한 행 키입니다. 필수값 grid.edit 인수: id, field
grid.closeEditor()편집 UI와 연결된 요청·팝업을 정리합니다. 값 확정 용도가 아닙니다.
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(editorsPlugin());
grid.uninstall('editors'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());해제 시 편집 UI·비동기 요청을 종료하고 editrequest 리스너와 렌더 래핑을 정리합니다. Vue에서는 onBeforeUnmount에서 grid.destroy()를 호출합니다.
관련 이벤트 editrequest validationerror datachange 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/inline-edit.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · inline-edit.js import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './inline-edit.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (v) =>
(Number.isFinite(v) && v >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'Enter/F2/더블클릭으로 셀을 직접 편집합니다. Enter/Tab 확정, Escape 취소. parse 후 validate를 실행합니다.';
Vue 3 전체 소스 · vue-inline-edit.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, editorsPlugin } from '../../src/index.js';
import { rows } from './inline-edit.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{
field: 'qty',
title: '수량',
parse: Number,
validate: (v) =>
(Number.isFinite(v) && v >= 0) || '0 이상 숫자를 입력하세요',
},
],
plugins: [editorsPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'Enter/F2/더블클릭으로 셀을 직접 편집합니다. Enter/Tab 확정, Escape 취소. parse 후 validate를 실행합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>셀 내부 편집 · 검증</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 parse/validate는 동기입니다. 비동기 validate는 지원하지 않습니다. readonly/permissions는 화면 편집 제한이며 서버 권한 검증을 대신하지 않습니다. 인라인 편집은 text/number/date/combo에 사용합니다.
연결 기능 메뉴 셀 내부 편집 · 검증 · 원클릭 select · radio · checkbox · 날짜 입력 · 달력 · 비동기 자동완성 · 조건부 readonly · 이미지 · 텍스트 · 버튼 컬럼
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
컬럼 도구 · columnToolsPlugin 헤더 메뉴의 컬럼 표시·필터와 조건 빌더를 제공합니다. 정렬·고정·너비 맞춤은 연결된 Core API에서 설정합니다.
설치 함수: columnToolsPlugin() · 해제 이름: columnTools
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 함수 인수없음 columnToolsPlugin()으로 설치합니다. options.columnMenu / column.columnMenu['columns','filter'] 영역과 순서를 지정합니다. columns=컬럼 표시 설정, filter=컬럼 필터. 컬럼 값이 그리드 값을 대체하며 []는 해당 헤더 메뉴를 숨깁니다. filterable:false이면 필터 영역을 제외합니다. 이전 sort 항목은 호환을 위해 허용하지만 표시하지 않습니다. column.sortabletrue 정렬 허용 여부. 정렬 실행은 Core의 설정 검사를 따릅니다. column.filterabletrue false인 컬럼은 필터 메뉴·조건 빌더에서 제외합니다. permissions.view없음 표시 권한이 없는 컬럼은 컬럼 관리·필터 필드 목록에서 제외합니다. filterTree / filters없음 그리드의 조건 트리 또는 컬럼별 조건 배열을 사용합니다. 조건은 원래 데이터 타입을 기준으로 비교합니다.
사용 순서 헤더의 ⋮ 또는 우클릭으로 간단한 메뉴를 엽니다. 컬럼 표시 설정 하위 창과 값 검색·전체 선택·체크 목록을 제공합니다. 검색 중 적용하면 검색 결과에서 체크한 값만 반영하며 아무 값도 선택하지 않으면 0행입니다. 초기화는 해당 컬럼 필터만 해제합니다. 체크 목록은 적재한 데이터 기준이며 200개씩 더 볼 수 있습니다. 서버 전체의 고유 값 목록을 자동 조회하지 않습니다. 컬럼 표시 설정을 누르면 표시 컬럼 관리와 같은 컬럼 검색·체크 목록이 바로 나타납니다. 체크 변경은 즉시 반영하며 마지막 표시 컬럼 숨김은 거부하고 권한이 없는 컬럼은 제외합니다. 텍스트/숫자/날짜 필터 하위 창에서 두 조건과 AND/OR를 지정할 수 있습니다. 조건 적용은 해당 컬럼의 값 목록 필터를 교체하며 다른 컬럼·전역 필터는 유지합니다. Enter로 적용, Escape로 하위 창부터 닫고 바깥 클릭으로 전체를 닫습니다. 조건 빌더에서 AND/OR와 필드·연산·값을 지정한 뒤 적용합니다. range/in 값은 쉼표로 나눕니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.openColumnMenu(field, anchor?)지정 헤더 컬럼의 메뉴를 엽니다. 필수값 grid.openColumnMenu 인수: field
grid.openColumns(anchor?)표시 컬럼 관리 창을 엽니다. grid.openFilterBuilder(anchor?)평면 AND/OR 조건 빌더를 엽니다. grid.setFilters(filters) / grid.setFilter(tree)조건을 API로 설정합니다. Core API이며 플러그인 해제 뒤에도 사용할 수 있습니다. 필수값 grid.setFilters 인수: filters
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(columnToolsPlugin());
grid.uninstall('columnTools'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());열린 도구 창·ResizeObserver·헤더 및 외부 클릭 리스너를 제거하고 설치 전의 openColumnMenu/openColumns/openFilterBuilder를 복원합니다.
관련 이벤트 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/column-tools.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · column-tools.js import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './column-tools.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
columnMenu: ['columns', 'filter'],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('컬럼 표시 설정', () => grid.openColumns());
select(
'전체 헤더 메뉴',
['전체', '컬럼 표시 설정만', '필터만', '메뉴 없음'],
(value) => {
const choices = {
'컬럼 표시 설정만': ['columns'],
필터만: ['filter'],
전체: ['columns', 'filter'],
'메뉴 없음': [],
};
grid.options.columnMenu = choices[value];
for (const column of grid.columns) delete column.columnMenu;
grid.refresh();
},
);
report.textContent =
'columnMenu 배열로 헤더 메뉴에 넣을 영역과 순서를 지정합니다. columns=컬럼 표시 설정, filter=컬럼 필터이며 기본값은 전체입니다. 컬럼 배열은 그리드 배열을 대체하며 []는 메뉴를 숨깁니다. 이 예제의 전체 헤더 메뉴 선택은 주문·수량·단가의 모든 헤더에 적용합니다. 선택하면 컬럼별 columnMenu를 제거하고 그리드 기본 구성을 함께 상속합니다. 초기 선택은 전체이며 세 컬럼의 실제 메뉴와 일치합니다. 필터/정렬 상태와 API는 메뉴를 숨겨도 유지합니다. 컬럼 표시 설정은 표시 컬럼 관리와 같은 컬럼 검색·체크 목록을 하위 창으로 제공합니다. 목록의 체크박스·행 간격·색상·검색창·스크롤 스타일은 필터 값 목록과 같습니다. 체크하면 즉시 표시하고 해제하면 즉시 숨깁니다. 마지막 표시 컬럼은 숨길 수 없으며 권한이 없는 컬럼은 목록에서 제외합니다. 메뉴에는 정렬 항목을 제공하지 않습니다. 필터는 값 검색·모두 선택·체크 목록과 적용/초기화 버튼으로 구성합니다. 검색 중 적용하면 검색 결과에서 체크한 값만 반영합니다. 텍스트/숫자/날짜 필터를 누르면 두 조건을 AND/OR로 결합하는 하위 창이 열립니다. 각 적용은 해당 컬럼의 기존 필터만 교체하며 다른 컬럼·전역 조건은 유지합니다. Escape로 하위 창부터 닫고, 바깥 클릭은 전체를 닫습니다. 목록은 적재한 데이터 기준이며 200개씩 더 볼 수 있습니다. 서버 전체 데이터의 조건 조회는 조건 필터를 사용합니다.';
Vue 3 전체 소스 · vue-column-tools.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, columnToolsPlugin } from '../../src/index.js';
import { rows } from './column-tools.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [columnToolsPlugin()],
columnMenu: ['columns', 'filter'],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('컬럼 표시 설정', () => grid.openColumns());
select(
'전체 헤더 메뉴',
['전체', '컬럼 표시 설정만', '필터만', '메뉴 없음'],
(value) => {
const choices = {
'컬럼 표시 설정만': ['columns'],
필터만: ['filter'],
전체: ['columns', 'filter'],
'메뉴 없음': [],
};
grid.options.columnMenu = choices[value];
for (const column of grid.columns) delete column.columnMenu;
grid.refresh();
},
);
message.value =
'columnMenu 배열로 헤더 메뉴에 넣을 영역과 순서를 지정합니다. columns=컬럼 표시 설정, filter=컬럼 필터이며 기본값은 전체입니다. 컬럼 배열은 그리드 배열을 대체하며 []는 메뉴를 숨깁니다. 이 예제의 전체 헤더 메뉴 선택은 주문·수량·단가의 모든 헤더에 적용합니다. 선택하면 컬럼별 columnMenu를 제거하고 그리드 기본 구성을 함께 상속합니다. 초기 선택은 전체이며 세 컬럼의 실제 메뉴와 일치합니다. 필터/정렬 상태와 API는 메뉴를 숨겨도 유지합니다. 컬럼 표시 설정은 표시 컬럼 관리와 같은 컬럼 검색·체크 목록을 하위 창으로 제공합니다. 목록의 체크박스·행 간격·색상·검색창·스크롤 스타일은 필터 값 목록과 같습니다. 체크하면 즉시 표시하고 해제하면 즉시 숨깁니다. 마지막 표시 컬럼은 숨길 수 없으며 권한이 없는 컬럼은 목록에서 제외합니다. 메뉴에는 정렬 항목을 제공하지 않습니다. 필터는 값 검색·모두 선택·체크 목록과 적용/초기화 버튼으로 구성합니다. 검색 중 적용하면 검색 결과에서 체크한 값만 반영합니다. 텍스트/숫자/날짜 필터를 누르면 두 조건을 AND/OR로 결합하는 하위 창이 열립니다. 각 적용은 해당 컬럼의 기존 필터만 교체하며 다른 컬럼·전역 조건은 유지합니다. Escape로 하위 창부터 닫고, 바깥 클릭은 전체를 닫습니다. 목록은 적재한 데이터 기준이며 200개씩 더 볼 수 있습니다. 서버 전체 데이터의 조건 조회는 조건 필터를 사용합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>컬럼 메뉴 · 표시 · 필터</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 GUI 조건 빌더는 평면 AND/OR이고 최대 20개 조건을 입력합니다. 중첩 API 조건을 GUI에서 적용하면 새 평면 조건으로 대체됩니다. 숫자 필터는 숫자를 입력해야 하며 마지막 표시 컬럼은 숨길 수 없습니다. 미저장 변경이 있는 서버 조회의 조건 변경은 Core 계약에 따라 거부될 수 있습니다.
연결 기능 메뉴 컬럼 메뉴 · 표시 · 필터 · 컬럼 필터 · AND/OR 조건 · 정렬 · 너비 · 순서 · 높이 · 레이아웃 저장 · 복원
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
트리 · treePlugin 부모 키를 가진 평탄한 행 배열을 계층으로 표시합니다. 5단계 예제에서 연결선·초기 펼침·접기·말단 검색 시 조상 유지를 확인합니다.
설치 함수: treePlugin(config) · 해제 이름: tree
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 field첫 컬럼 트리 들여쓰기와 펼침 버튼을 표시할 컬럼 field입니다. parentFieldparentId 각 행의 부모 rowKey가 들어 있는 데이터 필드입니다. 루트는 null 또는 부모 없음입니다. showLinesfalse 들여쓰기 영역에 계층 연결선을 표시합니다. defaultExpandedfalse true는 이미 적재된 부모를 처음 펼치고 false는 접습니다. 이후 사용자가 접은 상태를 유지합니다. expanded생략 처음 펼칠 행 키 배열. 지정하면 defaultExpanded보다 우선합니다. []는 처음 모두 접힘입니다. hasChildrenFieldhasChildren 자식이 아직 적재되지 않았더라도 이 필드가 true이면 펼침 버튼을 표시합니다. loadChildren없음 loadChildren(row,{signal})이 부모 키를 포함하는 자식 행 배열을 반환합니다. 처음 수동으로 펼칠 때 조회합니다.
사용 순서 모든 행에 고유 rowKey를 넣고 부모 행 키를 parentField에 지정합니다. treePlugin(config) 또는 plugins:[treePlugin()]과 options.tree로 구성합니다. 같은 속성은 플러그인 config가 우선합니다. 초기 상태는 defaultExpanded 또는 expanded로 정합니다. lazy 행은 hasChildrenField와 loadChildren을 함께 구성합니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.toggleTree(id)해당 행의 펼침 상태를 전환합니다. lazy 조회를 포함하며 Promise를 반환합니다. 필수값 grid.toggleTree 인수: id
grid.setTreeLines(boolean)연결선을 켜거나 끄고 다시 표시합니다. 필수값 grid.setTreeLines 인수: boolean
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(treePlugin({ field: 'name', showLines: true, defaultExpanded: true }));
grid.uninstall('tree'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());미완료 자식 조회를 취소하고 트리 변환·셀 렌더러를 제거합니다. role을 grid로 복원하고 설치 전의 toggleTree/setTreeLines를 복원합니다.
관련 이벤트 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/tree.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · tree.js import { ZixoGrid, treePlugin } from '../../src/index.js';
import { rows } from './tree.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '업무', width: 360 }],
plugins: [
treePlugin({ field: 'name', showLines: true, defaultExpanded: true }),
],
};
let grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
select('처음 표시', ['펼침', '접힘'], (value) => {
grid.destroy();
grid = new ZixoGrid(host, {
...options,
plugins: [
treePlugin({
field: 'name',
showLines: true,
defaultExpanded: value === '펼침',
}),
],
});
});
button('연결선 켜기', () => grid.setTreeLines(true));
button('연결선 끄기', () => grid.setTreeLines(false));
button('5단계 검색', () => grid.setSearch('5단계'));
button('검색 해제', () => grid.setSearch(''));
report.textContent =
'100행을 5단계 계층으로 구성합니다. defaultExpanded: true는 처음 펼침, false는 처음 접힘이며 기본값은 false입니다. 5단계 검색 시 1~4단계 조상을 유지합니다. parentId로 부모 행의 키를 참조합니다.';
Vue 3 전체 소스 · vue-tree.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, treePlugin } from '../../src/index.js';
import { rows } from './tree.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
const select = (label, values, action) =>
controls.value.push({
kind: 'select',
label,
values,
value: values[0],
action,
});
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [{ field: 'name', title: '업무', width: 360 }],
plugins: [
treePlugin({ field: 'name', showLines: true, defaultExpanded: true }),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
select('처음 표시', ['펼침', '접힘'], (value) => {
grid.destroy();
grid = new ZixoGrid(gridHost.value, {
...options,
plugins: [
treePlugin({
field: 'name',
showLines: true,
defaultExpanded: value === '펼침',
}),
],
});
});
button('연결선 켜기', () => grid.setTreeLines(true));
button('연결선 끄기', () => grid.setTreeLines(false));
button('5단계 검색', () => grid.setSearch('5단계'));
button('검색 해제', () => grid.setSearch(''));
message.value =
'100행을 5단계 계층으로 구성합니다. defaultExpanded: true는 처음 펼침, false는 처음 접힘이며 기본값은 false입니다. 5단계 검색 시 1~4단계 조상을 유지합니다. parentId로 부모 행의 키를 참조합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>트리 · 연결선 · 조상 필터</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 tree와 grouping은 같은 인스턴스에 함께 설치할 수 없습니다. 미조회 lazy 자식은 defaultExpanded:true만으로 자동 조회하지 않습니다. 여러 lazy 요청이 공유 로딩·오류·재시도 상태를 사용하므로 동시 요청 상태 표시에 제한이 있습니다. 순환 부모 참조는 허용되지 않습니다. 검색에 맞는 행의 조상을 유지하되 사용자가 접은 상태는 유지합니다.
연결 기능 메뉴 트리 · 연결선 · 조상 필터 · Tree / Detail lazy 요청 · 안정적인 행 ID
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
그룹 · 집계 · groupingPlugin 표시 중인 행을 지정한 필드 순서로 묶고 각 그룹의 행 수와 합계·평균·최소·최대를 표시합니다.
설치 함수: groupingPlugin(config) · 해제 이름: grouping
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 fields 조건부 필수값 [] 그룹 필드 배열입니다. 첫 필드가 상위 그룹이며 다음 필드가 하위 그룹입니다.그룹을 표시할 때 그룹 기준 필드를 1개 이상 지정합니다. []이면 그룹 없이 원래 행을 표시합니다.
aggregates[] {field,type,label} 배열입니다. field는 집계할 데이터 컬럼입니다. aggregates[].field 조건부 필수값 없음 집계할 데이터 컬럼의 field입니다.aggregates에 집계 항목을 추가하면 각 항목에 대상 field를 지정합니다. aggregates 자체는 생략할 수 있습니다.
aggregates[].typesum sum/avg/min/max 중 하나입니다. 숫자로 변환할 수 있는 값을 집계합니다. aggregates[].labelfield 값 그룹 행에 표시할 집계 제목입니다.
사용 순서 그룹 대상 컬럼과 숫자 집계 컬럼을 구성합니다. fields에 그룹 순서, aggregates에 집계 항목을 지정합니다. 초기에는 그룹을 펼쳐 표시합니다. 그룹의 +/− 버튼으로 전환하고 검색·필터 뒤 표시 행을 기준으로 바뀌는 집계를 확인합니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.toggleGroup(key)그룹을 접거나 펼칩니다. key는 해당 그룹 값 경로를 JSON.stringify한 문자열입니다. 예: JSON.stringify(["제품","서울"]). 필수값 grid.toggleGroup 인수: key
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(groupingPlugin({
fields: ['category', 'region'],
aggregates: [{ field: 'qty', type: 'sum', label: '수량 합계' }],
}));
grid.uninstall('grouping'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());그룹 view 변환을 제거하고 설치 전 toggleGroup을 복원합니다. 실제 데이터에 그룹 합계 행을 삽입하지 않습니다.
관련 이벤트 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/group.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · group.js import { ZixoGrid, groupingPlugin } from '../../src/index.js';
import { rows } from './group.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'category', title: '분류' },
{ field: 'region', title: '지역' },
{ field: 'name', title: '주문' },
{ field: 'qty', title: '수량' },
],
plugins: [
groupingPlugin({
fields: ['category', 'region'],
aggregates: [{ field: 'qty', type: 'sum', label: '수량 합계' }],
}),
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'분류→지역별로 묶고 각 그룹 합계를 표시합니다. +/−로 접고 펼칩니다.';
Vue 3 전체 소스 · vue-group.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, groupingPlugin } from '../../src/index.js';
import { rows } from './group.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'category', title: '분류' },
{ field: 'region', title: '지역' },
{ field: 'name', title: '주문' },
{ field: 'qty', title: '수량' },
],
plugins: [
groupingPlugin({
fields: ['category', 'region'],
aggregates: [{ field: 'qty', type: 'sum', label: '수량 합계' }],
}),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'분류→지역별로 묶고 각 그룹 합계를 표시합니다. +/−로 접고 펼칩니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>다단 그룹 · 그룹 집계</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 tree와 동시에 설치할 수 없습니다. sum/avg/min/max를 지원하며 사용자 지정 집계 함수와 Pivot은 제공하지 않습니다. 그룹 키는 행 rowKey가 아니라 그룹 값 경로입니다. 집계 대상은 현재 view의 행이며 전체/선택 Summary와 별개입니다.
연결 기능 메뉴 다단 그룹 · 그룹 집계 · 전체 · 필터 · 선택 Summary · 컬럼 필터 · AND/OR 조건
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
Master / Detail · detailPlugin 상세 버튼 아래에 별도 ZixoGrid 인스턴스를 만들고 해당 부모의 하위 데이터를 표시합니다. 예제는 부모·상세 각각 100행을 사용합니다.
설치 함수: detailPlugin(config) · 해제 이름: detail
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 field 필수값 없음 상세 버튼을 표시할 컬럼 field입니다. 보통 editable:false로 구성합니다.상세 버튼을 표시할 부모 컬럼의 field를 지정합니다.
columns하위 컬럼 배열 하위 그리드에 사용할 컬럼을 전달합니다. rows없음 rows(parentRow)가 로컬 하위 배열을 반환합니다. loadRows없음 loadRows(parentRow,{signal})이 하위 배열을 비동기 반환합니다. 조회 결과를 부모 키별로 캐시하며 로컬 rows보다 우선 사용합니다.
사용 순서 상세 버튼 컬럼을 부모 columns에 추가합니다. 하위 columns와 rows(parentRow) 또는 loadRows(parentRow,{signal})를 선언합니다. 상세 버튼을 누르면 별도 그리드가 생성됩니다. 같은 상세를 접거나 부모를 종료하면 하위 인스턴스를 정리합니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.toggleDetail(id)부모 행의 상세를 접거나 펼칩니다. 비동기 조회를 포함하며 Promise를 반환합니다. 필수값 grid.toggleDetail 인수: id
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(detailPlugin({
field: 'detail',
columns: [{ field: 'name', title: '하위 주문', width: 300 }],
rows: createDetailRows,
}));
grid.uninstall('detail'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());조회 AbortController를 취소하고 생성된 하위 그리드를 destroy합니다. 상세 렌더러·변환·render 리스너를 제거하고 이전 toggleDetail을 복원합니다. 재렌더/가상화 이탈 시에도 하위 인스턴스를 정리합니다.
관련 이벤트 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/detail.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · detail.js import { ZixoGrid, detailPlugin } from '../../src/index.js';
import { rows, createDetailRows } from './detail.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{ field: 'detail', title: '상세', editable: false },
],
plugins: [
detailPlugin({
field: 'detail',
columns: [{ field: 'name', title: '하위 주문', width: 300 }],
rows: createDetailRows,
}),
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent = '상세 버튼으로 하위 그리드를 생성하고 접으면 해제합니다.';
Vue 3 전체 소스 · vue-detail.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, detailPlugin } from '../../src/index.js';
import { rows, createDetailRows } from './detail.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{ field: 'detail', title: '상세', editable: false },
],
plugins: [
detailPlugin({
field: 'detail',
columns: [{ field: 'name', title: '하위 주문', width: 300 }],
rows: createDetailRows,
}),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value = '상세 버튼으로 하위 그리드를 생성하고 접으면 해제합니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>Master / Detail</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 하위 rowKey는 id이며 하위 그리드는 이 플러그인이 전달하는 columns/rows로 구성됩니다. 임의 하위 plugins 설정은 현재 제공하지 않습니다. 상세 영역은 고정 높이 180px, 하위 그리드는 160px로 생성합니다. lazy 조회 실패 시 error와 재시도를 사용하며 여러 동시 요청의 공유 상태 표시에 제한이 있습니다.
연결 기능 메뉴 Master / Detail · Tree / Detail lazy 요청
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
다단 컬럼 헤더 · columnGroupsPlugin 일반 컬럼 헤더 위에 상위·하위 그룹 제목을 표시합니다. 데이터 행을 그룹화하는 groupingPlugin과 용도가 다릅니다.
설치 함수: columnGroupsPlugin(config) · 해제 이름: columnGroups
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 groups 조건부 필수값 [] 그룹 노드 배열입니다. 노드는 {title,fields} 또는 {title,children}으로 구성합니다.다단 헤더를 표시하려면 그룹을 지정합니다. []이면 일반 헤더만 표시합니다.
groups[].title사용자 지정 헤더에 표시할 그룹 제목입니다. groups[].fields 조건부 필수값 컬럼 field 배열 그룹에 속한 컬럼 field를 지정합니다. 이동 후 떨어진 범위와 좌우 고정 경계는 별도 제목 구간으로 표시합니다. 데이터 rowKey가 아닙니다.각 그룹은 fields 또는 children 중 하나로 대상 컬럼을 연결합니다.
groups[].children 조건부 필수값 하위 그룹 배열 중첩 노드로 헤더 계층을 만듭니다. 상위 범위는 하위 fields를 모아 계산합니다.하위 그룹을 사용할 때 필요합니다. 말단 그룹은 fields로 연결합니다.
사용 순서 columns를 먼저 선언하고 그룹별 컬럼을 연속 순서로 배치합니다. fields로 범위를 지정하거나 children으로 하위 그룹을 만듭니다. 여러 그룹 헤더 행과 마지막 일반 컬럼 헤더의 범위·너비를 확인합니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.install(columnGroupsPlugin({groups}))컬럼 그룹 구성을 설치합니다. 그룹 설정 전용 변경 API는 없습니다. 필수값 grid.install 인수: plugin (name·install 함수 포함)
grid.uninstall("columnGroups")설치 전 renderHeader 동작을 복원합니다. 구성을 바꿀 때 해제 후 새 설정으로 다시 설치할 수 있습니다. 필수값 grid.uninstall 인수: name
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(columnGroupsPlugin({
groups: [
{ title: '주문 정보', fields: ['name'] },
{
title: '금액 정보',
children: [{ title: '수량과 단가', fields: ['qty', 'price'] }],
},
],
}));
grid.uninstall('columnGroups'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());renderHeader를 래핑해 그룹 헤더를 그립니다. 해제 시 설치 전 함수를 복원합니다. 행 데이터·변경 목록은 수정하지 않습니다.
관련 이벤트 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/headers.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · headers.js import { ZixoGrid, columnGroupsPlugin } from '../../src/index.js';
import { rows } from './headers.data.js';
const host = document.querySelector('#grid'),
report = document.querySelector('#report');
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [
columnGroupsPlugin({
groups: [
{ title: '주문 정보', fields: ['name'] },
{
title: '금액 정보',
children: [{ title: '수량과 단가', fields: ['qty', 'price'] }],
},
],
}),
],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
report.textContent =
'fields로 그룹을 지정하고 children으로 하위 그룹을 만듭니다. 짧은 계층은 남은 그룹 행을 병합하며, 숨김·순서 변경·고정 경계에 맞춰 그룹 제목을 나누어 표시합니다. 일반 컬럼 행 높이는 headerHeight, 그룹 행은 단계당 25px입니다.';
Vue 3 전체 소스 · vue-headers.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, columnGroupsPlugin } from '../../src/index.js';
import { rows } from './headers.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
let grid;
let alive = true;
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [
columnGroupsPlugin({
groups: [
{ title: '주문 정보', fields: ['name'] },
{
title: '금액 정보',
children: [{ title: '수량과 단가', fields: ['qty', 'price'] }],
},
],
}),
],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
message.value =
'fields로 그룹을 지정하고 children으로 하위 그룹을 만듭니다. 짧은 계층은 남은 그룹 행을 병합하며, 숨김·순서 변경·고정 경계에 맞춰 그룹 제목을 나누어 표시합니다. 일반 컬럼 행 높이는 headerHeight, 그룹 행은 단계당 25px입니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>다단 컬럼 그룹</h1>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 그룹 행은 단계당 25px이며 마지막 일반 컬럼 행은 headerHeight를 사용합니다. 짧은 계층은 남은 그룹 행을 병합합니다. 숨긴 컬럼은 그룹 범위에서 제외하며, 보이는 하위 그룹이 없는 단계는 표시하지 않습니다. 그룹을 가로지르는 이동이나 고정 경계에서는 제목이 여러 구간에 반복 표시됩니다. 그룹 전체 이동·그룹 제목 정렬/메뉴는 제공하지 않습니다.
연결 기능 메뉴 다단 컬럼 그룹 · 정렬 · 너비 · 순서 · 높이 · 컬럼 · 행 고정
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
내보내기 · 가져오기 · 인쇄 · exportPlugin CSV/XLSX 생성, CSV의 신규 행 가져오기, 현재 적재된 행의 인쇄 HTML을 제공합니다. 파일 형식별 범위와 데이터 기준을 구분해서 사용합니다.
설치 함수: exportPlugin() · 해제 이름: export
설정 속성 · 기본값 필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
설정 위치와 동작 설정 기본값 설명 함수 인수없음 exportPlugin()으로 설치합니다. exportCSV/exportXLSX의 downloadFilefalse true면 파일 다운로드를 시작합니다. false면 CSV 문자열 또는 XLSX Uint8Array를 반환합니다. CSV 데이터현재 표시 컬럼·전체 적재 행 필터로 화면에 줄어든 행만이 아니라 getRows()의 전체 적재 행을 사용합니다. CSV 가져오기 값헤더 기준 매핑 컬럼 title/field로 매핑하고 컬럼 parse·validate와 날짜 정규화를 적용합니다. 숫자·boolean 등은 parse를 직접 구성하세요. 신규 행 키만 추가합니다.
사용 순서 exportPlugin()을 설치합니다. 파일이 필요하면 exportCSV/exportXLSX에 downloadFile:true를 전달합니다. 테스트나 전송용이면 반환값을 사용합니다. CSV 가져오기는 문자열을 importCSV에 전달하고, 인쇄는 print 또는 getPrintHTML을 사용합니다.
연결 API 플러그인 또는 연결된 Core API 호출 동작 grid.exportCSV({downloadFile?})CSV 문자열 생성 및 선택적 다운로드. 수식으로 해석될 수 있는 값에는 보호 접두사를 사용합니다. grid.exportXLSX({downloadFile?})XLSX 바이트 생성 및 선택적 다운로드. 단일 시트입니다. grid.importCSV(text)신규 행을 추가하고 추가 행 수를 반환합니다. 중복 키·잘못된 CSV는 거부합니다. 필수값 grid.importCSV 인수: text
grid.getPrintHTML()현재 표시 컬럼·전체 적재 행의 인쇄 HTML을 반환합니다. grid.print()새 인쇄 창을 열고 인쇄합니다. 팝업이 차단되면 오류를 냅니다.
설치 · 해제 · Vue 3 수명주기 생성 시 plugins에 등록하거나, 아직 설치하지 않은 인스턴스에서 grid.install을 호출합니다. 같은 name은 중복 설치할 수 없습니다. 설정에 쓰는 데이터·함수는 아래 전체 예제 소스를 참고하세요.
grid.install(exportPlugin());
grid.uninstall('export'); // 플러그인 해제
// Vue 3 컴포넌트가 종료될 때
onBeforeUnmount(() => grid.destroy());해제 시 exportCSV/exportXLSX/importCSV/getPrintHTML/print 메서드를 제거합니다. 해제 후 이 메서드를 호출할 수 없습니다.
관련 이벤트 이벤트 구독은 grid.on(name, (detail, grid) => {})를 사용합니다. 해제 함수 또는 grid.off로 구독을 정리하세요.
실행 예제 · 실제 원본 아래 소스는 examples/features/export.js와 Vue 원본입니다. 해당 예제 경로를 기준으로 import하며, 행 데이터는 별도 파일을 함께 사용합니다.
JavaScript 예제 실행 ↗ · Vue 3 예제 실행 ↗ · 행 데이터 원본
JavaScript 전체 소스 · export.js import { ZixoGrid, exportPlugin } from '../../src/index.js';
import { rows } from './export.data.js';
import { createToolbar } from '../feature-controls.js';
const host = document.querySelector('#grid'),
toolbar = document.querySelector('#toolbar'),
report = document.querySelector('#report');
const { button, select } = createToolbar(toolbar, report);
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [exportPlugin()],
};
const grid = new ZixoGrid(host, options);
window.addEventListener('pagehide', () => grid.destroy(), { once: true });
grid.on(
'validationerror',
(detail) => (report.textContent = detail.error.message),
);
button('CSV 내보내기', () => grid.exportCSV({ downloadFile: true }));
button('XLSX 내보내기', () => grid.exportXLSX({ downloadFile: true }));
const upload = document.createElement('input');
upload.type = 'file';
upload.accept = '.csv';
upload.setAttribute('aria-label', 'CSV 가져오기');
upload.addEventListener('change', async () => {
if (!upload.files[0]) return;
try {
report.textContent = grid.importCSV(await upload.files[0].text());
} catch (e) {
report.textContent = e.message;
}
});
toolbar.append(upload);
report.textContent =
'CSV 가져오기는 신규 키의 행을 추가합니다. XLSX Import는 지원하지 않습니다.';
Vue 3 전체 소스 · vue-export.vue <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ZixoGrid, exportPlugin } from '../../src/index.js';
import { rows } from './export.data.js';
// Instance and DOM belong to this component, never to window or a global ID.
const gridHost = ref(null);
const message = ref('');
const controls = ref([]);
let grid;
let alive = true;
async function run(action, value) {
try {
const result = await action(value);
if (alive && result !== undefined)
message.value =
typeof result === 'string' ? result : JSON.stringify(result, null, 2);
} catch (error) {
if (alive) message.value = error.message;
}
}
const button = (label, action) =>
controls.value.push({ kind: 'button', label, action });
async function importFile(event) {
const file = event.target.files?.[0];
if (!file) return;
const text = await file.text();
if (alive) await run(() => grid.importCSV(text));
event.target.value = '';
}
onMounted(() => {
const options = {
rowKey: 'id',
height: 360,
rows: rows,
columns: [
{ field: 'name', title: '주문', width: 240 },
{ field: 'qty', title: '수량', type: 'number', parse: Number },
{ field: 'price', title: '단가', type: 'number', parse: Number },
],
plugins: [exportPlugin()],
};
grid = new ZixoGrid(gridHost.value, options);
grid.on(
'validationerror',
(detail) => (message.value = detail.error.message),
);
button('CSV 내보내기', () => grid.exportCSV({ downloadFile: true }));
button('XLSX 내보내기', () => grid.exportXLSX({ downloadFile: true }));
message.value =
'CSV 가져오기는 신규 키의 행을 추가합니다. XLSX Import는 지원하지 않습니다.';
});
onBeforeUnmount(() => {
alive = false;
grid?.destroy(); // Cancels grid requests, removes events and releases plugins.
grid = undefined;
});
defineExpose({ getGrid: () => grid });
</script>
<template>
<main>
<h1>CSV · XLSX · CSV Import</h1>
<div class="toolbar">
<template v-for="(control, index) in controls" :key="index">
<button
v-if="control.kind === 'button'"
type="button"
@click="run(control.action)"
>
{{ control.label }}
</button>
<label v-else
>{{ control.label }}
<select
v-model="control.value"
:aria-label="control.label"
@change="run(control.action, control.value)"
>
<option v-for="value in control.values" :key="value" :value="value">
{{ value }}
</option>
</select>
</label>
</template>
<input
type="file"
accept=".csv"
aria-label="CSV 가져오기"
@change="importFile"
/>
</div>
<div ref="gridHost" class="vue-grid-host"></div>
<div class="notes result" aria-live="polite">{{ message }}</div>
</main>
</template>
제약 사항 · 주의할 동작 XLSX 가져오기·복수 시트·고급 셀 서식은 지원하지 않습니다. CSV 가져오기는 ID 매칭 Update나 컬럼 매핑 UI를 제공하지 않습니다. 서버의 아직 조회하지 않은 페이지는 내보내기/인쇄에 포함되지 않습니다. 날짜 dateFormat과 셀 format은 화면 표시 설정입니다. 내보내기는 원시 데이터 값(날짜 ISO)을 사용합니다.
연결 기능 메뉴 CSV · XLSX · CSV Import · 적재된 전체 행 인쇄 · 서버 조회 · 저장 계약
이 플러그인 상세 페이지 독립 열기 ↗ · 플러그인 설명 전체
Git 소스로 매뉴얼과 Vue 예제를 실행할 때는 처음 받았거나 package.json/package-lock.json 변경 후 npm install로 개발 의존성을 설치한 뒤 npm start를 실행하세요. JavaScript 화면이 열려도 Vue 예제용 Vue 설치가 누락될 수 있습니다. --omit=dev 설치만으로는 Vue 예제를 실행할 수 없습니다.
2,400건 주문 관리 화면에서 검색·필터·수정·선택 집계·레이아웃 저장을 함께 사용합니다. 변경 확정은 브라우저 메모리의 기준값 갱신이며 외부 서버 저장은 서버 예제에서 확인하세요.
왼쪽 메뉴는 페이지를 이동하지 않고 같은 워크스페이스 안에서 예제를 전환합니다. 주문 편집과 방문한 예제 상태는 유지됩니다. 개발 매뉴얼 메뉴는 별도 독립 페이지를 새 탭으로 엽니다.
일반 셀은 더블클릭·Enter·F2로 셀 안에서 바로 편집합니다. Enter/Tab 또는 다른 셀 클릭으로 확정하고 Escape로 취소합니다. select는 셀에 표시된 선택 컨트롤을 한 번 클릭해 변경합니다. 체크박스·라디오는 테마에 맞춘 선택/비활성 스타일을 제공하며 Space·방향키와 키보드 포커스를 지원합니다. 날짜 달력과 자동완성은 입력 셀에 연결된 목록을 표시하며, 명시적인 사용자 popup 편집기만 별도 팝업을 사용합니다.
헤더 드래그: 파란 삽입선으로 이동 위치를 표시합니다. 대상 헤더의 왼쪽 절반은 앞, 오른쪽 절반은 뒤에 삽입합니다. 헤더 클릭: 오름차순 → 내림차순 → 해제. Shift+헤더: 다중 정렬. 헤더 ⋮: 필터, 왼쪽/오른쪽 고정, 숨김, 내용에 너비 맞춤. 경계 더블클릭도 너비를 맞춥니다. 컬럼 버튼: 표시 컬럼 검색/선택. 조건 필터: 평면 AND/OR 조건 최대20개. 숫자는 숫자로 검사하며 범위/목록은 쉼표로 구분합니다. 셀 선택: 하단 합계·평균. Ctrl+A: 현재 적재된 view 전체 선택. Esc: 선택 해제. 키보드 이동 시 가로 화면 밖 컬럼을 노출합니다. 그룹 예제: 그룹별 수량 합계·평균 단가. 밀도/테마는 새 화면 우측 도구에서 변경합니다. import {ZixoGrid, editorsPlugin, columnToolsPlugin} from '/src/index.js';
const grid = new ZixoGrid('#grid', {
rowKey: 'id', columns, rows,
plugins: [editorsPlugin(), columnToolsPlugin()]
});
// 외부 도구에서도 같은 팝업을 열 수 있습니다.
columnsButton.onclick = () => grid.openColumns(columnsButton);
filterButton.onclick = () => grid.openFilterBuilder(filterButton);너비 맞춤은 view의 처음500행을 측정합니다. 중첩 API 필터는 빌더에서 적용하면 새 평면 조건으로 교체됩니다. 모든 기능·성능의 상용 제품 동등성은 검증하지 않았습니다. 구현 범위와 공식 참고 문서
그리드 속성 설명 ZixoGrid(host, options)의 options에 지정하는 그리드 전체 설정입니다. 아래 기본값은 속성을 생략했을 때의 동작입니다.
필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
생성·행 데이터의 필수값
host 필수값 — ZixoGrid의 첫 번째 인수입니다. 실제 DOM 요소 또는 해당 요소를 찾는 선택자를 전달합니다.
options — 생략하거나 빈 객체로 생성할 수 있습니다. rows·rowKey·height 등은 기본값이 있으므로 모두 직접 지정할 필요가 없습니다.
각 행의 고유 키 값 조건부 필수값 — 생성 rows·setRows·행 추가·서버 조회로 데이터를 넣을 때 모든 행에 필요합니다. rowKey를 생략하면 각 행의 id가 필요합니다. 0은 유효하며 누락·빈 문자열·중복은 허용하지 않습니다.
이벤트 콜백 이벤트 콜백 속성 속성 타입 / 기본값 설명 사용 예 onrowreorder(detail, grid) => void 기본값: 없음행 이동 완료 후 {id,target,position,order}를 받습니다. grid.on('rowreorder',handler)와 같습니다. Undo/Redo는 datachange에서 확인합니다. onrowreorder: detail => console.log(detail.order)
onclick(detail, grid) => void 기본값: 없음cellclick 이벤트의 생성 옵션 콜백입니다. grid.on('cellclick', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. onclick: (detail, grid) => console.log(detail)
ondblclick(detail, grid) => void 기본값: 없음celldblclick 이벤트의 생성 옵션 콜백입니다. grid.on('celldblclick', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. ondblclick: (detail, grid) => console.log(detail)
onselectionchange(detail, grid) => void 기본값: 없음selectionchange 이벤트의 생성 옵션 콜백입니다. grid.on('selectionchange', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. onselectionchange: (detail, grid) => console.log(detail)
oneditrequest(detail, grid) => void 기본값: 없음editrequest 이벤트의 생성 옵션 콜백입니다. grid.on('editrequest', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. oneditrequest: (detail, grid) => console.log(detail)
onvalidationerror(detail, grid) => void 기본값: 없음validationerror 이벤트의 생성 옵션 콜백입니다. grid.on('validationerror', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. onvalidationerror: (detail, grid) => console.log(detail)
onrender(detail, grid) => void 기본값: 없음render 이벤트의 생성 옵션 콜백입니다. grid.on('render', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. onrender: (detail, grid) => console.log(detail)
onerror(detail, grid) => void 기본값: 없음error 이벤트의 생성 옵션 콜백입니다. grid.on('error', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. onerror: (detail, grid) => console.log(detail)
onsaveerror(detail, grid) => void 기본값: 없음saveerror 이벤트의 생성 옵션 콜백입니다. grid.on('saveerror', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. onsaveerror: (detail, grid) => console.log(detail)
ondatachange(detail, grid) => void 기본값: 없음datachange 이벤트의 생성 옵션 콜백입니다. grid.on('datachange', handler)와 같은 detail, grid 인수를 받습니다. 상세 데이터와 시점은 이벤트 메뉴를 참조하세요. ondatachange: (detail, grid) => console.log(detail)
데이터와 생성 데이터와 생성 속성 속성 타입 / 기본값 설명 사용 예 tooltipEnabledboolean 기본값: false셀 툴팁을 켜거나 끕니다. 기본은 숨김이며 시간/문구만 지정해도 켜지지 않습니다. tooltipEnabled: true
tooltipDelaynumber 기본값: 300ms마우스 진입 또는 키보드 셀 이동 뒤 표시까지의 대기 시간. 0은 즉시 표시입니다. tooltipDelay: 200
tooltipDurationnumber 기본값: 3000ms표시 후 자동으로 닫기까지의 시간. 0은 자동 닫기 없이 이탈·Escape·스크롤·편집 시 닫습니다. 툴팁 위에 마우스를 올리면 자동 닫기를 멈춥니다. tooltipDuration: 5000
rowsobject[] 기본값: []생성 시 행 데이터. 생략하고 생성 후 grid.setRows(rows)로 넣어도 됩니다. 내부에서 복사하며 rowKey 중복은 허용하지 않습니다. rows: [{ id: 1, name: '홍길동' }]
columns 조건부 필수값 Column[] 기본값: []데이터 컬럼을 화면에 표시하려면 컬럼 정의가 필요합니다. 생략한 빈 그리드 생성은 가능합니다.
표시할 컬럼 정의. field는 서로 달라야 합니다. 컬럼 속성 설명 메뉴를 참조하세요.columns: [{ field: 'name', title: '이름' }]
rowKeystring | (row) => string | number 기본값: 'id'행 식별자. 비어 있지 않은 문자열 또는 유한한 숫자여야 하며 수정·선택·고정에 사용합니다. 문자열로 지정한 키 컬럼은 편집할 수 없습니다. rowKey: 'orderId'
pluginsPlugin[] 기본값: []필요한 플러그인을 명시적으로 등록합니다. 옵션만 지정해도 플러그인이 자동 설치되지는 않습니다. tree와 grouping은 함께 등록할 수 없습니다. plugins: [editorsPlugin(), columnToolsPlugin()]
크기와 표시 크기와 표시 속성 속성 타입 / 기본값 설명 사용 예 heightnumber | string 기본값: host 높이가 있으면 100%, 없으면420height:auto는 상위 컨테이너의 하단 여백/형제 영역을 제외한 공간을 계산하고 확정 높이·max-height·화면 경계로 제한합니다. 조상 자체가 화면보다 크면 페이지 스크롤이 남을 수 있습니다. 좌우 그리드는 독립 계산하며 가로 flex 너비 분배도 유지합니다. 같은 부모의 상하 auto 여러 개는 영역별 높이를 배정하세요. 숫자는 px이며 %, px/vh/dvh 문자열도 지원합니다. %의 직접 기준은 host의 콘텐츠 높이입니다. host400px이면 100%=400px·50%=200px입니다. 일반 block 배치에서 부모만 고정하고 host가 auto이거나 min-height만 있으면 기준이 결정되지 않습니다. flex/grid로 확정 높이를 배정할 수도 있습니다. host에 확정 높이를 주고 % 높이 체인의 상위 높이도 정하세요. 생성 후 options 객체 변경은 자동 적용되지 않습니다. height: '100%'
rowNumbersboolean 기본값: false맨 앞 고정 No. 컬럼에 표시 순서대로 행번호를 표시합니다. 이동·숨김·편집 금지, 별도 배경. 데이터·내보내기에는 포함하지 않습니다. rowNumbers: true
rowDragFieldstring 기본값: 첫 번째 보이는 데이터 컬럼rowReorder가 true일 때 손잡이를 표시할 기존 데이터 컬럼의 field입니다. 지정 컬럼이 숨겨지면 손잡이도 숨겨집니다. 행번호 컬럼에는 표시하지 않습니다. rowDragField: 'name'
rowReorderboolean 기본값: false별도 컬럼 없이 첫 번째 보이는 데이터 셀 앞에 드래그 손잡이를 표시하며 Alt+위/아래 이동을 제공합니다. getRows 순서를 변경하고 저장 시 order 키 배열을 전달합니다. 정렬·필터·트리·그룹·고정행·저장 중에는 비활성화합니다. rowReorder: true
rowHeightnumber 기본값: 34기본 행 높이(px). 0보다 큰 유한한 수. 줄바꿈 시 콘텐츠에 맞춰 늘어날 수 있습니다. rowHeight: 40
headerHeightnumber 기본값: 38헤더 높이(px). 20 이상. headerWrap을 켜도 자동 높이로 바뀌지는 않습니다. headerHeight: 56
wrapboolean 기본값: false셀 줄바꿈 기본값. 컬럼 wrap이 명시되어 있으면 컬럼 값이 우선합니다. wrap: true
headerAlign'left' | 'center' | 'right' 기본값: 'center'헤더 제목의 전역 정렬. 컬럼 headerAlign이 지정되어 있으면 컬럼 값이 우선하며, 생략한 컬럼은 이 값을 따릅니다. 본문 align과 독립입니다. headerAlign: 'center'
headerWrapboolean 기본값: false헤더 줄바꿈 기본값. 컬럼 headerWrap이 우선합니다. headerWrap: true
columnReorderboolean 기본값: true헤더 드래그 이동 허용. 해당 컬럼의 reorderable도 true여야 합니다. columnReorder: false
theme'light' | 'dark' | 'compact' 기본값: 'light'테마. compact는 글자 크기를 줄이며 행 높이는 rowHeight로 별도 지정합니다. theme: 'dark'
locale'ko' | 'en' 기본값: 'ko'내부 상태·메뉴·달력·그룹/상세·검증 오류와 숫자·날짜 표시 언어. setLocale로 변경하며 업무 제목·데이터·사용자/서버 메시지는 원문을 유지합니다. locale: 'en'
ariaLabelstring 기본값: 'ZIXO 데이터 그리드'스크린 리더가 읽는 그리드 이름. ariaLabel: '주문 목록'
messagesobject 기본값: 언어별 기본 문구기본 UI 문구를 키별로 덮어씁니다. 지원 키는 아래 문구 설정을 참고하세요. messages: { empty: '주문이 없습니다' }
columnMenu('columns' | 'filter')[] 기본값: ['columns','filter']헤더 메뉴에 넣을 영역과 순서. columns=컬럼 표시 설정, filter=컬럼 필터. []는 헤더 메뉴를 숨깁니다. columnToolsPlugin 설치가 필요합니다. columnMenu: ['columns', 'filter']
errorDurationnumber 기본값: 1500ms그리드 오류 오버레이의 자동 숨김 시간(ms). 0은 자동 숨김 없음. 새 오류마다 다시 시작하며 로딩·종료 시 취소합니다. 자동 숨김은 검증 실패를 성공으로 바꾸거나 재시도 상태를 삭제하지 않습니다. errorDuration: 1500
loadingPositionstring 기본값: 'center'로딩 표시 위치: top-left, top-center, top-right, center, bottom-left, bottom-center, bottom-right. loadingPosition: 'center'
rowStyle(row) => object 기본값: 없음행 CSS 스타일 객체. backgroundColor 또는 background로 지정한 배경은 줄무늬·hover보다 우선하며 선택 셀의 강조는 유지합니다. cellStyle의 개별 배경과 행번호 전용 배경은 우선합니다. rowStyle: r => r.overdue ? { backgroundColor: '#fef2f2' } : {}
rowClass(row) => string 기본값: 없음행에 적용할 CSS 클래스. 여러 클래스는 공백으로 구분합니다. rowClass: r => r.overdue ? 'overdue' : ''
편집과 권한 편집과 권한 속성 속성 타입 / 기본값 설명 사용 예 editableboolean | (row, column) => boolean 기본값: true그리드 전체 편집 허용 조건. 컬럼 editable, readonly, 계산 컬럼 여부, permissions.edit도 함께 검사합니다. editable: (r, c) => r.status !== '완료'
readonlyboolean | (row, column) => boolean 기본값: false읽기 전용 조건. true이면 editable보다 우선합니다. readonly: true
referenceYearnumber 기본값: 편집 시 현재 연도날짜 단축 입력 0918/918을 해석할 기준 연도. 유효하지 않은 날짜는 반영하지 않습니다. referenceYear: 2026
permissionsobject 기본값: 제한 없음컬럼·행 표시, 편집, 버튼, 삭제 권한 콜백. 아래 권한 설정을 참고하세요. 서버에서도 권한을 검증해야 합니다. permissions: { edit: (r, c) => r.owner === currentUser }
조회와 집계 조회와 집계 속성 속성 타입 / 기본값 설명 사용 예 searchstring 기본값: ''표시되는 컬럼을 대상으로 대소문자 구분 없는 문자열 검색. 서버 모드에서는 요청에 전달됩니다. search: '서울'
sort{field, dir}[] 기본값: []다중 정렬 순서. dir은 asc 또는 desc. 서버 모드에서는 요청에 전달됩니다. sort: [{ field: 'amount', dir: 'desc' }]
filtersFilter[] 기본값: []필터 배열은 AND로 연결합니다. eq/ne/gt/gte/lt/lte/contains/in 등의 연산자를 사용하며 값은 데이터 타입에 맞춰 지정합니다. 컬럼 메뉴의 두 조건은 field와 op(and/or), rules 배열을 가진 그룹으로 저장됩니다. filters: [{ field: 'amount', op: 'gte', value: 1000 }]
filterTreeFilterGroup | Filter 기본값: 없음rules 배열을 사용하는 중첩 AND/OR 조건. filters와 filterTree를 모두 지정하면 두 조건을 AND로 적용합니다. 그룹당 최대 50개, 깊이 최대 8. filterTree: { op: 'or', rules: [{ field: 'status', op: 'eq', value: '신규' }] }
summary{field, type?, scope?, label?}[] 기본값: 미설정(푸터 숨김)푸터 집계. type: sum/avg/min/max(기본 sum), scope: all/filtered/selected(기본 all). 숫자로 변환 가능한 값만 집계합니다. summary: [{ field: 'amount', type: 'sum', scope: 'filtered', label: '합계' }]
pinnedTopRows(string | number)[] 기본값: []위쪽에 고정할 행 키. 스크롤 본문에서 제외됩니다. 고정 행이 너무 많으면 표시 공간에 제한이 있습니다. pinnedTopRows: [1, 2]
pinnedBottomRows(string | number)[] 기본값: []아래쪽에 고정할 행 키. 스크롤 본문에서 제외됩니다. pinnedBottomRows: [99]
serverobject 기본값: 없음(로컬 모드)페이지·무한 스크롤 서버 조회 설정. size와 fetch 계약은 아래 서버 설정을 참조하세요. server: { size: 100, fetch: fetchOrders }
treeobject 기본값: 없음treePlugin() 사용 시 트리 옵션. 플러그인에 직접 전달한 설정이 그리드 tree 설정보다 우선합니다. tree: { parentField: 'parentId', field: 'name', showLines: true, defaultExpanded: true }
권한 객체의 속성 권한 객체의 속성 속성 속성 타입 / 기본값 설명 사용 예 permissions.view(column) => boolean 기본값: 허용컬럼 표시와 컬럼 관리/필터의 노출을 허용합니다. 서버 권한을 대신하지 않습니다. permissions: {view: column => column.field !== "secret"}
permissions.viewRow(row) => boolean 기본값: 허용화면에 표시할 행을 허용합니다. getRows의 적재 데이터 자체를 삭제하지 않습니다. permissions: {viewRow: row => row.active}
permissions.edit(row,column) => boolean 기본값: 허용편집과 updateCells/붙여넣기의 허용 조건입니다. readonly/계산/키 필드 제한도 적용합니다. permissions: {edit: row => !row.locked}
permissions.button(row,column) => boolean 기본값: 허용셀 버튼을 비활성화합니다. 숨기는 설정은 buttonVisible입니다. permissions: {button: row => row.active}
permissions.delete(row) => boolean 기본값: 허용deleteRows에서 해당 행 삭제를 허용합니다. permissions: {delete: row => row.status === "신규"}
검색·정렬·조건 객체의 속성 검색·정렬·조건 객체의 속성 속성 속성 타입 / 기본값 설명 사용 예 sort[].field 조건부 필수값 string 기본값: 없음정렬 항목마다 대상 field를 제공합니다.
정렬 대상 데이터 field입니다.sort: [{field:"qty",dir:"desc"}]
sort[].dir"asc" | "desc" 기본값: 없음(명시 권장)정렬 방향입니다. 명시적으로 asc/desc를 지정하세요. sort: [{field:"qty",dir:"asc"}]
filters[].field 조건부 필수값 string 기본값: 없음말단 조건에 field를 지정합니다.
조건을 평가할 데이터 field입니다.filters: [{field:"qty",op:"gte",value:2}]
filters[].op 조건부 필수값 string 기본값: 없음말단 조건에 지원 연산자를 지정합니다.
eq/ne/contains/gt/gte/lt/lte/in/range. in/range는 배열 값을 사용합니다.filters: [{field:"qty",op:"range",value:[1,10]}]
filters[].valueunknown 기본값: undefined엄격 비교를 사용하므로 숫자/boolean은 원래 타입으로 지정하세요. filters: [{field:"active",op:"eq",value:true}]
filterTree.op 조건부 필수값 "and" | "or" 기본값: 없음 (rules 그룹에서 필수)rules 그룹에는 and 또는 or를 반드시 지정합니다.
rules 배열의 조건 결합 방식입니다. 말단 조건은 filters와 같은 field/op/value를 사용합니다.filterTree: {op:"or",rules:[{field:"qty",op:"gte",value:2}]}
filterTree.rules(Filter | FilterGroup)[] 기본값: 없음하위 조건/그룹입니다. 그룹당50개·최대 깊이8을 지원합니다. filterTree: {op:"and",rules:[{field:"name",op:"contains",value:"서울"}]}
집계 항목의 속성 집계 항목의 속성 속성 속성 타입 / 기본값 설명 사용 예 summary[].field 조건부 필수값 string 기본값: 없음집계 항목마다 대상 field를 제공합니다.
숫자 집계 대상 field입니다.summary: [{field:"qty"}]
summary[].type"sum" | "avg" | "min" | "max" 기본값: sum집계 함수입니다. 숫자로 변환 가능한 값만 집계합니다. summary: [{field:"qty",type:"sum"}]
summary[].scope"all" | "filtered" | "selected" 기본값: all전체 적재/필터 통과/선택 행 중 집계 대상을 지정합니다. summary: [{field:"qty",scope:"selected"}]
summary[].labelstring 기본값: field푸터 집계 제목입니다. summary: [{field:"qty",label:"수량 합계"}]
서버·트리 객체의 속성 서버·트리 객체의 속성 속성 속성 타입 / 기본값 설명 사용 예 server.sizenumber 기본값: 100페이지당 요청 행 수입니다. 아직 조회하지 않은 전체 행을 자동 적재하지 않습니다. server: {size:100,fetch:fetchRows}
server.fetch 조건부 필수값 async (request,{signal}) => {rows,total,hasMore} 기본값: 없음server를 지정할 때 조회 함수를 제공합니다.
서버 모드의 필수 조회 함수입니다. request={page,size,sort,filters,filterTree,search}; 응답 rows 배열·정수 total·boolean hasMore를 모두 반환합니다.server: {fetch: async (request,{signal}) => loadRows(request,signal)}
tree.fieldstring 기본값: 첫 컬럼(grid.columns[0])rowNumbers 사용 시 field를 명시하세요. treePlugin 설치 시 트리 토글을 표시할 컬럼입니다. 플러그인 직접 config가 우선합니다. tree: {field:"name"}
tree.parentFieldstring 기본값: parentId행의 부모 키를 담은 필드입니다. tree: {parentField:"parentId"}
tree.showLinesboolean 기본값: false트리 연결선 표시입니다. tree: {showLines:true}
tree.defaultExpandedboolean 기본값: false이미 적재된 부모를 처음 펼칩니다. lazy 자식을 자동 조회하지 않습니다. tree: {defaultExpanded:true}
tree.expanded(string | number)[] 기본값: 없음초기 펼친 키 목록. 지정하면 defaultExpanded보다 우선하고 []는 접힘입니다. tree: {expanded:[1,2]}
tree.hasChildrenFieldstring 기본값: hasChildren아직 자식을 조회하지 않은 행의 자식 존재 표시 필드입니다. tree: {hasChildrenField:"hasChildren"}
tree.loadChildrenasync (row,{signal}) => object[] 기본값: 없음lazy 펼침 시 조회한 자식 배열을 반환합니다. 자식의 고유 키와 부모 연결 값이 필요합니다. tree: {loadChildren: async (row,{signal}) => loadChildren(row.id,signal)}
언어 문구 객체의 속성 언어 문구 객체의 속성 속성 속성 타입 / 기본값 설명 사용 예 messages.loadingstring 기본값: 데이터를 가져오는 중…해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {loading: "데이터를 가져오는 중…"}
messages.morestring 기본값: 다음 행을 가져오는 중…해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {more: "다음 행을 가져오는 중…"}
messages.emptystring 기본값: 조회 결과가 없습니다해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {empty: "조회 결과가 없습니다"}
messages.errorstring 기본값: 오류해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {error: "오류"}
messages.applystring 기본값: 적용해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {apply: "적용"}
messages.cancelstring 기본값: 취소해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {cancel: "취소"}
messages.searchstring 기본값: 검색해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {search: "검색"}
messages.previousstring 기본값: 이전해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {previous: "이전"}
messages.nextstring 기본값: 다음해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {next: "다음"}
messages.closestring 기본값: 닫기해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {close: "닫기"}
messages.expandstring 기본값: 펼치기해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {expand: "펼치기"}
messages.collapsestring 기본값: 접기해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {collapse: "접기"}
messages.prevMonthstring 기본값: 이전 달해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {prevMonth: "이전 달"}
messages.nextMonthstring 기본값: 다음 달해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {nextMonth: "다음 달"}
messages.revertstring 기본값: 원복해당 UI 문구를 덮어씁니다. 생략한 키는 locale 기본값을 사용합니다. messages: {revert: "원복"}
부모의 남은 높이 자동 계산 height: auto를 지정하면 검색·하단 영역과 여백을 뺀 공간을 채웁니다. 상위 경로의 하단 여백·형제 영역과 확정 높이/max-height/화면 경계를 반영합니다. 조상 자체의 강제 높이가 화면을 넘는 경우 그리드 외 레이아웃도 조정해야 합니다. 부모 크기·형제 영역·창 크기 변경에도 재생성 없이 반영합니다. 좌우 그리드는 실제 아래 영역만 차감해 각각 독립 계산하며, 같은 가로 flex 부모에서는 기존 너비 분배를 유지합니다. 같은 부모의 상하 auto 여러 개는 높이 자동 균등 분할을 하지 않으므로 영역별 높이를 지정하세요. 전용 host를 사용하세요. 복잡한 다열·마진 상쇄 배치는 세로 flex와 gap으로 구성합니다. 기존 숫자·퍼센트 높이는 유지합니다.
JS/Vue 자동 높이 예제
height의 퍼센트 기준
options.height의 %는 그리드를 생성한 host div의 콘텐츠 높이 를 기준으로 합니다. 일반 block 배치에서 부모 높이만 지정하고 중간 host를 auto로 두면 100%와 50%가 모두 내용 높이로 커질 수 있습니다. min-height만 지정한 host도 퍼센트 기준을 확정하지 않습니다. 아래처럼 host까지 높이를 연결하세요.
<div style="height: 400px">
<div ref="gridHost" style="height: 100%"></div>
</div>
onMounted(() => {
grid = new ZixoGrid(gridHost.value, { columns, rows, height: '50%' });
}); // host400px 기준 grid200px. 100%는400px.
host에 직접 height:400px를 지정해도 됩니다. flex/grid 배치로 host에 확정 높이를 배정할 때는 min-height:0도 함께 확인하세요. padding·border가 있는 host는 콘텐츠 영역을 기준으로 계산합니다. 높이를 생략한 빈 host의 기본420px와, 기준 높이가 없는 명시적 height:'50%'는 다른 경우입니다. 생성 후 options.height만 변경해도 실행 중 그리드가 갱신되지는 않습니다. 부모 div · 퍼센트 높이 예제의 선택은 새 그리드를 생성해 비교하므로 편집·선택 상태가 초기화됩니다.
객체 속성의 세부 설정
permissions
view(column), viewRow(row), edit(row, column), button(row, column), delete(row)는 각각 컬럼 표시, 행 표시, 편집, 버튼 실행, 행 삭제 허용 여부를 boolean으로 반환합니다. 생략한 콜백은 허용으로 처리합니다.
permissions: {
view: column => column.field !== 'internalMemo',
viewRow: row => row.department === '영업',
edit: (row, column) => row.status !== '완료',
button: (row, column) => row.active,
delete: row => row.status === '신규'
}
server
server 설정 자체는 선택입니다. 서버 모드로 연결할 때 아래 항목은 필수값 입니다.
서버 모드의 필수값 항목 필수 여부 계약
server.fetch필수값 서버 모드에서 사용할 조회 함수. 조회 응답 객체를 반환합니다.
응답.rows필수값 행 배열. 빈 결과는 []이며 각 행에 고유 rowKey 값이 필요합니다.
응답.total필수값 전체 건수인 정수. 0도 유효합니다.
응답.hasMore필수값 추가 페이지 여부인 boolean. false도 반드시 반환합니다.
server.size는 선택이며 생략 시 100입니다.
size는 페이지 크기(기본 100)입니다. fetch(request, {signal})는 Promise로 {rows, total, hasMore}를 반환합니다. total은 정수, hasMore는 boolean입니다. request는 page(0부터 시작), size, sort, filters, filterTree, search를 포함합니다. 재조회 시 이전 요청을 취소하므로 signal을 실제 요청에 전달하세요.
server: {
size: 100,
fetch: async (request, { signal }) => {
const response = await fetch('/api/orders/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal
});
if (!response.ok) throw new Error('조회 실패');
return response.json(); // { rows: [...], total: 2400, hasMore: true }
}
}
서버 요청·응답 계약 과 서버 연동 예제에서 상세 구현을 확인하세요.
tree — treePlugin() 필요
parentField는 부모 행 키를 담는 필드(기본 'parentId'), field는 트리를 표시할 컬럼(기본 첫 컬럼), showLines는 연결선 표시(기본 false), defaultExpanded는 처음 전체를 펼칠지 정하는 boolean(기본 false)입니다. true이면 적재된 모든 부모 노드를 펼치고 false이면 접습니다. expanded는 처음 펼칠 행 키 배열이며 생략하면 defaultExpanded를 따릅니다. expanded를 지정하면 해당 배열이 우선하므로 []는 모두 접힘입니다. 이후 사용자가 접은 노드는 재렌더·검색 후에도 접힌 상태를 유지합니다. 미조회 lazy 자식은 자동 조회하지 않습니다. hasChildrenField(기본 'hasChildren')가 true인 행을 펼칠 때 loadChildren(row, {signal})으로 자식 행 배열을 비동기 조회할 수 있습니다. 반환 행에도 부모 필드와 고유 rowKey가 필요합니다.
plugins: [treePlugin()],
tree: {
parentField: 'parentId', field: 'name', showLines: true,
defaultExpanded: true, hasChildrenField: 'hasChildren',
loadChildren: async (row, { signal }) => {
const response = await fetch(`/api/tree/${row.id}`, { signal });
if (!response.ok) throw new Error('자식 조회 실패');
return response.json(); // [{ id: 2, parentId: 1, name: '자식' }]
}
}
messages
기본 UI 키: loading, more, empty, error, apply, cancel, search, previous, next, close, expand, collapse, prevMonth, nextMonth, revert. 내부 한국어 문구 키도 messages 객체에서 재정의할 수 있습니다. 예: messages: { "컬럼 표시 설정": "표시할 컬럼" }. {0} 같은 치환 토큰은 원래 문구와 같은 개수로 유지하세요. 지정하지 않은 키는 locale의 기본 문구를 사용하며 사용자/서버 오류를 자동 번역하지 않습니다.
그리드와 컬럼 옵션을 함께 사용하기
아래 예제는 ES 모듈 기준입니다. host에 해당하는 요소가 있어야 하며 src/zixo.css도 페이지에 연결하세요. 생성 후 변경할 때는 setRows, setColumnWidth, setColumnVisible, setTheme, setSearch 등의 API를 사용하세요. 컬럼 전체 교체용 setColumns는 제공하지 않으므로 컬럼 정의 전체 변경은 종료 후 다시 생성합니다.
import { ZixoGrid, editorsPlugin } from '../src/index.js';
const grid = new ZixoGrid(document.querySelector('#grid'), {
rowKey: 'id',
height: 420,
wrap: true,
plugins: [editorsPlugin()],
rows: [{ id: 1, name: '홍길동', status: '신규', qty: 2, price: 1500 }],
columns: [
{ field: 'name', title: '이름', pinned: 'left', width: 160 },
{ field: 'status', title: '상태', editor: 'select', options: ['신규', '완료'] },
{ field: 'qty', title: '수량', type: 'number', parse: Number,
validate: value => value >= 0 || '음수는 입력할 수 없습니다' },
{ field: 'price', title: '단가', type: 'number', parse: Number },
{ field: 'amount', title: '금액', type: 'number', align: 'right',
dependsOn: ['qty', 'price'], calculate: row => row.qty * row.price }
],
summary: [{ field: 'amount', type: 'sum', scope: 'filtered', label: '합계' }]
});
트리 초기 펼침·접힘 // 처음부터 전체 펼침 (이미 적재된 행)
plugins: [treePlugin({ field: 'name', defaultExpanded: true })]
// 처음에는 모두 접힘 (기본값)
plugins: [treePlugin({ field: 'name', defaultExpanded: false })]
// 일부 노드만 펼침: expanded가 defaultExpanded보다 우선
plugins: [treePlugin({ field: 'name', defaultExpanded: true, expanded: [1, 2] })]
// 그리드 옵션으로도 설정 가능
plugins: [treePlugin()],
tree: { field: 'name', defaultExpanded: true }
컬럼 속성 설명 컬럼 줄바꿈 · 자동 행 높이 실행 예제 에서 wrap/headerWrap 및 컬럼별 상속·끄기를 비교할 수 있습니다.
컬럼 속성은 생성 옵션 columns의 해당 항목 안에 지정합니다. 생성 전에 options.columns[0].width = 180처럼 따로 지정해도 됩니다. 생성 후 너비는 grid.setColumnWidth(field, 180)로 변경합니다. 각 속성의 초기 설정·변경 API는 아래 설명을 확인하세요.
options.columns 배열의 각 항목에 지정하는 컬럼 설정입니다. 그리드 전체 설정과 구분하여 사용하세요.
필수값 은 해당 입력에서 반드시 제공하는 값입니다. 조건부 필수값 은 설명에 적힌 기능을 사용할 때 필요합니다. 표시가 없는 옵션은 생략할 수 있으며 기본값을 사용합니다. 빈 문자열·누락과 0·false를 혼동하지 마세요.
식별과 배치 식별과 배치 속성 속성 타입 / 기본값 설명 사용 예 field 필수값 string 기본값: 없음각 컬럼에 고유한 field 문자열을 지정합니다. title은 생략할 수 있습니다.
행 데이터의 속성 이름. 다른 컬럼과 중복할 수 없습니다.field: 'name'
titlestring 기본값: field헤더에 표시할 텍스트. title: '이름'
widthnumber 기본값: 140컬럼 너비(px). flex가 있으면 남는 공간에 따라 재계산됩니다. width: 200
minWidthnumber 기본값: 40너비 최솟값(px). minWidth: 80
maxWidthnumber 기본값: 1200너비 최댓값(px). maxWidth: 400
flexnumber 기본값: 0실제 표시 영역에서 고정 컬럼 너비를 제외한 공간을 나누는 비율입니다. 세로 스크롤바와 테두리 너비는 배분 대상에서 제외합니다. 생략/0이면 width를 사용하고, 양수이면 width에 더하지 않고 비율로 너비를 계산합니다. minWidth/maxWidth를 지키며 제한으로 남거나 부족해진 공간은 재분배하지 않습니다. 컬럼 경계 드래그나 setColumnWidth 호출 시 해당 컬럼 flex는 0이 됩니다. 아래 계산 예제를 참고하세요. flex: 2
hiddenboolean 기본값: false컬럼을 숨깁니다. hidden: true
hideBelownumber 기본값: 없음그리드 컨테이너 폭이 이 값(px)보다 작으면 숨깁니다. hideBelow: 700
pinned'left' | 'right' 기본값: 없음좌측 또는 우측에 고정하여 가로 스크롤 중에도 표시합니다. pinned: 'left'
reorderableboolean 기본값: true드래그 이동 허용. 이동 출발 컬럼과 도착 컬럼 모두 허용해야 하며 전체 columnReorder도 검사합니다. reorderable: false
sortableboolean 기본값: true헤더 클릭 정렬 허용. setSort API 호출을 막는 권한 설정은 아닙니다. sortable: false
filterableboolean 기본값: truecolumnToolsPlugin 필터 메뉴에 표시할지 결정합니다. setFilter API 호출 자체를 막지는 않습니다. filterable: false
표시와 꾸미기 표시와 꾸미기 속성 속성 타입 / 기본값 설명 사용 예 align'left' | 'center' | 'right' 기본값: 'left'셀 내용 정렬. 헤더 정렬과 독립입니다. align: 'right'
headerAlign'left' | 'center' | 'right' 기본값: 그리드 headerAlign 상속 (기본 'center')헤더 제목 정렬. 지정한 컬럼 값이 그리드 전역 headerAlign보다 우선하며 생략하면 전역 값을 따릅니다. center는 컬럼 전체 너비의 중앙에 제목을 배치합니다. 정렬·필터·메뉴 아이콘은 오른쪽의 별도 영역에 표시되며 제목의 중심을 이동시키지 않습니다. align(본문 셀 정렬)과 독립입니다. headerAlign: 'center'
wrapboolean 기본값: 그리드 wrap 상속셀 줄바꿈. false를 명시하면 전체 wrap:true도 덮어씁니다. wrap: true
headerWrapboolean 기본값: 그리드 headerWrap 상속헤더 줄바꿈. 헤더 높이는 별도 설정합니다. headerWrap: true
type'text' | 'number' | 'date' | 'image' | 'button' 기본값: 기본 텍스트 표시숫자는 Intl, 날짜는 날짜 형식으로 표시합니다. type만으로 입력 변환·검증·편집기가 자동 설정되지는 않습니다. type: 'number'
imageSrcstring | (value,row,grid) => string 기본값: 셀 값type:image의 이미지 URL. 생략하면 행의 field 값을 URL로 사용합니다. imageSrc: (v, row) => row.photo
imageAltstring | (value,row,grid) => string 기본값: ''이미지 대체 텍스트. 장식 이미지 외에는 내용을 설명하는 문구를 지정하세요. imageAlt: (v, row) => row.name
imageTextstring | number | {left?,right?,top?,bottom?} | function 기본값: 없음이미지와 함께 표시할 텍스트. 객체를 사용하면 상하좌우에 동시에 표시합니다. 텍스트는 HTML로 해석하지 않으며 이미지 실패 후에도 유지합니다. imageText: (v, row) => row.name
imageTextPosition'left' | 'right' | 'top' | 'bottom' 기본값: 'right'문자열 imageText의 위치. 객체형 imageText는 각 키로 위치를 지정합니다. imageTextPosition: 'left'
imageWidthnumber 기본값: 24이미지 너비(px). 양수를 지정하고 컬럼 폭 안에서 표시합니다. imageWidth: 48
imageHeightnumber 기본값: 24이미지 높이(px). 양수를 지정하며 큰 이미지에는 rowHeight도 조정하세요. imageHeight: 48
imageFitcontain | cover | fill | none | scale-down 기본값: contain고정 이미지 영역 안에서 맞추는 방식입니다. imageFit: 'cover'
imageFallbackstring | (value,row,grid) => string 기본값: 없음이미지 로딩 실패 시 한 번 사용하는 대체 URL. 다시 실패하면 imagePlaceholder를 표시합니다. imageFallback: '/images/placeholder.svg'
imagePlaceholderstring 기본값: ''URL이 없거나 로딩에 실패했을 때 표시할 문구입니다. imagePlaceholder: '이미지 없음'
dateFormatstring 기본값: 없음 (locale 날짜 표시)날짜 컬럼별 표시·입력 형식. YYYY/yyyy, MM/M, DD/dd/D/d를 각각 한 번 사용합니다. 저장은 YYYY-MM-DD이며 format 콜백이 표시에서 우선합니다. dateFormat: 'YYYY.MM.DD'
format(value, row, locale) => string 기본값: 없음텍스트·숫자·날짜 표시 문자열을 반환하며 해당 type보다 우선합니다. 이미지/버튼은 전용 옵션을 사용합니다. HTML로 해석하지 않습니다. format: (v, r, locale) => Number(v).toLocaleString(locale)
cellStyle(value, row) => object 기본값: 없음셀 CSS 스타일 객체. cellStyle: v => ({ color: v < 0 ? '#b91c1c' : '' })
cellClass(value, row) => string 기본값: 없음셀 클래스. 여러 클래스는 공백으로 구분합니다. cellClass: v => v < 0 ? 'negative' : ''
icon(value, row) => string 기본값: 없음셀 앞에 표시할 아이콘 문자. 보조 장식으로 처리합니다. icon: v => v ? '●' : ''
tooltipEnabledboolean 기본값: 그리드 값이 컬럼의 툴팁 표시 여부. 그리드보다 우선하며 false로 개별 숨김이 가능합니다. tooltipEnabled: true
tooltipDelaynumber 기본값: 그리드 값해당 컬럼의 툴팁 표시 대기 시간(ms). 그리드 설정보다 우선합니다. tooltipDelay: 0
tooltipDurationnumber 기본값: 그리드 값해당 컬럼의 툴팁 유지 시간(ms). 0은 자동 닫기 없음. 그리드 설정보다 우선합니다. tooltipDuration: 8000
columnMenu('columns' | 'filter')[] 기본값: 그리드 값해당 컬럼의 헤더 메뉴 영역과 순서. 컬럼 배열이 그리드 배열을 대체합니다. []는 이 컬럼의 ⋮와 그리드 헤더 우클릭 메뉴를 숨깁니다. columnMenu: ['columns']
tooltip(value, row) => string 기본값: 셀 값스타일 툴팁 텍스트. 빈 문자열은 표시하지 않으며 HTML을 해석하지 않습니다. tooltip: (v, r) => `주문 ${r.id}: ${v}`
입력과 검증 입력과 검증 속성 속성 타입 / 기본값 설명 사용 예 editorstring 기본값: 'text'text/date/select/combo/checkbox/radio/button. 인라인 입력과 select는 editorsPlugin()을 등록합니다. checkbox/radio/button은 기본 렌더러에도 포함됩니다. editor: 'select'
editableboolean | (row, column) => boolean 기본값: true컬럼 편집 조건. 전체 editable도 허용해야 하며 readonly·권한·계산 컬럼 제한이 우선합니다. editable: r => r.active
readonlyboolean | (row, column) => boolean 기본값: false조건부 읽기 전용. true이면 편집을 막습니다. readonly: r => r.locked
parse(value, row) => value 기본값: 입력값 그대로저장 전에 동기 변환합니다. validate보다 먼저 실행합니다. parse: Number
validate(value, row) => boolean | string | undefined 기본값: 검증 없음true/undefined는 성공, false/문자열은 실패입니다. 문자열은 오류 문구입니다. 비동기 검증은 지원하지 않습니다. validate: v => v >= 0 || '음수는 입력할 수 없습니다'
optionsarray | (row) => array 기본값: []select/radio/combo 선택지. 원시 값 또는 {value,label}. 숫자·boolean 타입을 유지하며 문자열로 변환한 값이 중복되면 오류입니다. options: [{ value: 1, label: '사용' }, { value: 0, label: '중지' }]
loadOptionsasync (query, row, {signal}) => array 기본값: 없음combo 자동완성 선택지를 비동기로 가져옵니다. 이전 요청 취소를 위해 signal을 전달하세요. loadOptions: async (q, r, {signal}) => fetchCodes(q, signal)
debouncenumber 기본값: 180combo 입력 후 조회까지 대기 시간(ms). debounce: 250
headerCheckboxboolean 기본값: true (checkbox 컬럼)checkbox 헤더에 전체 체크 컨트롤을 표시합니다. false면 숨깁니다. 대상은 조회·편집 가능한 적재 행 전체이며 부분 체크 상태를 표시합니다. headerCheckbox: true
headerCheckboxTextstring 기본값: title 또는 field헤더 체크박스 옆 텍스트입니다. 빈 문자열이면 체크박스만 표시합니다. headerCheckboxText: '전체 선택'
buttonVisibleboolean | (row,grid) => boolean 기본값: truefalse면 버튼을 DOM에 표시하지 않습니다. 좌우 텍스트는 유지합니다. buttonDisabled와 별개입니다. buttonVisible: row => row.active
buttonLabelstring | (value,row,grid) => string 기본값: 셀 값 또는 '실행'type:button 또는 editor:button의 버튼 문구. 함수로 행별 문구를 정할 수 있습니다. buttonLabel: '조회'
buttonTextBeforestring | (value,row,grid) => string 기본값: 없음버튼 왼쪽에 표시할 텍스트. null/undefined/빈 문자열은 생략하며 HTML은 해석하지 않습니다. buttonTextBefore: (v, row) => '#' + row.id
buttonTextAfterstring | (value,row,grid) => string 기본값: 없음버튼 오른쪽 텍스트. 긴 텍스트는 공간에 맞게 줄여 표시하고 스타일 툴팁으로 전체 문구를 제공합니다. buttonTextAfter: (v, row) => row.locked ? '잠김' : '사용 가능'
buttonDisabledboolean | (row,grid) => boolean 기본값: false버튼 비활성 조건입니다. permissions.button이 거부해도 비활성화됩니다. buttonDisabled: row => row.locked
onClick(row, grid, event) => void 기본값: 없음버튼 클릭 콜백. row는 복사본이며 event는 원본 클릭 이벤트입니다. 변경에는 grid API를 사용합니다. permissions.button과 buttonDisabled를 함께 검사합니다. onClick: (r, grid) => console.log(r.id)
계산 계산 속성 속성 타입 / 기본값 설명 사용 예 dependsOnstring[] 기본값: []calculate가 참조하는 컬럼 field 목록. 계산 순서를 정하며 순환 참조는 오류입니다. dependsOn: ['qty', 'price']
calculate(row) => value 기본값: 없음함수로 계산하는 읽기 전용 컬럼. 의존 값 변경 시 다시 계산합니다. 문자열 수식은 지원하지 않습니다. calculate: r => r.qty * r.price
선택지 항목과 이미지 텍스트 객체 선택지 항목과 이미지 텍스트 객체 속성 속성 타입 / 기본값 설명 사용 예 options[].valueunknown 기본값: 원시 항목 자체객체형 select/radio/combo 항목의 실제 저장 값입니다. 숫자/boolean 타입을 유지하고 String(value)가 중복되지 않도록 지정합니다. options: [{value:1,label:"사용"}]
options[].labelstring 기본값: 원시 항목 자체객체형 선택지의 화면 문구입니다. value와 함께 지정하세요. loadOptions 응답 항목도 같은 구조입니다. options: [{value:1,label:"사용"}]
imageText.leftstring | number 기본값: 없음객체형 이미지 텍스트의 왼쪽 문구. 여러 위치를 함께 지정할 수 있고 빈 값은 생략합니다. imageText: {left: "설명"}
imageText.rightstring | number 기본값: 없음객체형 이미지 텍스트의 오른쪽 문구. 여러 위치를 함께 지정할 수 있고 빈 값은 생략합니다. imageText: {right: "설명"}
imageText.topstring | number 기본값: 없음객체형 이미지 텍스트의 위쪽 문구. 여러 위치를 함께 지정할 수 있고 빈 값은 생략합니다. imageText: {top: "설명"}
imageText.bottomstring | number 기본값: 없음객체형 이미지 텍스트의 아래쪽 문구. 여러 위치를 함께 지정할 수 있고 빈 값은 생략합니다. imageText: {bottom: "설명"}
flex — 남는 너비를 비율로 나누기
그리드의 실제 표시 영역에서 고정 컬럼 너비를 뺀 나머지를 flex 비율로 나눕니다. 실제 표시 영역은 테두리와 세로 스크롤바를 제외한 데이터 영역입니다.
columns: [
{ field: 'id', title: '번호', width: 100 },
{ field: 'name', title: '이름', flex: 2 },
{ field: 'department', title: '부서', flex: 1 }
]
실제 표시 영역이 1,000px이면 고정 100px을 제외한 900px을 2:1로 나눕니다.
flex 너비 계산 예 컬럼 설정 계산된 너비
번호 width: 100 100px
이름 flex: 2 600px
부서 flex: 1 300px
flex를 생략하거나 0으로 지정하면 width를 사용합니다. 양수일 때는 width에 더하지 않고 비율로 계산한 너비를 사용합니다.
minWidth·maxWidth 범위를 지킵니다. 제한 때문에 남거나 부족해진 공간을 다른 컬럼에 재분배하지 않습니다.
모든 컬럼이 flex: 1이면 실제 표시 영역을 균등하게 나눕니다. minWidth 합이 영역을 초과하면 가로 스크롤이 생기는 것이 정상입니다.
행 수 변경으로 세로 스크롤바가 생기거나 사라질 때, 또는 컨테이너 너비가 바뀔 때 다시 계산합니다.
컬럼 경계를 드래그하거나 grid.setColumnWidth()를 호출하면 해당 컬럼 flex는 0이 되어 고정 너비로 바뀝니다.
반응형 · flex 실행 예제 에서 고정 너비와 2:1 비율 배분을 확인할 수 있습니다.
날짜 입력은 editor: 'date'와 그리드 referenceYear를 사용합니다. dateFormat은 컬럼별 표시·입력 형식이며, 저장 값은 YYYY-MM-DD로 유지합니다. 예: YYYY.MM.DD, DD/MM/YYYY, yyyy-MM-dd, YYYY년 M월 D일. YYYY/yyyy·MM/M·DD/dd/D/d를 각각 한 번 사용합니다. 생략하면 locale 표시를 사용하며 format 콜백이 표시에서 우선합니다. 초기 rows/setRows 데이터는 ISO 날짜로 전달하세요. 모든 콜백은 애플리케이션에서 직접 제공하는 함수입니다.
실제 로컬 브라우저 검증 기록 — 2026-10-12 KST Node v22.14.0, Chrome 154.0.8037.92 headless, 1280×900, Playwright 1.64.0, axe 4.13.0, Vue 3.5.43에서 검사했습니다. 기능 16개·실제 입력 7개·독립 예제 17개·매뉴얼 iframe 17개·Vue 생성/종료 17개 통과, accessibility 예제 기본 상태의 axe 위반 0건입니다. 매뉴얼 코드 탭은 JavaScript 클릭 및 Vue 방향키 전환을 검사합니다.
실제 입력 검사는 마우스 드래그/Shift/Ctrl 선택, 클립보드 복사/붙여넣기·Undo/Redo/Delete, Fill Handle, Home/End/Page/F2/Ctrl+F, 헤더 드래그 및 마우스/키보드 너비 조절, wheel Infinite 조회·오류 재시도, HTTP503 저장 실패 후 재시도·서버 반영 및 rollback을 포함합니다. 선택 시 DOM 교체로 발생한 pointerover 재진입·더블클릭 손실을 수정하고 재검사했습니다. axe 검사기는 명시적 browser context를 사용하도록 수정했습니다.
독립 예제 검사는 grid 생성과 JavaScript 오류 없음, Vue 검사는 생성 및 종료 후 DOM 제거를 확인합니다. 각 예제의 모든 동작을 검증한 것은 아닙니다. Excel 앱·실제 인쇄·스크린리더·Spring·전체 모듈 조합·모든 상태의 접근성은 미검증입니다. 미완료 기능은 ROADMAP.ko.md에서 확인하세요.
재현: 테스트 의존성과 Vue를 준비한 뒤 BROWSER_CHANNEL=chrome npm run test:browser를 실행합니다. 설치 Chrome 대신 Playwright Chromium도 사용할 수 있습니다. 결과 원문: 최종 브라우저 결과 , 초기 검사 , 수정 전 입력 검사 , Node 결과 . 기능별 범위는 AUDIT.ko.md, 보안 범위는 SECURITY-AUDIT.ko.md에 기록했습니다.
2026-10-13 브라우저 재검사 2026-10-13 KST, Chrome 155.0.8059.39 headless(1280×900)에서 재검사했다. 기능16개·실제 사용자 입력8개 통과, 독립 예제17개·매뉴얼 iframe17개 및 탭 전환·Vue 생성/종료17개 통과, accessibility 예제 기본 상태 axe 위반0건, JavaScript 오류0건이다. 최근 변경한 마우스 선택 시 전체 포커스 테두리 숨김, Tab/Shift+Tab 접근 및 방향키 입력 시 포커스 테두리 유지도 실제 입력으로 통과했다. 이번 재검사에서 제품 코드 추가 수정은 없었다. 최종 원문은 docs/test-results/browser.json이다. Spring·실제 스크린리더·Excel 앱·전체 UI 상태 접근성은 이번에도 미검증이다.
업무 UI 개편 · 최종 검증 2026-10-13 12:04 KST, Chrome155.0.8059.39 headless(1280×900). 기존 기능16개·실제입력8개·신규 워크스페이스 UI10개 통과, 독립 예제17개·매뉴얼 iframe17개/탭·Vue 생성/종료17개 통과. 접근성 예제, 신규 라이트/다크 업무 화면, 조건 팝업의 axe 검사 모두 위반0건. Node21개 및 JS54개 구문·HTML/import 참조 검사 통과. 390px 화면의 팝업 경계/가로 이탈도 확인했다. 전체 기능 조합·실제 스크린리더·Excel 앱·Spring·성능/메모리 벤치마크는 이번 범위 밖이다.
UI 개편 최종 결과 원문 · 신규 API·지원 범위·공식 참고 자료
셀 내부 편집·원클릭 select 최종 검증 2026-10-13 KST, 로컬 Chrome155.0.8059.39에서 셀 내부 편집과 원클릭 select 변경 후 전체 재검사를 완료했다. 기능16개·실제 입력11개·워크스페이스11개 총38개 통과, 독립 예제17개·매뉴얼 iframe17개/탭·Vue 생성/종료17개 통과. 접근성 예제·업무 라이트/다크·조건 팝업 axe 위반0건, Node21개·JS55개 구문/참조 검사 통과. 셀 내부 입력·렌더 중 입력 보존·Tab/blur 확정·Escape 취소·검증 실패 원자성·문자 입력 시작과 select 원클릭/즉시 반영/숫자 타입/readonly/Ctrl+Z를 실제 입력으로 확인했다. OS 한글 IME·모든 편집/팝업 상태의 axe·전체 기능 조합은 별도 미검증이다.
최종 검사 원문
기능별 매뉴얼·예제 소스 개편 검증 2026-10-13 KST, Chrome155에서 기능16개·사용자 입력11개·워크스페이스11개·기능별 매뉴얼4개 총42개 검사 통과. 독립 예제17개·매뉴얼 iframe17개·Vue 생성/종료17개 통과, 접근성5상태 axe 위반0건. 문법 강조 후 매뉴얼4개/복사 원문·select 테두리 별도 재검사도 통과했고, CSS outline/border는 none이다. 조건부 readonly·텍스트 tooltip 예제도 직접 검사했다. JS56개 구문과 HTML/import 참조 검사 통과. 실제 스크린리더·Excel 앱·Spring·전체 UI 조합은 미검증이다.
전체 검사 원문 · select·매뉴얼 최종 확인
그리드·컬럼 속성 설명 — 2026-10-13 그리드30개·컬럼35개 속성에 타입·기본값·동작·예제를 추가했습니다. Chrome155에서 매뉴얼5개 검사(두 메뉴·직접 링크·390px 포함) 통과, 두 속성 메뉴 axe 위반0건입니다. 문서의 통합 설정 예제를 실행하여 계산 금액과 푸터 합계3000을 확인했습니다. 전체 브라우저 검사43개와 독립/iframe/Vue 예제 각17개도 통과했습니다.
속성 메뉴 실제 결과 · 전체 브라우저 결과
Vue 3 SFC 예제 — 2026-10-13 script setup/template 기반 예제17개 생성·해제·원본 제공을 검사했습니다. Vue 입력 바인딩·테마·CSV 가져오기, 두 인스턴스 격리·재마운트·해제 시 조회 취소 및 기본 UI 접근성까지 전용20개 검사 통과, JavaScript 오류0·axe 위반0건입니다. 전체 브라우저43개와 Node21개도 통과했습니다. 외부 Vite 프로젝트 및 SSR 실행은 별도 미검증입니다.
Vue 3 검사 결과 · Vue 3 적용 안내
메뉴별 전용 콘텐츠·플러그인 설명 — 2026-10-13 각 메뉴의 전용 JS·Vue 3 예제42개씩 생성/실행했습니다. 고유 패널·iframe·소스, 필요한 플러그인만 import하는지와 필터·lazy 조회·select·설치/해제 조작을 포함한 전용44개 검사 통과. 플러그인 설명8개·접근성 위반0·JavaScript 오류0건입니다. 전체 브라우저43개와 매뉴얼 iframe42개도 통과했습니다.
전용 메뉴 검사 결과
실행 예제·샘플 소스 대응 — 2026-10-13 Vue 소스 탭은 Vue 실행 화면으로 전환하며 실행 파일 경로와 독립 실행 링크도 함께 바뀝니다. 중간 기본 데이터를 덮어쓰는 소스를 제거하여 최종 컬럼·데이터만 표시합니다. 실제 Chrome에서42개 메뉴의 JS/Vue 초기 화면과 표시 소스·파일 원문·복사 내용 일치 검사 모두 통과했습니다. 전체 브라우저43개 및 Node21개도 통과했습니다.
화면·소스 대조 결과
이벤트 설명 — 2026-10-13 DOM 이벤트6개와 모델 변경 구독을 코드와 대조했습니다. 문서의 JavaScript·Vue 3 소스를 직접 실행하여 실제 전달 데이터·부모 전달·구독 해제를 검사했습니다. 메뉴·직접 링크·모바일·접근성을 포함한3개 검사 통과, JavaScript 오류0·axe 위반0건입니다.
이벤트 문서 실제 검사 결과
클릭·더블클릭과 이벤트별 실행 — 2026-10-13 onclick/ondblclick 옵션과 cellclick/celldblclick을 추가했습니다. DOM 이벤트8개 및 모델 구독의9개 예제를 JavaScript/Vue에서 직접 조작하여 로그·전달 데이터·소스 연결·해제를 확인했습니다. 전용22개 검사, 전체 브라우저43개, Node21개 통과. 이벤트 메뉴 접근성 위반0·JavaScript 오류0건입니다.
이벤트별 실제 실행 결과
통합 이벤트 API — 2026-10-13 9개 이벤트를 생성 옵션 또는 grid.on으로 받으며 동일한 detail·grid를 전달합니다. 구독 해제·중복 방지·초기 렌더·브라우저 기본 이벤트 격리·Vue 전달을 포함한 전용23개 검사 통과, JavaScript 오류0·접근성 위반0건입니다. 전체 브라우저43개·화면/소스 대조42개·Node21개도 통과했습니다.
통합 이벤트 실제 결과
예제별 행 데이터 탭 — 2026-10-14 기능 42개와 이벤트 9개가 JavaScript·Vue 3 공통 .data.js를 사용합니다. 일반 예제 최소 100행, 상세 100행, 대량 10만 행입니다. 서버는 조회 전 빈 배열·페이지당 100행, lazy는 초기 부모 1행·확장 후 101행입니다. 데이터 탭은 실행 화면을 초기화하지 않습니다.
Chrome 실제 인스턴스 검사70개·소스/화면 대조42개·이벤트23개·메뉴/조작44개 통과, JavaScript 오류0건입니다. 기존 핵심16·입력11·워크스페이스11·매뉴얼5 검사와 독립 JS/Vue 각17개도 통과했습니다.
실제 행 수와 동작 결과 · 변경 화면
5단계 트리 예제 — 2026-10-14 100행의 5단계 계층을 처음부터 펼쳐 표시합니다. 매뉴얼 JS/Vue·독립 JS/Vue·공통 샘플 5종에서 단계별 들여쓰기·접기/펼치기를 확인했고, 검색 버튼이 있는 4종에서 말단 검색 시 조상을 유지했습니다. JavaScript 오류0건.
실제 검사 결과 · 5단계 화면
트리 초기 펼침 옵션 — 2026-10-14 defaultExpanded로 처음 펼침/접힘을 선택합니다. expanded 키 배열 우선순위·사용자 접힘 유지·새 부모·lazy 조회와 JS/Vue 처음 표시 선택을 실제 Chrome에서 검사했습니다. 트리5종·옵션 계약·소스 대조42개·기존 브라우저 검사 통과, JavaScript 오류0건.
옵션 계약 실제 결과
여러 행 추가·생성 후 데이터 설정 — 2026-10-14 insertRows(rows)와 insert(rows)는 여러 행을 한 번에 추가하며 Undo 한 번으로 취소합니다. 생성 옵션 rows 없이 만든 뒤 setRows(rows)로 전체 데이터를 넣는 JS/Vue 예제를 제공합니다. ID 중복·누락/계산 실패 보존·이벤트·복사·전체 교체 기준 초기화를 실제 API와 화면에서 확인했습니다. Node23개·새 브라우저5개·소스 대조42개 및 기존 브라우저 검사가 통과했습니다. JavaScript 오류0건.
실제 API와 예제 검사 · 변경 화면
날짜 컬럼별 형식 — 2026-10-14 dateFormat으로 컬럼별 표시·입력 형식을 지정하고 저장 값은 YYYY-MM-DD로 유지합니다. JS/Vue 예제의 3가지 표기·직접 입력·달력 월/선택·Undo·잘못된 날짜 거부와 format 우선순위·기본 locale 표시를 Chrome 전용3개 검사에서 확인했습니다. Node25개·소스 대조42개와 기존 브라우저 검사도 통과했습니다. JavaScript 오류0건.
날짜 형식 실제 검사 · 날짜 형식 비교 화면
플러그인별 상세 설명 — 2026-10-14 플러그인 설명 아래8개 하위 메뉴와 독립 상세 페이지8개를 제공합니다. 설정·API·설치/해제·Vue 수명주기·실제 원본·실행 예제·제약을 각각 정리했습니다. Chrome 메뉴/독립 페이지/원본/링크/키보드/모바일 검사17개와 기존 기능 검사44개 통과, JavaScript 오류0·자동 접근성 위반0건입니다.
실제 상세 페이지 검사 · 하위 메뉴와 상세 화면
이미지·버튼 컬럼 — 2026-10-14 type:image와 type:button을 제공하며 기존 editor:button도 유지합니다. 이미지 실제 로딩·대체/빈 표시·alt·크기와 버튼 클릭·Enter/Space·비활성·권한·복사본·폼 미제출·기본 행 중앙 배치·기존 방식 호환을 Chrome 전용5개 검사에서 확인했습니다. JS/Vue 예제는100행, 소스 대조43개·Node25개와 기존 브라우저 검사도 통과했습니다. JavaScript 오류0·전용 예제 axe 위반0건.
실제 이미지·버튼 검사 · 예제 화면
버튼 컬럼 좌우 텍스트 — 2026-10-14 buttonTextBefore/buttonTextAfter로 문자열 또는 행별 함수를 지정합니다. JS/Vue 배치·텍스트 클릭과 버튼 동작 구분·키보드·비활성·접근성·기존 editor 방식·빈 값·HTML 안전·데이터 갱신·말줄임과 버튼 표시를 Chrome 전용6개 검사에서 확인했습니다. 소스 대조43개 통과, JavaScript 오류0·예제 axe 위반0건.
실제 버튼 검사 · 좌우 텍스트 예제 화면
기존 브라우저 핵심16·입력11·워크스페이스11·매뉴얼5 및 독립 JS/Vue 각17개도 재검사 통과했습니다. JavaScript 오류0·자동 접근성 위반0건. 기존 기능 재검사
가로 스크롤 헤더·데이터 정렬 — 2026-10-14 세로 스크롤바가 차지하는 폭을 헤더에도 반영해 오른쪽 끝의 15px 어긋남을 수정했습니다. 일반/overlay 스크롤바와 처음·중간·끝·역방향·휠, 좌우 고정·다단 헤더·너비·행 수·화면 폭·헤더 포커스를 Chrome 전용4개 검사에서 확인했습니다. JavaScript 오류0건.
실제 정렬 검사 · 오른쪽 끝 정렬 화면
기존 브라우저 핵심16·입력11·워크스페이스11·매뉴얼5 및 독립 JS/Vue 각17개도 재검사 통과했습니다. JavaScript 오류0·자동 접근성 위반0건. 기존 기능 검사
스타일 툴팁·밀리초 시간 설정 — 2026-10-14 tooltipDelay(기본300ms)와 tooltipDuration(기본3000ms)을 그리드/컬럼별로 설정하며 컬럼 값이 우선합니다. duration 0은 자동 닫기 없음입니다. 밝은/다크 스타일·ARIA·키보드·hover·화면 경계·해제와 JS/Vue 실행 예제에서 실제2500ms 유지 및 메뉴로1000ms 변경을 Chrome 전용5개 검사에서 확인했습니다. 키보드 재렌더 예약 표시와 ARIA 구조 오류를 수정 후 재검사 통과했습니다.
소스 대조43개·이벤트23개·날짜3개·이미지/버튼6개와 기존 브라우저 핵심16·입력11·워크스페이스11·매뉴얼5 및 독립 JS/Vue 각17개 재검사 통과. JavaScript 오류0·자동 접근성 위반0건.
실제 툴팁 검사 · 스타일과 시간 설정 화면 · 기존 기능 검사
헤더 메뉴 아코디언 — 2026-10-14 정렬·컬럼 설정·컬럼 필터를 각각 접고 펼칩니다. 처음에는 정렬만 펼치고 적용 중인 필터가 있으면 필터도 펼칩니다. JS/Vue 초기 상태·마우스·Enter/Space·키보드 순환·Escape·접근성2개와 기존 워크스페이스11개 검사 통과. 기본 높이233.5px, JavaScript 오류0·메뉴 axe 위반0건. 접힌 영역 내부의 키보드 이동 문제를 수정 후 재검사했습니다.
실제 아코디언 검사 · 접기·펼치기 화면
헤더 메뉴 영역별 설정 — 2026-10-14 columnMenu 배열로 sort·columns·filter 영역과 순서를 그리드/컬럼별로 지정합니다. 컬럼 값이 우선이고 []는 해당 헤더 메뉴를 숨깁니다. JS/Vue 실제 구성 전환·8개 조합·순서·컬럼 우선·금지 영역·필터 펼침·설정 오류와 기존 워크스페이스11개를 Chrome 전용5개 검사에서 확인했습니다. 소스 대조43개 통과, JavaScript 오류0·메뉴 axe 위반0건.
실제 메뉴 설정 검사 · 필터만 추가한 메뉴 화면
기존 브라우저 핵심16·입력11·워크스페이스11·매뉴얼5 및 독립 JS/Vue 각17개도 통과했습니다. JavaScript 오류0·자동 접근성 위반0건. 기존 기능 검사
100×100 가로 스크롤 왕복 개선 — 2026-10-14 가로 이동 때 셀 전체를 다시 만들던 동작을 제거하고 데이터 위치를 기준으로 헤더를 동기화합니다. 실제 Chrome20회 왕복에서 행 교체300개→0개, JS/Vue 붙여넣기 전후 모두 첫 행 유지·헤더 위치 차이0을 확인했습니다. 전용3개·일반/overlay 정렬4개 및 기존 브라우저 검사가 통과했습니다. JavaScript 오류0·자동 접근성 위반0건.
왕복 측정과 실제 검사 · 기존 기능 검사
툴팁 기본 숨김 — 2026-10-14 tooltipEnabled 기본값은 false입니다. 그리드/컬럼에서 true로 켜고 컬럼 설정이 우선합니다. 문구·시간만 지정하면 표시하지 않습니다. 전용 JS/Vue 예제에는 true를 명시했습니다. 기본 숨김·컬럼 켜기·개별 끄기와 기존 동작을 Chrome6개 검사에서 확인했습니다. JavaScript 오류0·전용 axe 위반0건.
실제 툴팁 검사
모든 Vue 예제의 종료 버튼 제거 — 2026-10-14 독립17·기능43·이벤트9의 Vue 예제69개에서 종료 버튼을 제거하고 생성기도 수정했습니다. 화면 이탈 시 자동 해제하고 SFC 수명주기 정리는 유지합니다. 버튼 부재·pagehide 정리·실제 조작·원본 소스를 Chrome 기능별45개·이벤트23개·소스 대조43개·Vue SFC20개 검사에서 확인했습니다. JavaScript 오류0건.
Vue 수명주기 검사 · 기능별 예제 검사 · 이벤트 예제 검사
예제 초기 속성을 생성 설정에 통합 — 2026-10-14 초기 컬럼 속성 대입과 컬럼/플러그인 추가를 options 객체에 직접 넣도록 정리했습니다. 생성 전 options.columns[0].tooltip 등의 별도 대입도 가능하며 생성 후 원래 옵션 변경은 자동 반영되지 않는다고 설명했습니다. JS/Vue 소스 대조43개·기능45개·이벤트23개·Vue SFC20개 및 기존 전체 브라우저 재검사 통과. JavaScript 오류0·자동 접근성 위반0건.
화면·실제 소스 대조 · 기존 기능 검사
컬럼 줄바꿈 비교 예제 — 2026-10-14 컬럼 줄바꿈·자동 행 높이 메뉴를 추가했습니다. JS/Vue100행에서 셀/헤더 true·false·그리드 상속을 비교하고 너비 버튼으로 행 높이 변화를 확인합니다. 헤더 높이는 headerHeight로 별도 지정합니다. Chrome 전용2개 검사에서 실제 줄 수와 행 높이132px→73.5px 변화를 확인했습니다. 전체 기능46개·소스 대조44개 통과, JavaScript 오류0·전용 axe 위반0건.
실제 줄바꿈 검사 · 줄바꿈 비교 화면
기본 셀 세로 중앙 정렬 — 2026-10-14 행 높이가 늘어나도 기본 텍스트와 입력 컨트롤 내용을 세로 중앙에 배치합니다. 가로 정렬·줄바꿈·말줄임과 이미지/버튼 배치는 유지합니다. JS/Vue 줄바꿈2개·120px 행6종의 중심 좌표·이미지/버튼6개·가로 왕복3개·소스 대조44개 및 기존 전체 브라우저 검사가 통과했습니다. 레이아웃 갱신 중 측정하던 검사를 배치 완료 후 측정하도록 정리했습니다. JavaScript 오류0·자동 접근성 위반0건.
기본 셀 중심 좌표 · 줄바꿈 중앙 정렬 검사 · 수정 화면
기능별 설정 설명 정리 — 2026-10-14 공통 툴팁 대입 문구를 기능별 초기 속성 또는 실행 API로 교체했습니다. 기능44개·이벤트9개 실제 안내와 예시 코드 구문을 확인했고 툴팁 대입은 툴팁 안내에만 남았습니다. JS/Vue 소스 대조44개도 통과했습니다.
실제 안내 검사 · 화면·소스 대조
테마 빌더 — 2026-10-14 라이트·다크·포레스트, 색상11개와 서체/크기를120행 실제 그리드에서 조절합니다. CSS 복사·다운로드와 JavaScript/Vue 3 적용 코드를 제공합니다. 로컬 Chrome에서 즉시 반영·편집값 유지·높이 좌표·복사/다운로드 원문·내보낸 JS 실행·Vue 컴파일·매뉴얼 메뉴를 확인했습니다. 안내 문구 대비 실패를 수정해 재검사 통과,3개 프리셋 및390px 화면 axe 위반0·JavaScript 오류0·모바일 문서 가로 넘침0입니다. 기존 매뉴얼 JS/Vue 소스·화면 대조44개도 재검사 통과했습니다. 별도 팝업·달력·툴팁은 테마 범위에서 제외하며 높이는 생성 옵션을 함께 적용합니다.
실제 테마 빌더 검사 · 데스크톱 화면 · 모바일 화면
다단 컬럼 그룹 헤더 수정 — 2026-10-14 일반 컬럼 헤더가38px 대신 전체88px로 늘어나 아래50px이 잘리는 오류를 실제 Chrome에서 재현하고 수정했습니다. 일반 행의 높이를 고정하고 짧은 계층 병합, 숨긴 가지 제외, 순서 변경/좌우 고정 경계별 제목 분할을 적용했습니다. 전용4개 검사에서 JS/Vue·독립 예제 잘림0px, 병합50px, 고정/가로 왕복/이동/숨김/너비·높이/빈 그룹/3단 계층/해제·재설치가 통과했습니다. 기존 스크롤 정렬4개와 핵심16·입력11·워크스페이스11·매뉴얼5·독립 JS/Vue 각17개도 재검사 통과했습니다. JavaScript 오류0건입니다. 그룹 전체 이동·그룹 제목 정렬/메뉴·스크린리더 검사는 남아 있습니다.
헤더 실제 좌표 검사 · 수정 화면 · 스크롤 정렬 재검사
사내 Nexus npm 패키지 준비 — 2026-10-14 @kloz/zixo-grid@0.1.0 패키지를 사내 kloz-npm hosted 주소로 고정하고 소스/CSS/사용 문서만 포함하도록 준비했습니다. 20파일·압축51867bytes의 tgz를 별도 프로젝트에 실제 설치했고 README Vue 3 예제의 Chrome 표시·CSS·편집·종료 검사가 통과했습니다. JavaScript 오류0입니다. Nexus npm 인증이 없어 실제 저장소 등록·원격 재설치는 아직 수행하지 않았습니다.
실제 패키지 설치 검사 · 설치한 패키지 화면
사내 Nexus npm 등록·원격 설치 검증 — 2026-10-14 @kloz/zixo-grid@0.1.0을 kloz-npm에 등록했습니다. 새 캐시·새 프로젝트에서 사내 Nexus로부터 직접 설치했고 원격 sha512와 사전 검증 tgz가 일치했습니다. 설치한 패키지로 README Vue3 예제의 실제 Chrome 표시/CSS·편집·종료 정리가 통과했습니다.3개 검사 통과·JavaScript 오류0입니다. 공개 npmjs에는 업로드하지 않았습니다.
Nexus 실제 다운로드/설치 검사 · 설치한 패키지 실행 화면
가벼운 난독화 npm 0.1.1 — 2026-10-14 Git src는 유지하고 npm에는 가볍게 난독화한 dist만 배포합니다. 소스맵·문자열 인코딩·실행 흐름 변형은 사용하지 않습니다. dist15파일이 이전10만 행 검증본과 바이트 단위로 일치하며, dist를 사용한 단위/서버25개와 전체 Chrome/Vue 검사가 통과했습니다. npm 0.1.1을 사내 Nexus에 등록하고 새 캐시로 내려받아 해시·Vue3 렌더/편집/종료를 재검사했습니다. JavaScript 오류0입니다.
10만 행7회 중앙값은 일반787.5ms, 가벼운802.5ms이며 마지막 행 이동은12.9ms/12.7ms였습니다. 모듈 다운로드/최초 파싱은 제외한 로컬 측정이며 모든 환경의 무손실 성능을 보장하는 수치는 아닙니다.
dist 전체 검사 · 10만 행 비교 · Nexus 원격 설치 검사
데이터 조회 API 문서 — 2026-10-15 getRows/getChangedRows/selectedCells/copy의 반환 범위와 대체 코드를 전용 메뉴로 정리했습니다. getSelectedRows/getRow/getValue는 미제공으로 구분했으며 런타임 API는 추가하지 않았습니다. Chrome에서 메뉴/단독 패널·코드6개 구문과 복사·필터/정렬·선택·TSV·변경 목록 동작을 대조해2개 검사 통과, JavaScript 오류0입니다.
실제 문서/API 대조 · 조회 API 매뉴얼 화면
로고 아래 버전 표시 — 2026-10-19 메인·매뉴얼·플러그인 상세·테마 빌더 로고 아래에 package.json 기준 v0.1.1을 표시합니다. Chrome 데스크톱11개 페이지와390px 모바일3개 표시 검사14개가 통과했고 JavaScript 오류0입니다. npm 라이브러리는 변경하지 않았습니다.
버전 표시 검사 · 메인 화면 · 모바일 화면
clearRows() · npm 0.1.2 — 2026-10-19 setRows([])와 같은 전체 비우기 API를 추가했습니다. 변경/Undo/Redo/선택을 초기화하고 삭제 변경으로 기록하지 않습니다. JS/Vue CRUD 예제에 비우기·복원 버튼을 추가했습니다. 단위/서버25개·행 API5개·소스 대조44개와 난독화 dist 전체 Chrome/Vue 검사, 실제 설치한 clearRows 검사가 통과했습니다. 사내 Nexus0.1.2 등록 후 새 캐시 원격 설치/해시/동작도 확인했습니다.
행 API 검사 · Nexus 설치 검사
Git 이슈 #1: 헤더 중앙 정렬 — 2026-10-19 headerAlign: center의 제목이 실제 컬럼 중앙보다 왼쪽으로 치우치는 문제를 Chrome에서 재현했습니다. 메뉴 없는80px 헤더의 오차는 수정 전−16.51px이었습니다. 제목과 정렬·필터·메뉴 영역을 분리하고 중앙 정렬의 좌우 여백을 맞췄습니다. 전용8개 상태에서 고정/줄바꿈/정렬/필터/가로 이동/너비 변경 후 최대 중심 오차0.51px, JavaScript 오류0입니다. 줄바꿈2·메뉴5·다단 그룹4·스크롤 정렬4개와 단위/서버25개, 전체 Chrome 핵심16·입력11·워크스페이스11·매뉴얼5·독립JS/Vue각17개 및 소스 대조44개가 통과했습니다. 해당 axe 위반0입니다. 난독화 패키지 빌드/별도 설치도 확인했으며 npm 등록은 하지 않았습니다.
실제 좌표 측정 · 수정 화면 · 전체 Chrome 검사
간단한 컬럼 메뉴·체크 목록 — 2026-10-19 값 검색·전체 선택·체크 목록·적용/초기화와 텍스트/숫자/날짜 조건 하위 창을 추가했습니다. 두 조건 AND/OR, 빈 선택, 타입 보존, 재열기/레이아웃 복원, 서버 실제 조회와 Vue3, Escape/Tab·바깥 클릭·해제 정리를 Chrome에서 검사했습니다. 소스/난독화 dist 전용각9개 통과·JavaScript 오류0, 데스크톱/390px 팝업 경계 및 해당 axe 위반0입니다. 기존 메뉴5개·단위/서버25개·전체 Chrome 핵심16·입력11·워크스페이스11·매뉴얼5·독립JS/Vue각17개와 소스 대조44개도 통과했습니다. 값 목록은 적재한 데이터 기준이며 서버 전체 값 목록 조회는 제공하지 않습니다. npm에는 등록하지 않았습니다.
실제 메뉴 조작 검사 · 난독화 dist 검사 · 데스크톱 화면 · 모바일 화면
컬럼 메뉴 정렬 제거·명칭 변경 — 2026-10-19 메뉴 정렬 버튼을 제거하고 ‘컬럼 표시 설정’으로 이름을 변경했습니다. 기본 메뉴는 columns/filter이며 이전 sort 옵션은 화면에서 제외합니다. 전용9개·메뉴 구성5개(워크스페이스11개 포함)·구문294개/HTML/import 검사가 통과했고 JavaScript 오류0입니다.
메뉴 실제 조작 검사 · 메뉴 구성/회귀 검사 · 변경 화면
컬럼 표시 설정 목록 통합 — 2026-10-19 컬럼 표시 설정 하위 창과 표시 컬럼 관리가 공통 컬럼 목록을 사용하도록 변경했습니다. 컬럼 검색·즉시 표시/숨김·현재 컬럼 숨김과 복원·권한 제외·마지막 컬럼 보호를 검사했고, 전용10개 검사·JavaScript 오류0·데스크톱/390px 해당 axe 위반0을 확인했습니다. JS/Vue 메뉴 검사에도 하위 목록의 컬럼 숨김/복원을 추가했습니다.
실제 동작 검사 · JS/Vue 메뉴 검사 · 컬럼 목록 화면
목록 스타일 통일·전체 헤더 메뉴 수정 — 2026-10-19 컬럼 표시/필터 값 목록의 행·체크박스·검색창 스타일을 공통화하고 실제 computed style 일치를 검사했습니다. 예제 선택기가 주문 컬럼만 바꾸던 문제를 수정해 ‘전체 헤더 메뉴’가 주문·수량·단가에 함께 적용됩니다. JS/Vue 각각 세 컬럼의 전체/표시 설정만/필터만/메뉴 없음 모두 통과했습니다. 전용10개·메뉴 구성5개(워크스페이스11개 포함)·구문294개/HTML/import 검사 통과, JavaScript 오류0입니다.
스타일/동작 검사 · 세 컬럼·JS/Vue 검사 · 통일한 목록 화면
업무 팝업 제거·이미지/텍스트 복합 셀 — 2026-10-19 업무 팝업 코드·메뉴·전용 예제를 제거했습니다. 이미지 셀에 imageText/imageTextPosition을 추가해 텍스트를 좌우/상하 또는 여러 방향에 동시에 배치합니다. JS/Vue 상대 좌표·동시 배치·이미지 실패 후 텍스트 유지·HTML 문자열 안전성·선택/복사·기존 버튼·팝업 제거 등8개 검사가 소스/난독화 dist에서 모두 통과했습니다. JavaScript 오류0·관련 axe 위반0입니다. 단위/서버25개·전체 Chrome 핵심16·입력11·워크스페이스11·매뉴얼5·독립JS/Vue각17개와 소스 대조43개, 구문290개/참조 검사, 패키지 설치 및 단일 HTML 핵심16개도 통과했습니다. npm 등록은 하지 않았습니다.
복합 셀 실제 검사 · 난독화 dist 검사 · 매뉴얼 화면 · 단일 HTML 검사
0.2.0 버전·README 동기화 — 2026-10-19 package/lock·README.md·README.ko.md·API 현재 버전과 웹 배지를0.2.0으로 갱신했습니다. npm run check에 두 README의 현재 선언 버전 일치 검사를 추가했습니다. Chrome 데스크톱11개·390px3개 페이지 배지 검사14개 통과·JavaScript 오류0, 0.2.0 tgz 별도 설치/Vue 실행과 구문290개/참조/버전 검사도 통과했습니다. npm 등록은 수행하지 않았습니다.
실제 버전 표시 검사 · 0.2.0 패키지 설치 검사
사내 Nexus 0.2.0 등록·원격 설치 — 2026-10-19 최종 README와 난독화 배포 코드를 포함한 @kloz/zixo-grid@0.2.0을 사내 kloz-npm에 등록했습니다.20파일·압축66991bytes·해제232631bytes이며 원본 src/소스맵은 제외했습니다. 단위/서버25개·구문290개/참조/버전 일치 검사를 통과한 tgz를 등록했고, 새 캐시의 원격 다운로드/해시 일치/Vue3 렌더·편집·clearRows·종료 검사도 통과했습니다. JavaScript 오류0입니다.
최종 패키지 검증 · Nexus 원격 설치 검증
2026-10-21 — 퍼센트 높이·행 제어·컨트롤 확장 퍼센트 div 높이, readonly 붙여넣기 메시지 없음, 헤더 전체 체크·텍스트, 자동 행번호, 행 드래그·실제 서버 순서 저장/재조회, 빈 이미지 대체 표시 제거, 조건별 버튼 숨김을 확인했습니다. 새 기능18개·이벤트25개·이미지/버튼8개·소스/화면47개·단위/서버27개 및 전체 Chrome 회귀 검사를 통과했습니다. 새 JS/Vue 예제의 axe 위반과 JavaScript 오류는0입니다.
행 드래그는 정렬·검색·필터·트리·그룹·고정행·저장 중에는 비활성화합니다. 서버는 변경 배치의 order를 처리해야 합니다. 로컬 개발 서버는 실제 반영하며 운영 DB 연동은 애플리케이션 책임입니다. 결과: 전용 결과 · 실제 화면 · 이벤트 · 소스 일치 . Git 업로드와 npm 등록은 수행하지 않았습니다.
2026-10-21 — 예제 프레임 높이 자동 맞춤 예제 바깥 세로 스크롤을 없애도록 iframe 높이를 내용에 맞춥니다. 그리드 내부 데이터 스크롤은 유지합니다. 데스크톱1280px·모바일390px에서 기능47개와 이벤트10개를 JS/Vue로 확인했고, 동적 내용 증감과 기존 매뉴얼 회귀를 포함한234개 검사가 통과했습니다. JavaScript 오류0. 실제 검사 결과 · 화면 . 재검사: node scripts/browser-manual-frames.mjs.
2026-10-21 · 0.4.0 행 이동 손잡이·Vue3 전환 전용 순서 컬럼 대신 기존 데이터 셀 앞에 손잡이를 표시합니다. rowDragField로 대상 컬럼을 지정합니다. Vue3 탭 전환 후 그리드가 화면 위로 밀리던 현상을 재현하고, 실행 방식 변경 후 그리드 프레임으로 화면을 맞추도록 수정했습니다. 예제 초기화는 기존 동작과 같습니다.
Chrome155에서 47기능·10이벤트의 JS→Vue→JS→Vue 전환을 1280/390px에서 검사했습니다. 전환456개와 동적 높이1·매뉴얼 회귀5로 총462검사 통과, JavaScript 오류0건입니다. 행 제어19, 독립 Vue3 등20, 소스/실행 일치47, Node27 및 로컬 패키지2도 통과했습니다. 실제 드래그·키보드 이동·변경 순서 저장/재조회·Undo/Redo를 포함합니다. 전체 기능 조합·실제 스크린리더·타 브라우저는 별도 검증 대상입니다.
전환 검사 원문 · 행 제어 결과 · 화면 위치 측정 · Vue3 최종 화면
2026-10-22 · 0.4.1 rowStyle 배경 rowStyle.backgroundColor/background가 셀 배경·줄무늬·hover에 가려지는 문제를 재현하고 수정했습니다. 행 배경은 줄무늬·hover보다 우선하며 선택 강조·셀별 cellStyle 배경·행번호 전용 배경을 유지합니다. 중첩 그리드에는 배경을 전달하지 않습니다.
실제 Chrome에서 수정 전8개 실패, 수정 후 중첩 격리를 포함한9개 통과·오류0건. Node27, 전체 브라우저 핵심16·입력11·워크스페이스11·매뉴얼5 및 독립JS17/Vue17, 로컬 패키지2 검사도 통과했습니다. JS/Vue 예제·고정 컬럼·선택/해제·hover·조건 변경/Undo·가상 스크롤·테마를 확인했습니다. 전체 조합·타 브라우저·실제 스크린리더 검증은 별도입니다.
수정 전 결과 · 수정 후 결과 · Vue3 배경 적용 화면
2026-10-22 · 0.4.2 다국어·읽기 전용 붙여넣기·Vue 실제0.4.0 패키지에서 붙여넣기 도중 readonly 전환 후 잔류 오류, en 메뉴의 한국어 표시를 재현했습니다. 새 Git checkout의 Vue runtime404·빈 화면과 캐시 보존 pagehide 이벤트의 Vue 종료도 확인했습니다.
내부 문구는 ko/en 언어 설정에 맞추고 업무/서버 메시지는 원문을 유지합니다. readonly 반영 중 거부도 원자 무시합니다. 개발 서버는 설치된 Vue를 직접 제공하며, 예제는 import 실패 안내와 캐시 보존/실제 종료를 구분합니다. 실제 그리드 렌더 뒤 화면 위치를 보정합니다.
Chrome 전용 회귀11개·매뉴얼 프레임462개·소스 일치47개·Vue20개·Node29개·로컬 패키지2개 및 전체 기능 회귀 통과, JS 오류0건입니다. 캐시 검사는 수명주기 이벤트를 재현했으며 실제 브라우저 bfcache 왕복·타 브라우저·스크린리더·전체 조합은 미검증입니다.
0.4.0 비교·수정 후 검사 · Vue3 영문 메뉴 화면 · 전체 전환 검사
2026-10-22 · 0.5.0 오류 표시 시간 errorDuration 기본값은2000ms이며 0은 자동 숨김 없음입니다. 연속 오류는 마지막 오류부터 다시 세고, 로딩 전환·readonly 오류 해제·검증 편집 종료·destroy 시 타이머를 취소합니다. 자동 숨김 후에도 입력 검증·조회 재시도는 유지합니다.
실제 Chrome 전용12개 검사·기존 다국어/readonly/Vue 회귀11개·Node29개·난독화 패키지2개 통과. 기본2초·사용자 시간/0·연속 오류·로딩·언어·검증·빈 상태·재시도·종료와 JS/Vue 예제를 확인했습니다. 전체 조합·타 브라우저·스크린리더는 미검증입니다.
전용 검사 원문 · Vue 오류 표시 화면
2026-10-22 · 퍼센트 높이 기준과 JS/Vue 비교 Chrome155에서 host400px의 그리드100%=400px·50%=200px를 확인했습니다. 일반 block host가 auto이면 부모 높이만 고정해도 비율이 적용되지 않는 현상을 재현했습니다. 코어 로직은 유지하고 기준 높이 설명과 JS/Vue 100%·50% 선택 예제를 보완했습니다.
전용12개·기존 행 제어19개·화면/소스 대조47개 통과, JavaScript 오류0건입니다. flex/grid·padding/border·부모 크기 변경·숨긴 컨테이너·생성 후 옵션 대입과 재생성도 확인했습니다. 다른 브라우저와 전체 조합 검사를 뜻하지 않습니다. 소스0.5.1, 실제 Nexus0.5.0입니다. 검사 결과 · Vue 비교 화면
2026-10-22 · 0.5.2 기본 오류 표시1500ms 사용자 요청으로 errorDuration 기본값을1500ms로 변경했습니다. 생성 기본 숫자만 변경하고 기존 타이머 로직과 직접 지정한 시간/0 동작은 유지합니다. 이전0.5.0의2000ms 기록은 당시 검사 결과입니다.
실제 Chrome 전용13개 검사 통과, JavaScript 오류0건. 기본1500ms·명시2000ms·사용자 시간/0·연속 오류·로딩/언어·검증·빈 상태·재시도·종료·JS/Vue 예제를 확인했습니다. 소스0.5.2는 미배포이며 Nexus 최신은0.5.1입니다. 현재 검사 결과 · Vue 예제
2026-10-22 · 0.6.0 부모 남은 공간 자동 높이 height: auto는 부모의 세로 흐름에서 검색·하단·gap·여백을 뺀 공간을 채웁니다. 부모가 내용 높이 auto이면 화면 높이를 기준으로 합니다. 전용 host와 세로 부모당 자동 그리드 하나를 기준으로 하며 기존 숫자·퍼센트 설정은 유지합니다. 크기 변경 시 재생성하지 않습니다.
Chrome 전용16개·기존 퍼센트12개·JS/Vue 화면/소스48개·설치 패키지2개 통과, JavaScript 오류0건. block/flex/grid·padding/border·부모/검색/하단 크기·형제 추가/제거·숨김/표시·화면 resize·데이터/선택/스크롤·destroy 스타일 복원을 확인했습니다. 소스0.6.0은 미배포이며 실제 Nexus 최신은0.5.2입니다. 다른 브라우저·전체 조합은 미검증입니다. 실제 결과 · Vue 예제
2026-10-22 · 0.6.1 자동 높이의 페이지 넘침 수정 수정 전10개 중8개 실패를 재현했습니다. 모바일 JS/Vue 예제는700px 화면에서929px까지 늘어났습니다. 상위 하단 여백·형제 영역·상위 높이 제한을 반영하고 예제를 max-height로 바꿨습니다. auto 예제의 매뉴얼 iframe만780px로 고정해 상호 크기 조절 순환도 끊었습니다.
최종 Chrome 전용12개·기존 auto16개·퍼센트12개·JS/Vue 화면/소스48개·설치 패키지2개 통과, JS 오류0건입니다. 390×700/1100×900 예제의 페이지 scrollHeight/clientHeight와 JS↔Vue 크기 안정성을 검사했습니다. 조상 자체의 강제 높이나 다른 콘텐츠가 화면을 넘으면 페이지 스크롤은 남을 수 있습니다. 소스0.6.1은 미배포이며 실제 Nexus 최신은0.6.0입니다. 수정 전 · 최종 결과 · 모바일 Vue 화면
2026-10-22 · 0.6.2 좌우 auto 그리드 0.6.1 원본으로 전용14개 중9개 실패를 재현했습니다. DOM 뒤쪽 옆 패널을 아래 공간으로 차감해 좌우 높이가2px/500px가 됐습니다. 실제 세로 위치·가로 겹침으로 아래 영역만 차감하고, 가로 flex에서는 host의 기존 너비 분배를 유지하도록 수정했습니다.
최종 다중 그리드14개·단일 auto16개·페이지 넘침12개·퍼센트12개·JS/Vue 화면/소스48개·설치 패키지2개 통과, JS 오류0건입니다. 좌우 높이500px/500px와 각100행·데이터/선택/스크롤 독립·크기 변경·종료 정리를 확인했습니다. 같은 부모의 상하 auto 여러 개는 높이를 자동 균등 분할하지 않습니다. 소스0.6.2는 미등록이며 Nexus 최신은0.6.1입니다. 수정 전 · 최종 결과 · 좌우 그리드 화면
2026-10-22 · 0.6.3 함수·속성 매뉴얼 대조 전체 함수 API 메뉴(Core54개·플러그인12개·export12개·독립 GridModel17개), setRows/insert/insertRows/clearRows의100행 JS/Vue 예제, 그리드/컬럼49/54개 및 객체 내부40/6개 속성을 정리했습니다. 존재하지 않는 setColumns 안내와 오래된 붙여넣기 설명도 수정했습니다. 그리드 실행 소스는 변경하지 않았습니다.
정적 대조5개·실제 Chrome8개·JS/Vue 화면/소스49개 통과, JS 오류0건입니다. 직접 링크/메뉴 검색/속성 행·교체/추가/초기화·모델/날짜/조건 유틸리티·모바일 접근을 확인했습니다. 모든 함수 조합과 임의 플러그인 전체를 실행 검증한 것은 아닙니다. 소스0.6.3은 미등록이며 Nexus 최신은0.6.2입니다. 정적 대조 · 실제 브라우저 결과 · 모바일 설명 화면
2026-10-23 · 0.7.0 매뉴얼 메뉴·통합 검색 49개 예제를 관련13개 주제로 재분류하고 본문·함수·속성·HTML/JS/Vue/행 데이터 소스 검색을 추가했습니다. 검증 기록·버전 이력은 검색에서 제외합니다. 그리드 실행 소스는 변경하지 않았습니다.
실제 Chrome 검색12개·기존 API8개·화면/소스49개, 정적 API5개·JS325개 구문/HTML/import/버전 검사를 통과했고 JS 오류0건입니다. 결과 문맥·함수/속성명·숨김 Vue/이벤트 탭·그리드 유지·기존 앵커·AND/대소문자·더 보기·키보드·390px 모바일을 확인했습니다. 소스0.7.0은 미등록이며 실제 Nexus 최신은0.6.2입니다. 검색 검사 · 검색 화면 · 모바일 화면
2026-10-23 · 0.7.0 사내 등록·원격 설치 확인 소스 커밋7bd3b13을 origin/master에 푸시하고 검증한 tgz를 사내 Nexus에 등록했습니다. 최종 정적 검사·로컬 패키지2개, 새 캐시 원격 설치/SHA512/Chrome·Vue 검사3개 통과·JS 오류0건입니다. 실제 사내 최신은0.7.0입니다. 앞선 미등록 설명은 당시 작업 상태입니다. 로컬 패키지 · 원격 설치 검사
2026-10-23 · 0.7.1 Vue 로딩·실패·설치 안내 사용자는 npm install 후 문제가 해결됐다고 확인했습니다. Git 예제 실행 절차에 설치 단계를 보완했습니다. 별도로 모듈을 지연시키자0.7.0이 기존 iframe을 즉시 비워 로딩 중 빈 화면을 표시하는 것을 재현했습니다. 이전 수정은 완료 후 위치만 복구했습니다. 이제 새 예제가 렌더될 때까지 기존 그리드를 유지하고 실패 시 설치/재시도 안내를 표시합니다. 그리드 실행 src는 변경하지 않았습니다.
Chrome 전용9개·전체 프레임478개·화면/소스49개·검색12개·API8개 통과, JS 오류0건. 지연·의존성503·재시도·시간 초과·취소·반복 전환·모바일·높이 관찰을 확인했습니다. 소스0.7.1은 미등록이며 실제 Nexus는0.7.0입니다. 수정 전 측정 · 전용 검사 · 전체 전환 검사 · Vue 전환 화면