)
}
}
)
(
}
{
)
)
(
)
(
(
{
}
)
(
)
}
)
)
{
(
(
)
)
}
)
(
}

Todo MVC (not mvc)

  1. <!DOCTYPE html>
  2. <html lang="en">
  3.   <head>
  4.     <meta charset="utf-8" />
  5.     <title></title>
  6.     <style>
  7.       body {
  8.         background: white;  
  9.       }
  10.       .new-todo {
  11.         padding: 4px;
  12.       }
  13.       .todo {
  14.         border: 1px solid black;
  15.         overflow: hidden;
  16.         background: #efefef;
  17.       }
  18.       .todo-close {
  19.         display: none;
  20.       }
  21.       .todo:hover .todo-close {
  22.         display: block;
  23.       }
  24.       .todo * {
  25.         float: left;
  26.         padding: 10px; 
  27.       }
  28.       .todo-complete {
  29.         background: green;
  30.       }
  31.       .todo-complete .todo-val {
  32.         text-decoration: line-through;
  33.         color: white;
  34.       }
  35.  
  36.       .show-active .todo-complete {
  37.         display: none;
  38.       }
  39.  
  40.       .show-complete .todo:not(.todo-complete) {
  41.         display: none;
  42.       }
  43.       .todo-edit {
  44.         background: gray !important;
  45.         color: black !important;
  46.         text-decoration: none !important;
  47.       }
  48.       .todo-name {
  49.         width: 200px;  
  50.         border: 1px solid gray;
  51.       }
  52.  
  53.       .check-all[data-on="true"] {
  54.         font-weight: bold;
  55.  
  56.         background: darkGray;
  57.       }
  58.     </style>
  59.   </head>
  60.   <body>
  61.     <div class="app">
  62.       <button class="check-all">&#9660;</button>
  63.       <input class="todo-name" type="text" placeholder="What needs to be done?"/>
  64.       <div class="todos"></div>
  65.       <div class="todo-foot">
  66.         <span>
  67.           <span class="todo-num">#</span>
  68.           item<span class="plural">s</span>
  69.           left
  70.         </span>
  71.         <button class="all">all</button>
  72.         <button class="active">active</button>
  73.         <button class="completed">completed</button>
  74.         <button class="clear-completed">clear completed</button>
  75.       </div>
  76.     </div>
  77.  
  78.     <script id="todo" type="text/html">
  79.       <div class="todo">
  80.         <input class="todo-check" type="checkbox" />
  81.         <div class="todo-val">$val</div>
  82.         <button class="todo-close">X</button>
  83.       </div>
  84.     </script> 
  85.  
  86.     <script>
  87.       (function() {  
  88.         var todoName = document.querySelector('.todo-name'),
  89.             todoTmpl = document.querySelector('#todo').innerHTML,
  90.             todos = document.querySelector('.todos'),
  91.             todoFoot = document.querySelector('.todo-foot'),
  92.             todoNum = document.querySelector('.todo-num'),
  93.             plural = document.querySelector('.plural'),
  94.             checkAll = document.querySelector('.check-all'),
  95.             evts = {};
  96.  
  97.         function save() {
  98.           localStorage.todos = todos.innerHTML;
  99.         }
  100.  
  101.         function addTodo(value) {
  102.           if (value.trim().length > 0) {
  103.             todoName.value = '';
  104.             todos.innerHTML += todoTmpl.replace('$val', value);
  105.             generalUpdate();
  106.           }
  107.         }
  108.  
  109.         function updateProp(target, attr, val) {
  110.           val ? target.setAttribute(attr, attr) : target.removeAttribute(attr);
  111.           target[attr] = val;
  112.         }
  113.  
  114.         function setTodoComplete(todo, val) {
  115.           if (val) {
  116.             todo.classList.add('todo-complete');
  117.           } else {
  118.             todo.classList.remove('todo-complete');
  119.             checkAll.setAttribute('data-on', false); 
  120.             localStorage.setItem('allChecked', false);
  121.           }
  122.           updateProp(todo.querySelector('.todo-check'), 'checked', val);
  123.         }
  124.  
  125.         function updateItemsLeft(todoCount) {
  126.           todoNum.innerHTML = todoCount;
  127.           plural.style.display = todoCount === 1 ? 'none' : 'inline';
  128.         }
  129.  
  130.         function handleNoItems(todoCount) {
  131.           todoFoot.style.display = todoCount === 0 ? 'none' : 'block';
  132.         }
  133.  
  134.         function generalUpdate() {
  135.           var todoCount = document.querySelectorAll('.todo').length,
  136.               todoCompleted = document.querySelectorAll('.todo-complete');
  137.           updateItemsLeft(todoCount - todoCompleted.length);
  138.           handleNoItems(todoCount);
  139.           save();
  140.         }
  141.  
  142.         evts['check-all-click'] = function() {
  143.           var todos = document.querySelectorAll('.todo'),
  144.               leng = todos.length,
  145.               val = checkAll.allChecked = !checkAll.allChecked;
  146.           for (var i = 0; i < leng; i++) {
  147.             setTodoComplete(todos[i], val);
  148.           }
  149.           checkAll.setAttribute('data-on', val); 
  150.           localStorage.setItem('allChecked', val);
  151.         };
  152.  
  153.         evts['todo-close-click'] = function(e, todo) {
  154.           todos.removeChild(todo);
  155.         };
  156.  
  157.         evts['todo-check-click'] = function(e, todo) {
  158.           setTodoComplete(todo, e.target.checked);
  159.         };
  160.  
  161.         evts['all-click'] = function() {
  162.           todos.classList.remove('show-active');
  163.           todos.classList.remove('show-complete');
  164.           delete localStorage.mode;
  165.         };
  166.  
  167.         evts['active-click'] = function() {
  168.           evts['all-click']();
  169.           todos.classList.add('show-active');
  170.           localStorage.mode = 'active';
  171.         };
  172.  
  173.         evts['completed-click'] = function() {
  174.           evts['all-click']();
  175.           todos.classList.add('show-complete');
  176.           localStorage.mode = 'completed';
  177.         };
  178.  
  179.         evts['clear-completed-click'] = function(e, todo) {
  180.           var completed = document.querySelectorAll('.todo-complete'), 
  181.               leng = completed.length;
  182.           for (var i = 0; i < leng; i++) {
  183.             completed[i].parentNode.removeChild(completed[i]);
  184.           }
  185.         };
  186.  
  187.         todoName.addEventListener('change', function(e) {
  188.           addTodo(todoName.value);
  189.         });
  190.  
  191.         document.addEventListener('click', function(e) {
  192.           var key = e.target.className + '-click',
  193.               todo = e.target.parentNode;
  194.           if (evts[key] != null) {
  195.             evts[key](e, todo);
  196.             generalUpdate();
  197.           }
  198.         });
  199.  
  200.         function todoBlur(e) {
  201.           e.target.removeEventListener('blur', todoBlur);
  202.           e.target.classList.remove('todo-edit');
  203.           e.target.contentEditable = false;
  204.           if (e.target.innerHTML.trim().length === 0) {
  205.             e.target.parentNode.parentNode.removeChild(e.target.parentNode);  
  206.           }
  207.           save();
  208.         }
  209.  
  210.         document.addEventListener('dblclick', function(e) {
  211.           var range, sel;
  212.           if (e.target.className === 'todo-val') {
  213.             e.target.contentEditable = true;
  214.             e.target.initialVal = e.target.innerHTML;
  215.             e.target.focus();
  216.             e.target.addEventListener('blur', todoBlur);
  217.             e.target.classList.add('todo-edit');
  218.  
  219.             // put cursor at end
  220.             range = document.createRange();
  221.             range.selectNodeContents(e.target);
  222.             range.collapse(false);
  223.             sel = window.getSelection();
  224.             sel.removeAllRanges();
  225.             sel.addRange(range);
  226.           }
  227.         });
  228.  
  229.         document.addEventListener('keydown', function(e) {
  230.           if (document.activeElement.classList.contains('todo-val')) {
  231.             if (e.which === 27) { // ESC
  232.               e.target.innerHTML = e.target.initialVal;
  233.               todoBlur(e);  
  234.             } else if (e.which === 13) { // ENTER
  235.               todoBlur(e);  
  236.             }
  237.           }
  238.         });
  239.  
  240.         document.addEventListener('keyup', function(e) {
  241.           if (document.activeElement.classList.contains('todo-val')) {
  242.             save();
  243.           }
  244.         });
  245.  
  246.         if (localStorage.todos != null) todos.innerHTML = localStorage.todos;
  247.         if (localStorage.allChecked === 'true') evts['check-all-click']();
  248.         if (localStorage.mode != null) evts[localStorage.mode + '-click']();
  249.  
  250.         generalUpdate();
  251.  
  252.       })();
  253.     </script>
  254.   </body>
  255. </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 😀

// javascript // tricks // ui
snippet.zone ~ 2021-24 /// {s/z}