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