AUISoft에이유아이소프트
AUIGrid 3.0.20 Documentation

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를 참고하십시오.

문서 홈으로 돌아가기