<div id="todoapp"></div>
<footer class="info">
  <p>Double-click to edit a todo</p>
  <p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
  <p>Created by <a href="http://github.com/Cweili">Cweili</a></p>
  <p>Base On Tech <a href="https://github.com/stasm/innerself">Innerself</a></p>
</footer>
/**
 * Innerself
 * https://github.com/stasm/innerself/blob/f755b172e393726f922207fb5c08ab6deae8c67c/index.js
 */
function html([first, ...strings], ...values) {
  // Weave the literal strings and the interpolations.
  // We don't have to explicitly handle array-typed values
  // because concat will spread them flat for us.
  return values.reduce(
    (acc, cur) => acc.concat(cur, strings.shift()),
    [first]
  )

  // Filter out interpolations which are bools, null or undefined.
  .filter(x => x && x !== true || x === 0)
  .join("");
}

function createStore(reducer) {
  let state = reducer();
  const roots = new Map();
  const prevs = new Map();

  function render() {
    for (const [root, component] of roots) {
      const output = component();

      // Poor man's Virtual DOM implementation :)  Compare the new output
      // with the last output for this root.  Don't trust the current
      // value of root.innerHTML as it may have been changed by other
      // scripts or extensions.
      if (output !== prevs.get(root)) {
        prevs.set(root, output);
        root.innerHTML = output;

        // Dispatch an event on the root to give developers a chance to
        // do some housekeeping after the whole DOM is replaced under
        // the root. You can re-focus elements in the listener to this
        // event. See example03.
        const event = new CustomEvent("render", { detail: state });
        root.dispatchEvent(event);
      }
    }
  };

  return {
    attach(component, root) {
      roots.set(root, component);
      render();
    },
    connect(component) {
      // Return a decorated component function.
      return (...args) => component(state, ...args);
    },
    dispatch(action, ...args) {
      state = reducer(state, action, args);
      render();
    },
  };
}

/**
 * TodoMVC Innerself
 */
const todoStorage = {
  set(todos) {
    window.localStorage.setItem('todos', JSON.stringify(todos));
  },
  get() {
    const todos = window.localStorage.getItem('todos');
    return todos ? JSON.parse(todos) : [];
  }
};

const init = {
  todos: todoStorage.get(),
  editedTodo: null,
  filter: 'all',
  filters: {
    all: () => true,
    active: todo => !todo.completed,
    completed: todo => todo.completed
  }
};

const actions = {
  add(state, newTodo) {
    if (newTodo && newTodo.trim()) {
      state.todos.push({
        title: newTodo.trim(),
        completed: false
      });
      todoStorage.set(state.todos);
    }
  },
  toggle({ todos }, index) {
    const todo = todos[index];
    todo.completed = !todo.completed;
    todoStorage.set(todos);
  },
  editStart(state, index) {
    state.editedTodo = state.todos[index];
  },
  editEnd(state, title) {
    const {
      editedTodo,
      todos
    } = state;
    if (editedTodo) {
      editedTodo.title = title.trim();
      if (!editedTodo.title) {
        todos.splice(todos.indexOf(editedTodo), 1);
      }
      state.editedTodo = null;
      todoStorage.set(todos);
    }
  },
  editCancel(state) {
    state.editedTodo = null;
  },
  remove({ todos }, index) {
    todos.splice(index, 1);
    todoStorage.set(todos);
  },
  toggleAll({ todos }, completed) {
    todos.forEach(todo => {
      todo.completed = completed;
    });
    todoStorage.set(todos);
  },
  clearCompleted({ todos }) {
    todos
      .map((todo, index) => todo.completed && index)
      .filter(index => index !== false)
      .reverse()
      .map(index => actions.remove({ todos }, index))
      .length;
  },
  switchFilter(state, filter) {
    state.filter = filter;
  }
};

function reducer(state = init, action, args) {
  actions[action] && actions[action](state, ...args);
  return state;
}

const {
  attach,
  connect,
  dispatch
} = createStore(reducer);

const TodoItem = connect((
  { editedTodo },
  todo,
  index
) => html`
  <li
    class="${todo.completed && 'completed'} ${todo == editedTodo && 'editing'}">
    <div class="view">
      <input
        class="toggle"
        type="checkbox"
        ${todo.completed && 'checked'}
        onchange="dispatch('toggle', ${index})"/>
      <label ondblclick="dispatch('editStart', ${index})">
        ${todo.title}
      </label>
      <button
        class="destroy"
        onclick="dispatch('remove', ${index})">
      </button>
    </div>
    <input
      class="edit"
      type="text"
      value="${todo.title}"
      onkeyup="event.keyCode == 13 && dispatch('editEnd', this.value) || event.keyCode == 27 && dispatch('editCancel')"
      onblur="dispatch('editEnd', this.value)"/>
  </li>
`);

const Header = connect(() => html`
  <header class="header">
    <h1>todos</h1>
    <input
      class="new-todo"
      autocomplete="off"
      placeholder="What needs to be done?"
      onkeyup="event.keyCode == 13 && dispatch('add', this.value)"
      autofocus/>
  </header>
`);

const Footer = connect(({
  todos,
  filter,
  filters
}) => html`
  <footer class="footer">
    <span class="todo-count">
      <strong>${
        todos
          .filter(filters.active)
          .length
      }</strong> item${
        todos
          .filter(filters.active)
          .length > 1 && 's'
      } left
    </span>
    <ul class="filters">
      ${
         Object.keys(filters).map(f => html`
          <li>
            <a
              href="###"
              class="${filter == f && 'selected'}"
              onclick="dispatch('switchFilter', '${f}')">
              ${f[0].toUpperCase() + f.substr(1)}
            </a>
          </li>
        `)
      }
    </ul>
    ${
       todos
        .filter(filters.completed)
        .length > 0 && html`
          <button class="clear-completed" onclick="dispatch('clearCompleted')">
            Clear completed
          </button>
        `
    }
  </footer>
`);

const TodoList = connect(({
  todos,
  filters,
  filter
}) => html`
  <section class="main">
    <input
      id="toggle-all"
      class="toggle-all"
      type="checkbox"
      ${todos.every(todo => todo.completed) && 'checked'}
      onchange="dispatch('toggleAll', this.checked)"/>
    <label for="toggle-all"></label>
    <ul class="todo-list">
      ${
        todos
          .filter(filters[filter])
          .map((todo, index) => TodoItem(todo, index))
      }
    </ul>
    ${Footer()}
  </section>
`);

const App = connect(({ todos }) => html`
  <section class="todoapp">
    ${Header()}
    ${todos.length > 0 && TodoList()}
  </section>
`);

window.dispatch = dispatch;

attach(App, document.getElementById('todoapp'));
View Compiled

External CSS

  1. https://unpkg.com/todomvc-common/base.css
  2. https://unpkg.com/todomvc-app-css/index.css

External JavaScript

This Pen doesn't use any external JavaScript resources.