8. 사용자 정의 렌더러 만들기
CustomRenderer는 셀 표시를, CustomEditRenderer는 편집 화면을 직접 만드는
기능입니다. 숫자 3을 ★★★☆☆로 표시하고 별을 눌러 점수를 바꾸는
예제로 시작해 보겠습니다.
8.1 파일 준비
렌더러와 편집기를 각각 JS, CSS 파일로 작성하고 AUIGrid.js 뒤에 연결합니다. 외부 라이브러리는 필요하지 않습니다.
<link rel="stylesheet" href="./AUIGrid/AUIGrid_style.css">
<script src="./AUIGrid/AUIGridLicense.js"></script>
<script src="./AUIGrid/AUIGrid.js"></script>
<script src="./js/renderers/AUIGrid.RatingRenderer.js"></script>
<link rel="stylesheet" href="./js/renderers/AUIGrid.RatingRenderer.css" />
<script src="./js/editRenderers/AUIGrid.RatingEditor.js"></script>
<link rel="stylesheet" href="./js/editRenderers/AUIGrid.RatingEditor.css" />
행의 rating에는 1~5의 정수를 저장합니다. 칼럼에 dataType: "numeric"을
지정하여 편집 후에도 숫자로 저장합니다.
{ id: "S01", service: "상품 안내", rating: 3 }
8.2 CustomRenderer로 별 표시하기
AUIGrid.Class로 클래스를 만들고 AUIGrid.RendererBase를 상속합니다.
update()에서 this.data[this.dataField]를 읽어 별을 그립니다.
별을 담을 요소는 한 번만 만듭니다. 그리드가 셀을 재사용할 때는 채운 별과 빈 별의 개수만 바꿉니다.
AUIGrid.RatingRenderer.js
window.AUIGrid.RatingRenderer = window.AUIGrid.Class({
tagName: "div",
cssClass: "aui-grid-renderer-base aui-rating-renderer",
__filled: null,
__empty: null,
// 셀이 재사용되므로 별을 담을 요소는 한 번 만들고 표시 값만 갱신합니다.
update: function () {
if (!this.__filled) {
this.__filled = document.createElement("span");
this.__empty = document.createElement("span");
this.__filled.className = "aui-rating-filled";
this.__empty.className = "aui-rating-empty";
this.element.append(this.__filled, this.__empty);
this.element.setAttribute("role", "img");
}
// 데이터가 아직 없는 초기 셀도 빈 별로 표시합니다.
const value = this.data ? Number(this.data[this.dataField]) : 0;
const score = Number.isInteger(value) && value >= 1 && value <= 5 ? value : 0;
this.__filled.textContent = "★".repeat(score);
this.__empty.textContent = "☆".repeat(5 - score);
this.element.setAttribute("aria-label", score ? "5점 만점에 " + score + "점" : "미평가");
this.setHeight(this.rowHeight - 2);
},
// 직접 보관한 DOM 참조를 해제합니다.
destroy: function (unload) {
this.__filled = this.__empty = null;
this.$super.destroy(unload);
}
}).extend(window.AUIGrid.RendererBase);
AUIGrid.RatingRenderer.css
/* 별점 셀에만 적용합니다. 채운 별과 빈 별은 모양과 색상으로 구분합니다. */
.aui-rating-renderer {
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
letter-spacing: 2px;
white-space: nowrap;
}
.aui-rating-filled { color: #b77900; }
.aui-rating-empty { color: #8793a3; }
8.3 CustomEditRenderer로 점수 선택하기
AUIGrid.EditRendererBase를 상속하고 create()에서 별 버튼과 확인, 취소 버튼을
만듭니다. 현재 칼럼 정보는 this.columnData에서 바로 읽습니다.
별을 누르면 injectValue()로 적용할 점수만 전달합니다. 확인은
triggerEditEndEvent(), 취소는 triggerEditCancelEvent()를 호출합니다. 행 데이터를 직접
수정하지 않으므로 취소와 Undo/Redo가 유지됩니다.
AUIGrid.RatingEditor.js
window.AUIGrid.RatingEditor = window.AUIGrid.Class({
tagName: "div",
cssClass: "aui-grid-edit-renderer-custom aui-rating-editor",
__score: 0,
__focusTimer: null,
create: function () {
const original = this.data[this.dataField];
this.__score = Number(original) || 0;
// 선택하지 않고 닫아도 원래 점수가 보존됩니다.
this.injectValue(original);
// 현재 칼럼 정보는 엔진이 전달한 columnData에서 바로 읽습니다.
this.element.setAttribute("role", "group");
this.element.setAttribute("aria-label", this.columnData.headerText + " 선택");
const stars = document.createElement("div");
stars.className = "aui-rating-stars";
for (let score = 1; score <= 5; score++) {
const button = document.createElement("button");
button.type = "button";
button.dataset.score = score;
button.setAttribute("aria-label", score + "점");
stars.appendChild(button);
}
const actions = document.createElement("div");
actions.className = "aui-rating-actions";
// 데이터가 아닌 고정된 버튼만 HTML로 만듭니다.
actions.innerHTML = '<button type="button" data-action="confirm">확인</button> <button type="button" data-action="cancel">취소</button>';
this.element.append(stars, actions);
this.__refreshStars();
this.__onClick = this.__onClick.bind(this);
this.__onKeyDown = this.__onKeyDown.bind(this);
this.element.addEventListener("click", this.__onClick);
this.element.addEventListener("keydown", this.__onKeyDown);
// 그리드가 레이어를 배치한 뒤 첫 별에 포커스를 옮깁니다.
this.__focusTimer = setTimeout(function () { stars.firstChild.focus(); }, 0);
},
// 별을 다시 그려도 버튼은 유지하므로 키보드 포커스가 사라지지 않습니다.
__refreshStars: function () {
const score = this.__score;
this.element.querySelectorAll("[data-score]").forEach(function (button) {
const filled = Number(button.dataset.score) <= score;
button.textContent = filled ? "★" : "☆";
button.classList.toggle("is-filled", filled);
button.setAttribute("aria-pressed", Number(button.dataset.score) === score ? "true" : "false");
});
this.element.querySelector('[data-action="confirm"]').disabled = !Number.isInteger(score) || score < 1 || score > 5;
},
__onClick: function (event) {
const button = event.target.closest("button");
if (!button) return;
if (button.dataset.score) {
this.__score = Number(button.dataset.score);
// 행 데이터를 직접 바꾸지 않고 그리드에 적용 후보만 전달합니다.
this.injectValue(this.__score);
this.__refreshStars();
} else if (button.dataset.action === "confirm") {
this.triggerEditEndEvent(this.__score);
} else if (button.dataset.action === "cancel") {
this.triggerEditCancelEvent();
}
},
__onKeyDown: function (event) {
// Tab, Enter, Space는 버튼의 기본 동작을 사용하고 그리드에는 전달하지 않습니다.
event.stopPropagation();
if (event.key === "Escape") {
event.preventDefault();
this.triggerEditCancelEvent();
}
},
destroy: function (unload) {
clearTimeout(this.__focusTimer);
this.element.removeEventListener("click", this.__onClick);
this.element.removeEventListener("keydown", this.__onKeyDown);
this.$super.destroy(unload);
}
}).extend(window.AUIGrid.EditRendererBase);
AUIGrid.RatingEditor.css
/* 별점 편집기 안에만 적용합니다. */
.aui-rating-editor {
box-sizing: border-box;
width: 240px;
padding: 12px;
color: #273444;
background: #fff;
border: 1px solid #b8c8db;
border-radius: 6px;
box-shadow: 0 4px 14px rgba(0, 0, 0, .15);
}
.aui-rating-editor button { font: inherit; cursor: pointer; }
.aui-rating-editor button:focus-visible { outline: 2px solid #2877b8; outline-offset: 1px; }
.aui-rating-stars { display: flex; justify-content: center; }
.aui-rating-stars button {
width: 40px;
padding: 4px 0;
color: #8793a3;
background: transparent;
border: 0;
font-size: 30px;
line-height: 1.2;
}
.aui-rating-stars button.is-filled { color: #b77900; }
.aui-rating-actions { margin-top: 12px; text-align: right; }
.aui-rating-actions button {
padding: 6px 12px;
color: inherit;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 4px;
}
.aui-rating-actions button[data-action="confirm"] { color: #fff; background: #2877b8; border-color: #2877b8; }
.aui-rating-actions button:disabled { opacity: .45; cursor: default; }
Tab으로 버튼을 이동하고 Enter 또는 Space로 누릅니다. Esc는 취소합니다. 바깥 셀 클릭으로 편집이 끝나면
injectValue()로 전달한 점수가 적용됩니다. destroy()에서는 등록한
이벤트와 예약된 포커스를 정리합니다.
8.4 칼럼에 연결하고 실행하기
jsClass에는 클래스 자체를 전달합니다. 아래 컨테이너 다음에 스크립트를 실행하십시오.
<div id="grid_wrap" style="width: 100%; height: 300px"></div>
const columnLayout = [
{ dataField: "id", headerText: "ID", width: 90, editable: false },
{ dataField: "service", headerText: "서비스", editable: false },
{
dataField: "rating", headerText: "만족도", width: 240,
dataType: "numeric", // 편집 후에도 숫자로 저장합니다.
renderer: {
type: "CustomRenderer",
jsClass: AUIGrid.RatingRenderer
},
editRenderer: {
type: "CustomEditRenderer",
jsClass: AUIGrid.RatingEditor,
vPosition: "bottom"
}
}
];
const myGridID = AUIGrid.create("#grid_wrap", columnLayout, {
editable: true, rowHeight: 40, enableUndoRedo: true
});
AUIGrid.setGridData(myGridID, [
{ id: "S01", service: "상품 안내", rating: 3 },
{ id: "S02", service: "주문 접수", rating: 5 },
{ id: "S03", service: "배송 안내", rating: 4 },
{ id: "S04", service: "교환 접수", rating: 2 },
{ id: "S05", service: "추가 상담", rating: 1 }
]);
만족도 셀을 더블클릭하거나 선택 후 F2로 엽니다. 네 번째 별을 누르고 확인하면
rating에 숫자 4가 저장됩니다. 취소 또는 Esc는 기존
값을 유지하며 Ctrl+Z로 저장한 값을 되돌릴 수 있습니다.
8.5 내 화면에 맞게 바꾸기
별의 색상과 크기는 두 CSS 파일에서 변경합니다. 데이터 필드명을 바꾸려면 칼럼의
dataField와 행의 필드명을 함께 바꾸십시오. 렌더러와 편집기는
this.dataField를 사용하므로 그대로 재사용할 수 있습니다.
직접 추가한 속성과 메서드는 __score처럼 __로 시작해
기본 클래스와 구분합니다. 전체 설정은 CustomRenderer와
CustomEditRenderer API를 참고하십시오.