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

Multiple Class Trick

  1. .thing.thing.thing {
  2.   color: green;
  3. }
  4.  
  5. .thing.thing {
  6.   color: red;
  7. }
  8.  
  9. .thing {
  10.   color: blue;
  11. }

What color would p.thing be?

// css // tricks // ui

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

PointerUp Before Click

  1. document.body.innerHTML += 'click/tap anywhere<br>'
  2.  
  3. document.addEventListener('click', () => {
  4.   document.body.innerHTML += 'click<br>'
  5. })
  6.  
  7. document.addEventListener('pointerup', () => {
  8.   document.body.innerHTML += 'pointerup<br>'
  9. })

pointerup fires before click

Thinking about DOM

  1. const liveData = {}
  2. const dom = new Proxy(liveData, {
  3.   get(o, key) {
  4.     if (o[key]) {
  5.       return o[key]
  6.     } else {
  7.       const fn  = node.bind(null, key)
  8.       return fn
  9.     }
  10.   }
  11. });
  12.  
  13. const tags = {}
  14. const isTag = tag => { 
  15.   if (tags[tag] != null) {
  16.     // already calculated
  17.     return tags[tag]
  18.   }
  19.   const result = !/Unknown/.test(document.createElement(tag) + '')
  20.   tags[tag] = result
  21.   return result
  22. }
  23.  
  24. const click = new WeakMap()
  25.  
  26. function pointerUp(e) { 
  27.   const curr = e.target;
  28.   if (!curr) return
  29.  
  30.   const action = click.get(curr)
  31.   if (action) action(e)
  32.  
  33.   // bubble to parent
  34.   if (curr?.parentNode?.tagName != 'BODY') {
  35.     pointerUp({ target: e.target.parentNode })
  36.   }
  37. }
  38.  
  39. document.addEventListener('pointerup', e => {
  40.   pointerUp(e)
  41. })
  42.  
  43. function attrs(el, props) {
  44.   const keys = Reflect.ownKeys(props)
  45.   for (let i = 0; i < keys.length; i++) {
  46.     const key = keys[i];
  47.     const val = props[key]
  48.     if (typeof val != 'function') {
  49.       el.setAttribute(key, val)
  50.     } else if (key === 'click') {
  51.       click.set(el, val)
  52.     }
  53.   }
  54. }
  55.  
  56. const textarea = document.createElement('textarea')
  57.  
  58. function node(type, ...args) {
  59.   let el;
  60.   const namedEl = isTag(type);
  61.   if (namedEl) {
  62.     el = document.createElement(type)
  63.   } else {
  64.     el = document.createElement('div')
  65.     el.className = type
  66.   }
  67.  
  68.   const leng = args.length
  69.   for (let i = 0; i < leng; i++) {
  70.     const arg = args[i]
  71.     if ((i === 1 || leng === 1)
  72.       && typeof arg === 'string') {
  73.         if (arg.includes('&')) {
  74.           textarea.innerHTML = arg
  75.           el.innerText = textarea.innerHTML
  76.         } else { 
  77.           el.innerText = arg
  78.         }
  79.     } else if (i === 0 && typeof arg === 'string') {
  80.       el.classList.add(arg)
  81.     } else { 
  82.       if (typeof arg === 'object') {
  83.         if (arg.el) { 
  84.           el.appendChild(arg.el)
  85.         } else {
  86.           attrs(el, arg)
  87.         }
  88.       } 
  89.     }
  90.   }
  91.   document.body.appendChild(el)
  92.   return { el }
  93. }
  94.  
  95.  
  96. const { header, h1, h2, style, div, button } = dom
  97.  
  98. style(`
  99.   body, html {
  100.     font-family: sans-serif;
  101.   }
  102.   .edit-me {
  103.     margin: 1em;
  104.     outline: 1px solid red;
  105.     padding: .5em;
  106.   }
  107. `)
  108.  
  109. header(
  110.   h1('Dom Thoughts'),
  111.   h2('sub-header', 'Can\'t help playing with this', 
  112.     { style: 'font-style: italic;' }
  113.   )
  114. )
  115. div('main', 
  116.   button('hello', 'Hello', { click: () => alert('hello') }),
  117.   div('edit-me', 'Edit Me', { contentEditable: true })
  118. )

Just playing around with DOM creation syntax, kind of reacty… This is the main part:

  1. const { header, h1, h2, style, div, button } = dom
  2.  
  3. style(`
  4.   body, html {
  5.     font-family: sans-serif;
  6.   }
  7.   .edit-me {
  8.     margin: 1em;
  9.     outline: 1px solid red;
  10.     padding: .5em;
  11.   }
  12. `)
  13.  
  14. header(
  15.   h1('Dom Thoughts'),
  16.   h2('sub-header', 'Can\'t help playing with this', 
  17.     { style: 'font-style: italic;' }
  18.   )
  19. )
  20. div('main', 
  21.   button('hello', 'Hello', { click: () => alert('hello') }),
  22.   div('edit-me', 'Edit Me', { contentEditable: true })
  23. )

What is Clicked?

  1. document.addEventListener('click', e => console.log(e.target))

Try that one in your console and then click on stuff. I use that frequently when debugging…

// dom // tricks // ui
snippet.zone ~ 2021-22 /// {s/z}