Vanilla JS TodoList Project Version3

DW J·2022년 9월 2일
0

project-todolist

목록 보기
4/4

Version3

하나의 파일에 있던 로직을 내 방식대로 아래와 같이 변경

  • 각각의 파일은 모듈단위로 생성
  • 추가 삭제 수정 등을 관리하는 todoData.js
  • DOM의 직접적인 제어를 담당하는 todoView.js
  • 사용자 액션에 의한 이벤트를 관리하는 todo.js

1. todo.js

  • Todo.js는 사용자 액션에 의한 이벤트 핸들러를 관리한다
  • 이벤트 핸들러 내부에서 data와 view에게 필요한 데이터를 넘겨준다
// TODO lib
import utilitylib from "./lib/utilitylib.js";

// TODO Data
import todoData from "./todoData.js";

// TODO View
import todoView from "./todoView.js";

const todos = {
    handler: {},

    // TODO 초기화
    init: function () {
        this.initHandler();
        todoData.init();
        todoView.init(this.handler);
    },

    initHandler: function () {
        this.handler = {
            handleInputAddKeyup: this.handleInputAddKeyup.bind(this),
            handleButtonAddClick: this.handleButtonAddClick.bind(this),
            handleCompleteClick: this.handleCompleteClick.bind(this),
            handleContentDbclick: this.handleContentDbclick.bind(this),
            handleUpdateContentKeyup: this.handleUpdateContentKeyup.bind(this),
            handleUpdateContentFocusout: this.handleUpdateContentFocusout.bind(this),
            handleRemoveClick: this.handleRemoveClick.bind(this)
        }
    },

    // TODO HANDLER
    handleInputAddKeyup: function (e) {
        if (e.key === "Enter") {
            const { target } = e;
            if (utilitylib.emptyValueCheck(target.value, "내용을 입력해 주세요")) return;
            const viewData = todoData.add(target.value);

            todoView.viewAdd(viewData);
            target.value = "";
        }
    },

    handleButtonAddClick: function (e) {
        const { target } = e;
        const input = target.previousElementSibling;
        if (utilitylib.emptyValueCheck(input.value, "내용을 입력해 주세요")) return;
        const viewData = todoData.add(input.value);

        todoView.viewAdd(viewData);
        input.value = "";
    },

    handleCompleteClick: function (e) {
        const { target } = e;
        const id = utilitylib.getId(e.target);
        const checked = target.checked;

        todoData.complete(id, checked);
        todoView.viewComplete(id, checked);
    },

    handleContentDbclick: function (e) {
        const id = utilitylib.getId(e.target);
        todoView.viewModify(id);
    },

    handleUpdateContentKeyup: function (e) {
        if (e.key === "Enter") {
            const { target } = e;
            const id = utilitylib.getId(e.target);
            const value = target.value;

            if (utilitylib.emptyValueCheck(value, "내용을 입력해주세요.")) return;

            todoData.update(id, value);
            todoView.viewUpdate(target, value);
        }
    },

    handleUpdateContentFocusout: function (e) {
        const { target } = e;
        const id = utilitylib.getId(e.target);
        const value = target.value;

        if (utilitylib.emptyValueCheck(value, "내용을 입력해주세요.")) return;

        todoData.update(id, value);
        todoView.viewUpdate(target, value);
    },

    handleRemoveClick: function (e) {
        const id = utilitylib.getId(e.target);

        todoData.remove(id);
        todoView.viewRemove(id);
    }
}

todos.init();

2. todoData.js

  • todoData.js는 todolist를 구성하는 데이터를 관리한다
  • element(view)와 관련 된 어떠한 로직도 존재하지 않아야 한다
  • 서버와 통신을 위한 API는 todoData.js에서 구현한다(예정)
// TODO lib
import utilitylib from "./lib/utilitylib.js";

// TODO DATA 객체
const todoData = {
    _itemList: [],

    init: function () {
        this.fetchData();
    },

    fetchData: function () {
        // API로 가져온 데이터 셋팅 - 서버구현 후 사용
        // this._itemList = this.fetchDataItems();        
        return this._itemList;
    },

    getItem: function (id) {
        return this._itemList.filter(item => item.id === id);
    },

    add: function (content) {
        const additem = { id: utilitylib.uuid(), complete: false, content };
        const completeTodoIdx = this._itemList.findIndex(item => item.complete);

        if (completeTodoIdx > -1) {
            this._itemList.splice(completeTodoIdx, 0, additem);
        }
        else {
            this._itemList.push(additem);
        }

        return additem;
    },

    remove: function (id) {
        const removeitem = this.getItem(id);
        this._itemList.splice(removeitem, 1);
    },

    update: function (id, content) {
        this._itemList.forEach(item => {
            if (item.id === id) {
                item.content = content;
                return false; // forEach break
            }
        });
    },

    complete: function (id, isComplete) {
        const updateItemIndex = this._itemList.findIndex(item => item.id === id);
        const updateItem = this._itemList.splice(updateItemIndex, 1);
        const itemListLength = this._itemList.length;

        updateItem[0].complete = isComplete;

        if (isComplete) {
            this._itemList.splice(itemListLength, 0, updateItem[0]);
        }
        else {
            this._itemList.splice(0, 0, updateItem[0]);
        }
    },
}

export default todoData;

3. todoView.js

  • todoView.js는 element추가, 생성, 삭제 등 view와 관련 된 로직을 포함한다
  • API나 그와 관련 된 데이터를 가공하는 로직은 존재하지 않아야 한다
// TODO lib
import elementlib from "./lib/elementlib.js";
import utilitylib from "./lib/utilitylib.js";

// TODO view 객체
const todoView = {
    _viewWrapper: null,
    _viewHeader: null,
    _viewContents: null,
    _viewElementList: null,
    handler: null,

    // TODO 초기화
    init: function (handler) {
        this.handler = handler;
        this.initViewCreate();
        this.render();
    },

    initViewCreate: function () {
        this._viewWrapper = this.initViewWrapperCreate();
        this._viewHeader = this.initViewHeaderCreate();
        this._viewContents = this.initViewContentsCreate();
    },

    initViewWrapperCreate: function () {
        const viewWrapperData = {
            tagName: "div",
            attrs: { className: "wrapper-todo" }
        };

        return elementlib.elementCreate(viewWrapperData);
    },

    initViewHeaderCreate: function () {
        const viewHeaderData = {
            tagName: "div",
            attrs: { className: "todo-header" },
            children: [
                {
                    tagName: "div",
                    attrs: { className: "todo-title" },
                    children: [{ tagName: "h2", attrs: { textContent: "TODO TITLE" } }]
                },
                {
                    tagName: "div",
                    attrs: { className: "todo-date-contents" },
                    children: [
                        { tagName: "span", attrs: { className: "todo-year", textContent: "====" } },
                        { tagName: "span", attrs: { className: "todo-month", textContent: "==" } },
                        { tagName: "span", attrs: { className: "todo-date", textContent: "==" } },
                        { tagName: "span", attrs: { className: "todo-day" } }
                    ]
                }
            ]
        };

        return elementlib.elementCreate(viewHeaderData);
    },

    initViewContentsCreate: function () {
        const viewContentsData = {
            tagName: "div",
            attrs: { className: "todo-contents" },
            children: [
                {
                    tagName: "div",
                    attrs: { className: "todo-add-contents" },
                    children: [
                        {
                            tagName: "input",
                            attrs: { className: "add-input", type: "text" },
                            events: [{ type: "keyup", handler: this.handler.handleInputAddKeyup }]
                        },
                        {
                            tagName: "button",
                            attrs: { className: "add-button", type: "button", textContent: "+" },
                            events: [{ type: "click", handler: this.handler.handleButtonAddClick }]
                        }
                    ]
                },
                {
                    tagName: "ul",
                    attrs: { className: "todo-list" }
                },
                {
                    tagName: "div",
                    attrs: { className: "todo-update-guide",  }
                }
            ]
        };

        return elementlib.elementCreate(viewContentsData);
    },

    // TODO render
    render: function () {
        this.renderView();
        this.renderViewSetting();
        // this.renderViewElementList();
    },

    renderView: function () {
        this._viewWrapper.appendChild(this._viewHeader);
        this._viewWrapper.appendChild(this._viewContents);

        document.body.appendChild(this._viewWrapper);
    },

    renderViewSetting: function () {
        this.renderViewElementListSetting();
        this.renderViewDateElementSetting();
    },

    renderViewElementListSetting: function () {
        this._viewElementList = elementlib.getElement(".todo-list");
    },

    renderViewDateElementSetting: function () {
        const currentDate = utilitylib.getDate();
        const year = elementlib.getElement(".todo-year");
        const month = elementlib.getElement(".todo-month");
        const date = elementlib.getElement(".todo-date");

        elementlib.setElementAttribute(year, { textContent: currentDate.year + "년" });
        elementlib.setElementAttribute(month, { textContent: currentDate.month + "월" });
        elementlib.setElementAttribute(date, { textContent: currentDate.date + "일" });
    },

    // renderViewElementList: function () {
    //     elementList?.forEach(element => {
    //         const viewElement = this.viewCreate(element);
    //         this._viewElementList.appendChild(viewElement);
    //     });
    // },

    // TODO DOM조작
    viewCreate: function (viewData) {
        const { id, content, complete} = viewData;
        const viewElementData = {
            tagName: "li",
            attrs: { id, className: (complete) ? "disabled" : "" },
            children: [
                {
                    tagName: "input",
                    events: [{ type: "click", handler: this.handler.handleCompleteClick }],
                    attrs: { className: "todo-checkbox", type: "checkbox", checked: complete }
                },
                {
                    tagName: "div",
                    events: [{ type: "dblclick", handler: this.handler.handleContentDbclick }],
                    attrs: { className: "todo-content", textContent: content },
                    children: [
                        {
                            tagName: "input",
                            events: [
                                { type: "keyup", handler: this.handler.handleUpdateContentKeyup },
                                { type: "focusout", handler: this.handler.handleUpdateContentFocusout }
                            ],
                            attrs: { type: "text", className: "hide" }
                        }
                    ]
                },
                {
                    tagName: "span",
                    events: [{ type: "click", handler: this.handler.handleRemoveClick }],
                    attrs: { className: "todo-remove", textContent: "X" }
                }
            ]
        }

        return elementlib.elementCreate(viewElementData);
    },


    viewAdd: function (viewElementData) {
        const viewElement = this.viewCreate(viewElementData);
        const viewCompleteElement = elementlib.getElement('.disabled');

        if (viewCompleteElement) {
            this._viewElementList.insertBefore(viewElement, viewCompleteElement);
        }
        else {
            this._viewElementList.appendChild(viewElement);
        }
    },

    viewComplete: function (id, isComplete) {
        const viewCompleteElement = elementlib.getElement("#" + id);

        if (isComplete) {
            elementlib.addClass(viewCompleteElement, "disabled");
            this._viewElementList.appendChild(viewCompleteElement);
        }
        else {
            elementlib.removeClass(viewCompleteElement, "disabled");
            this._viewElementList.prepend(viewCompleteElement);
        }
    },

    viewModify: function (id) {
        const viewElementParent = elementlib.getElement("#" + id);
        const viewModeElement = Array.prototype.slice.apply(viewElementParent.childNodes)
        .filter(node => node.classList.value === "todo-content")[0];

        if (viewModeElement.tagName !== "INPUT" && elementlib.hasClass(viewElementParent, "disabled") === false) {
            const modifyModeElement = viewModeElement.querySelector("input"); // TODO 변경
            if (elementlib.hasClass(modifyModeElement, "show")) return;

            const viewElementValue = elementlib.getText(viewModeElement);
            elementlib.setText(viewModeElement, "");

            elementlib.setValue(modifyModeElement, viewElementValue);
            elementlib.addClass(modifyModeElement, "show");
            elementlib.removeClass(modifyModeElement, "hide");

            viewModeElement.appendChild(modifyModeElement);
            modifyModeElement.focus();
        }
    },

    viewUpdate: function (target, value) {
        const viewElementParent = target.parentElement; // elementlib로 빼야하나?
        const updateTextNode = elementlib.createText(value);

        elementlib.addClass(target, "hide");
        elementlib.removeClass(target, "show");
        viewElementParent.appendChild(updateTextNode);
    },

    viewRemove: function (id) {
        const viewRemoveElement = elementlib.getElement("#" + id);
        this._viewElementList.removeChild(viewRemoveElement);
    },
}

export default todoView;

4. 앞으로 진행 할 내용들

4-1) 기능관련 내용 추가

  • 드래그 앤 드랍 기능 추가
  • 국가별로 날짜가 보여질 수 있는 기능 추가
  • 애니메이션 효과 추가

4-2) 서버 및 DB 작업

  • 서버 작업 진행 - node express 사용
  • DB 작업 진행 - Mysql 사용

4-3) AWS에 실제 배포

4-4) 리팩토링 작업 진행


09/07 내용 추가

server와 DB작업 후 data와 view의 구조적인 문제로 인해 리팩토링 작업 진행 예정..ㅠㅠ

profile
잘하는것보다 꾸준히하는게 더 중요하다

0개의 댓글