Todo MVC (not mvc)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
<style>
body {
background: white;
}
.new-todo {
padding: 4px;
}
.todo {
border: 1px solid black;
overflow: hidden;
background: #efefef;
}
.todo-close {
display: none;
}
.todo:hover .todo-close {
display: block;
}
.todo * {
float: left;
padding: 10px;
}
.todo-complete {
background: green;
}
.todo-complete .todo-val {
text-decoration: line-through;
color: white;
}
.show-active .todo-complete {
display: none;
}
.show-complete .todo:not(.todo-complete) {
display: none;
}
.todo-edit {
background: gray !important;
color: black !important;
text-decoration: none !important;
}
.todo-name {
width: 200px;
border: 1px solid gray;
}
.check-all[data-on="true"] {
font-weight: bold;
background: darkGray;
}
</style>
</head>
<body>
<div class="app">
<button class="check-all">▼</button>
<input class="todo-name" type="text" placeholder="What needs to be done?"/>
<div class="todos"></div>
<div class="todo-foot">
<span>
<span class="todo-num">#</span>
item<span class="plural">s</span>
left
</span>
<button class="all">all</button>
<button class="active">active</button>
<button class="completed">completed</button>
<button class="clear-completed">clear completed</button>
</div>
</div>
<script id="todo" type="text/html">
<div class="todo">
<input class="todo-check" type="checkbox" />
<div class="todo-val">$val</div>
<button class="todo-close">X</button>
</div>
</script>
<script>
(function() {
var todoName = document.querySelector('.todo-name'),
todoTmpl = document.querySelector('#todo').innerHTML,
todos = document.querySelector('.todos'),
todoFoot = document.querySelector('.todo-foot'),
todoNum = document.querySelector('.todo-num'),
plural = document.querySelector('.plural'),
checkAll = document.querySelector('.check-all'),
evts = {};
function save() {
localStorage.todos = todos.innerHTML;
}
function addTodo(value) {
if (value.trim().length > 0) {
todoName.value = '';
todos.innerHTML += todoTmpl.replace('$val', value);
generalUpdate();
}
}
function updateProp(target, attr, val) {
val ? target.setAttribute(attr, attr) : target.removeAttribute(attr);
target[attr] = val;
}
function setTodoComplete(todo, val) {
if (val) {
todo.classList.add('todo-complete');
} else {
todo.classList.remove('todo-complete');
checkAll.setAttribute('data-on', false);
localStorage.setItem('allChecked', false);
}
updateProp(todo.querySelector('.todo-check'), 'checked', val);
}
function updateItemsLeft(todoCount) {
todoNum.innerHTML = todoCount;
plural.style.display = todoCount === 1 ? 'none' : 'inline';
}
function handleNoItems(todoCount) {
todoFoot.style.display = todoCount === 0 ? 'none' : 'block';
}
function generalUpdate() {
var todoCount = document.querySelectorAll('.todo').length,
todoCompleted = document.querySelectorAll('.todo-complete');
updateItemsLeft(todoCount - todoCompleted.length);
handleNoItems(todoCount);
save();
}
evts['check-all-click'] = function() {
var todos = document.querySelectorAll('.todo'),
leng = todos.length,
val = checkAll.allChecked = !checkAll.allChecked;
for (var i = 0; i < leng; i++) {
setTodoComplete(todos[i], val);
}
checkAll.setAttribute('data-on', val);
localStorage.setItem('allChecked', val);
};
evts['todo-close-click'] = function(e, todo) {
todos.removeChild(todo);
};
evts['todo-check-click'] = function(e, todo) {
setTodoComplete(todo, e.target.checked);
};
evts['all-click'] = function() {
todos.classList.remove('show-active');
todos.classList.remove('show-complete');
delete localStorage.mode;
};
evts['active-click'] = function() {
evts['all-click']();
todos.classList.add('show-active');
localStorage.mode = 'active';
};
evts['completed-click'] = function() {
evts['all-click']();
todos.classList.add('show-complete');
localStorage.mode = 'completed';
};
evts['clear-completed-click'] = function(e, todo) {
var completed = document.querySelectorAll('.todo-complete'),
leng = completed.length;
for (var i = 0; i < leng; i++) {
completed[i].parentNode.removeChild(completed[i]);
}
};
todoName.addEventListener('change', function(e) {
addTodo(todoName.value);
});
document.addEventListener('click', function(e) {
var key = e.target.className + '-click',
todo = e.target.parentNode;
if (evts[key] != null) {
evts[key](e, todo);
generalUpdate();
}
});
function todoBlur(e) {
e.target.removeEventListener('blur', todoBlur);
e.target.classList.remove('todo-edit');
e.target.contentEditable = false;
if (e.target.innerHTML.trim().length === 0) {
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
}
save();
}
document.addEventListener('dblclick', function(e) {
var range, sel;
if (e.target.className === 'todo-val') {
e.target.contentEditable = true;
e.target.initialVal = e.target.innerHTML;
e.target.focus();
e.target.addEventListener('blur', todoBlur);
e.target.classList.add('todo-edit');
// put cursor at end
range = document.createRange();
range.selectNodeContents(e.target);
range.collapse(false);
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
});
document.addEventListener('keydown', function(e) {
if (document.activeElement.classList.contains('todo-val')) {
if (e.which === 27) { // ESC
e.target.innerHTML = e.target.initialVal;
todoBlur(e);
} else if (e.which === 13) { // ENTER
todoBlur(e);
}
}
});
document.addEventListener('keyup', function(e) {
if (document.activeElement.classList.contains('todo-val')) {
save();
}
});
if (localStorage.todos != null) todos.innerHTML = localStorage.todos;
if (localStorage.allChecked === 'true') evts['check-all-click']();
if (localStorage.mode != null) evts[localStorage.mode + '-click']();
generalUpdate();
})();
</script>
</body>
</html>
I’ve always liked Todo MVC. At some point a few years back I tried to re-create the behavior in a natural vanilla way (whatever was natural to me). The above is what came out at the time. Now, with es6 and some other tricks that I like to use – I think this could be even better… still fun to look at.
NOTE: It is not MVC at all – not even close 😀