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