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 Controller from './src/controller.js';

Controller.create();

controller

js
import View from './view.js';
import Model from './model.js';
import {query} from '../utils/dom.js';

class Controller {
  constructor() {
    const parentNode = query(document, '#app');
    const view = View.mount({parentNode, controller: this});
    const model = Model.create();

    Object.assign(this, {view, model});

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

  addTodoItem (item) {
    this.model.addTodoItem(item)
  }
  removeTodoItem (item) {
    this.model.removeTodoItem(item)
  }

  static create () {
    return new Controller();
  }
}

export default Controller

view

js
import {clone, html, query} from '../utils/dom.js';

class View {
  constructor({controller, parentNode}) {
    const dom = View.template();
    Object.assign(this, {
      controller,
      input: query(dom, 'input[type=text]'),
      button: query(dom, 'button'),
      list: query(dom, 'ol'),
      summary: query(dom, 'h2'),
    });

    parentNode.appendChild(dom);
    this.bindEvent();
  }
  bindEvent () {
    this.button.addEventListener('click', () => {
      this.controller.addTodoItem(this.input.value);
      this.input.value = '';
    });
  }
  render (model) {
    this.renderTodoList(model);
    this.renderTodoSummary(model);
  }
  renderTodoList (model) {
    const {todoList} = model;
    const fragment = document.createDocumentFragment();
    const li = html('li');

    todoList
      .map((todo) => {
        const clonedLi = clone(li, {
          innerHTML: View.liTemplate(todo)
        });
        query(clonedLi, 'button')
          .addEventListener('click', () => {
            this.controller.removeTodoItem(todo)
          });

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

    this.list.innerHTML = '';
    this.list.appendChild(fragment);
  }
  renderTodoSummary (model) {
    const {todoList} = model;
    this.summary.innerHTML = `TODO: ${todoList.length}`
  }
  static template () {
    return html('div', {
      innerHTML: `
        <h2></h2>
        <input type="text">
        <button type="button">Add</button>
        <ol></ol>
      `
    })
  }
  static liTemplate (todo) {
    return `
      <span id="${todo.id}">${todo.item}</span>
      <button type="text">X</button>
    `
  }
  static mount({controller, parentNode}) {
    return new View({controller, parentNode})
  }
}

export default View

dom

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

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

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

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

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';

class Model {
  constructor() {
    Object.assign(this, {
      data: {
        id: 0,
        todoList: [],
      },
      subject: createSubject()
    })
  }
  get cloneData () {
    return {todoList: [...this.data.todoList]}
  };

  notify() {
    this.subject.notify(this.cloneData)
  }
  subscribe (observer) {
    this.subject.subscribe(observer);
    observer(this.cloneData)
  }
  unsubscribe (observer) {
    this.subject.unsubscribe(observer)
  }

  addTodoItem (item) {
    const newTodo = {id: this.data.id++, item};
    this.data.todoList = [...this.data.todoList, newTodo];
    this.notify();
  }
  removeTodoItem (item) {
    this.data.todoList = this.data.todoList
      .filter((todoItem) => todoItem !== item);
    this.notify();
  }

  static create() {
    return new Model();
  }
}

export default Model

데모

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