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를 지원하는 브라우저에서만 동작함