The Shortest, Framework Free, TODO App

Published 2021-02-12

A TodoMVC App in ~200 lines of plain JavaScript and 0 dependencies.




Source:

const tempEl = document.createElement('div');
const sanitize = (value) => {
  if (value) {
    if (typeof value === 'object' && value.__html__) {
      return value.__html__;
    }
    if (Array.isArray(value)) {
      return value.map(sanitize).join('');
    }
  }
  tempEl.textContent = value;
  return tempEl.innerHTML;
};
const html = (parts, ...values) => {
  return {
    __html__: parts
      .map((part, i) => {
        return part + (i < values.length ? sanitize(values[i]) : '');
      })
      .join(''),
  };
};

const todoApp = (state) => {
  const remaining = state.items.filter((item) => !item.complete);
  let toggleAll = '';
  if (state.items.length) {
    toggleAll = html` <input
        id="toggle-all"
        type="checkbox"
        class="toggle-all"
        ${remaining.length ? '' : 'checked'}
        onclick="toggleAll();"
      />
      <label for="toggle-all">Mark all as complete</label>`;
  }
  return html`<div class="todoapp">
    ${header()}
    <section class="main" ${state.items.length ? '' : 'style="display: none;"'}>
      ${toggleAll}
      <ul class="todo-list">
        ${state.items.map(todo)}
      </ul>
      ${footer(remaining, state.items)}
    </section>
  </div>`;
};
const header = () =>
  html`<header class="header">
    <h1>todos</h1>
    <input
      type="text"
      class="new-todo"
      id="todoInput"
      placeholder="What needs to be done?"
      onkeydown="onCreate(event)"
      autofocus
      value="${state.newTodo}"
    />
  </header>`;
const todo = (item, i) => {
  if (state.filter === 'completed' && !item.complete) return '';
  if (state.filter === 'active' && item.complete) return '';
  let body = '';
  if (item._editing) {
    body = html`
      <input
        type="text"
        class="edit"
        autofocus
        value="${item.name}"
        onkeydown="onSave(event, ${i})"
        onblur="onSave(event, ${i})"
      />
    `;
  } else {
    body = html`
      <div class="view">
        <input
          type="checkbox"
          class="toggle"
          ${item.complete ? 'checked' : ''}
          onclick="toggle(${i});"
        />
        <label ondblclick="startEditing(${i})">${item.name}</label>
        <button class="destroy" onClick="remove(${i})" />
      </div>
    `;
  }
  return html`
    <li
      class="${item.complete ? 'completed' : ''} ${item._editing
        ? 'editing'
        : ''}"
    >
      ${body}
    </li>
  `;
};
const footer = (remaining, items) => {
  let clearCompleted = '';
  if (remaining.length !== items.length) {
    clearCompleted = html` <button
      class="clear-completed"
      onClick="clearCompleted()"
    >
      Clear completed
    </button>`;
  }
  return html`<footer class="footer">
    <span class="todo-count">
      <strong> ${remaining.length ? remaining.length : '0'} </strong>
      ${remaining.length === 1 ? 'item' : 'items'} left
    </span>
    <ul class="filters">
      <li>
        <a
          class="${state.filter === '' ? 'selected' : ''}"
          onClick="updateFilter('')"
        >
          All
        </a>
      </li>
      <li>
        <a
          class="${state.filter === 'active' ? 'selected' : ''}"
          onClick="updateFilter('active')"
          >Active</a
        >
      </li>
      <li>
        <a
          class="${state.filter === 'completed' ? 'selected' : ''}"
          onClick="updateFilter('completed')"
        >
          Completed
        </a>
      </li>
    </ul>
    ${clearCompleted}
  </footer>`;
};

function toggleAll() {
  const hasRemaining = state.items.filter((i) => !i.complete).length != 0;
  state.items.forEach((i) => (i.complete = hasRemaining));
  turnTheCrank();
}
function clearCompleted() {
  state.items = state.items.filter((i) => !i.complete);
  turnTheCrank();
}
function onCreate(e) {
  const text = getFinalText(e);
  if (text) {
    state.items.push({
      name: text,
      complete: false,
    });
    state.newTodo = '';
    turnTheCrank('todoInput');
  }
}
function onSave(e, i) {
  if (e.which === 27) {
    state.items[i]._editing = false;
    e.target.value = state.items[i].name;
    turnTheCrank();
    return;
  }
  const text = getFinalText(e);
  if (text) {
    state.items[i].name = text;
    state.items[i]._editing = false;
    setItems();
    turnTheCrank();
  } else if (text !== null && state.items[i] && state.items[i]._editing) {
    state.items[i]._editing = false;
    state.items.splice(i, 1);
    setItems();
    turnTheCrank();
  }
}
function toggle(i) {
  state.items[i].complete = !state.items[i].complete;
  turnTheCrank();
}
function remove(i) {
  state.items.splice(i, 1);
  turnTheCrank();
}
function startEditing(i) {
  state.items[i]._editing = true;
  turnTheCrank();
}
function updateFilter(filter) {
  window.location.hash = filter;
}
window.onhashchange = function () {
  state.filter = window.location.hash.split('#')[1] || '';
  turnTheCrank();
};
const getFinalText = (e) =>
  e.which === 13 || e.type === 'blur' ? e.target.value.trim() : null;
let state;
window.onload = () => {
  const container = document.getElementById('container');
  const prevState = window.localStorage.getItem('todos-vanilla-slim');
  state = {
    filter: window.location.hash.split('#')[1] || '',
    newTodo: '',
    items: (prevState && JSON.parse(prevState)) || [],
  };
  turnTheCrank(null, () => {
    document.querySelectorAll('.new-todo')[0].focus();
    setItems(); // apprently TodoMVC tests need this line
  });
};
function turnTheCrank(refocus, cb) {
  requestAnimationFrame(() => {
    container.innerHTML = todoApp(state).__html__;
    if (refocus) document.getElementById(refocus).focus();
    if (cb) cb();
  });
}
const setItems = (window.onbeforeunload = () =>
  window.localStorage.setItem(
    'todos-vanilla-slim',
    JSON.stringify(state.items),
  ));

https://github.com/tantaman/fk-your-frameworks-todomvc