Skip to content

참고자료: Maria - The MVC Framework for JavaScript Application

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';
import Model from './src/model.js';
import View from './src/view.js';

const model = new Model();
const view = new View(model);
const controller = new Controller(model, view);

view.setController(controller);

document
  .getElementById('app')
  .appendChild(view.build());

model

js
import Subject from '../utils/subject.js';

class Model extends Subject {
  constructor() {
    super();

    Object.assign(this, {
      state: {
        id: 0,
        todoList: [],
      },
    });

    this.addTodoItem('default item1');
    this.addTodoItem('default item2');
  }

  getState () {
    return {
      todoList: [...this.state.todoList]
    }
  }

  addTodoItem (text) {
    const newTodo = {
      id: this.state.id++,
      item: text
    };
    this.state.todoList = [...this.state.todoList, newTodo];
    this.notify();
  }

  removeTodoItem (item) {
    this.state.todoList = this.state.todoList
      .filter((todoItem) => {
        return !(todoItem.item === item.item && todoItem.id === item.id)
      });
    this.notify();
  }
}

export default Model

subject

js
export default class Subject {
  constructor() {
    this.set = new Set()
  }
  notify (value) {
    this.set.forEach(observer => observer.update(value))
  }
  subscribe (observer) {
    this.set.add(observer)
  }
  unsubscribe (observer) {
    this.set.remove(observer)
  }
}

view

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

class View {
  constructor(model) {
    Object.assign(this, {model});

    this.model.subscribe(this);
  }

  build() {
    const dom = View.template();

    Object.assign(this, {
      input: query(dom, 'input[type=text]'),
      button: query(dom, 'button'),
      list: query(dom, 'ol'),
      summary: query(dom, 'h2'),
    });

    this.render();
    this.bindEvent();
    return dom;
  }

  render () {
    this.renderTodoList();
    this.renderTodoSummary();
  }

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

        return clonedLi;
      })
      .reduce((fragment, clonedLi) => {
        fragment.appendChild(clonedLi);
        return fragment;
      }, document.createDocumentFragment());

    this.list.innerHTML = '';
    this.list.appendChild(fragment);
  }

  renderTodoSummary () {
    const {todoList} = this.model.getState();
    this.summary.innerHTML = `TODO: ${todoList.length}`
  }

  bindEvent () {
    this.button.addEventListener('click', () => {
      this.controller.addTodoItem(this.input.value);
      this.input.value = '';
    });
  }

  update() {
    this.render();
  }

  setController (controller) {
    Object.assign(this, {controller});
  }

  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>
    `
  }
}

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)
};

controller

js
class Controller {
  constructor(model, view) {
    Object.assign(this, {model, view})
  }

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

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

export default Controller

데모

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