Skip to content

MVC는 2020.08 MVC에 설명한다.

app

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<body>
<div id="app"></div>
<script type="module" src="app.js"></script>
</body>
</html>
js
import {createController} from './src/controller.js';

createController();

controller

js
import {createView} from './view.js';
import {createModel} from './model.js';
import {query} from '../utils/dom.js';

export const createController = () => {
  const parentNode = query(document, '#app');
  const model = createModel();
  const controller = {
    addTodoItem: item => {
      model.addTodoItem(item)
    },
    removeTodoItem: item => {
      model.removeTodoItem(item)
    }
  };
  const view = createView({parentNode, controller});

  model.subscribe(data => {
    view.render(data);
  })
};

view

js
import {assign} from '../utils/helper.js'
import {append, clone, events, html, query} from '../utils/dom.js';

const template = () => html('div', {
  innerHTML: `
    <h2></h2>
    <input type="text">
    <button type="button">Add</button>
    <ol></ol>
  `
});
const liTemplate = todo => `
  <span id="${todo.id}">${todo.item}</span>
  <button type="text">X</button>
`;

export const createView = ({controller, parentNode}) => {
  const dom = template();
  const state = {
    input: query(dom, 'input[type=text]'),
    button: query(dom, 'button'),
    list: query(dom, 'ol'),
    summary: query(dom, 'h2'),
  };

  const render = (model) => {
    renderTodoList(model);
    renderTodoSummary(model);
  };
  const renderTodoList = (model) => {
    const {todoList} = model;
    const fragment = document.createDocumentFragment();
    const li = html('li');

    todoList
      .map((todo) => {
        const clonedLi = clone(li, {
          innerHTML: liTemplate(todo)
        });

        events(query(clonedLi, 'button'), {
          click: () => {
            controller.removeTodoItem(todo)
          }
        });

        return clonedLi;
      })
      .forEach((clonedLi) => {
        append(fragment, clonedLi);
      });

    assign(state.list, {innerHTML: ''});
    append(state.list, fragment);
  };
  const renderTodoSummary = (model) => {
    const {todoList} = model;
    assign(state.summary, {
      innerHTML: `TODO: ${todoList.length}`
    });
  };

  const mount = () => {
    append(parentNode, dom);
    events(state.button, {
      click: () => {
        controller.addTodoItem(state.input.value);
        state.input.value = '';
      }
    });
  };

  mount();

  return {render}
};

helper

js
export const assign = (node, props) => {
  return Object.assign(node, props);
};

dom

js
import {assign} from './helper.js'

export const html = (tagName, props = {}) => {
  return assign(document.createElement(tagName), props);
};

export const clone = (node, props) => {
  return assign(node.cloneNode(true), props);
};

export const query = (parentNode, selector) => {
  return parentNode.querySelector(selector)
};

export const events = (node, options) => {
  Object
    .entries(options)
    .forEach(([eventName, listener]) => {
      node.addEventListener(eventName, listener)
    })
};

export const append = (parentNode, childNode) => {
  parentNode.appendChild(childNode)
};

model

observer

js
export const createSubject = () => {
  const set = new Set();

  return {
    notify: (value) => {
      set.forEach(observer => observer(value))
    },
    subscribe: (observer) => {
      set.add(observer)
    },
    unsubscribe: (observer) => {
      set.remove(observer)
    }
  };
};

model.js

js
import {createSubject} from '../utils/observer.js';
import {assign} from '../utils/helper.js';

export const createModel = () => {
  const state = {
    data: {
      id: 0,
      todoList: [],
    },
    subject: createSubject()
  };

  const notify = () => {
    state.subject.notify(cloneData())
  };
  const cloneData = () => {
    return {todoList: [...state.data.todoList]}
  };
  const subscribe = observer => {
    state.subject.subscribe(observer);
    observer(cloneData())
  };
  const unsubscribe = observer => {
    state.subject.unsubscribe(observer)
  };
  const addTodoItem = item => {
    const {data} = state;
    const newTodo = {id: data.id++, item};

    assign(data, {
      todoList: [...data.todoList, newTodo]
    });
    notify();
  };
  const removeTodoItem = item => {
    const {data} = state;
    const filteredTodoList = data.todoList
      .filter(todoItem => todoItem !== item);

    assign(data, {
      todoList: filteredTodoList
    });
    notify();
  };

  return {
    subscribe,
    unsubscribe,
    addTodoItem,
    removeTodoItem
  }
};

데모

ESM를 지원하는 브라우저에서만 동작함