<div id="todos">
  <form method="post" action="/my-backend-script">
    <input type="text" placeholder="What's next?" />
    <input type="submit" value="Add" />
  </form>
  <pre></pre>
  <ul></ul>
  <label>
    <input type="checkbox" />
    Hide completed tasks
  </label>
</div>
#todos {
  font-family: sans-serif;
  color: #444;
}

/**
 * Todo List Input
 */
#todos form {
  display: flex;
}

#todos form input[type="text"],
#todos form input[type="submit"] {
  padding: 5px 10px;
  border: 1px solid #666;
  outline: none;
}

#todos form input[type="text"] {
  flex: 1;
}

#todos form input[type="text"]:focus {
  border-color: #369;
}

#todos form input[type="submit"] {
  display: none;
  text-transform: uppercase;
}

#todos form input[type="submit"]:active {
  border-color: #369;
  background-color: #369;
  color: #fff;
}

/*
By adding a simple "is-valid" class to the
Todo's FORM - that wraps all the other
components - then we can easily fix the
style to display a "ready to sent" UI.

It's Javascript that implements the logic
to add or remove the "is-valid" class.
*/
#todos form.is-valid input[type="submit"] {
  display: block;
}

#todos form.is-valid input[type="text"] {
  margin-right: 10px;
}



/**
 * Todos List
 */

#todos ul {
  margin: 10px 0 0 0;
  padding: 0;
  border: 1px solid #ddd;
  border-radius: 4px;
}

#todos ul li {
  padding: 5px 10px;
  border-bottom: 1px solid #ddd;
  transition: background-color 500ms ease;
}

#todos ul li:last-child {
  border-bottom: none;
}

#todos ul li:hover {
  background-color: #cde;
}

#todos ul li.is-done {
  font-style: italic;
  text-decoration: line-through;
  font-size: 10px;
  background: #eee;
}

#todos label {
  display: block;
  border: none;
  margin: 10px 0 0 0;
  padding: 0;
  color: #4ad;
  outline: none;
  font-size: 12px;
}
/**
 * App's State
 * As the features grow, we need a more
 * flexible data structure for the
 * app's state. Objects are great!
 */
const state = {
  hideCompleted: false,
  formIsValid: false,
  todos: [
    {
      text: 'buy milk',
      completed: true,
    },
    {
      text: 'clean kitchen',
      completed: false,
    },
    {
      text: 'learn js',
      completed: false,
    },
  ],
}


/**
 * =====================================
 * Hande State Rendering
 * =====================================
 * 
 * This portion of the app has the single
 * responsibility to sync the App's State
 * with the final HTML.
 *
 * You can act on existing nodes by adding
 * or removing classes, changing the inner
 * text or value of UI components.
 *
 * A normal (but not optimal) approach is
 * "destroy/create" so to avoid complex
 * DOM updates.
 *
 * But in the end, it is up to you.
 * Thing is, you don't have to worry with
 * the App's logic here, you just have to
 * be sure that the final HTML reflects the
 * data in "state".
 */

const render = () => {
  resetDOMList()
  renderDOMList()
  renderDOMFormStatus()
  renderDOMHelperMessage()
}

/**
 * A "data -> DOM" rendering approach makes
 * it very easy to apply dynamic filters
 * based on the app's state.
 *
 * This is a great example of a new feature
 * that we could implement WITHOUT changing
 * the main rendering logic :-)
 */
const getVisibleTodos = () =>
  state.todos
    .filter(item => state.hideCompleted
      ? !item.completed
      : true
    )

const resetDOMList = () =>
  document
    .querySelectorAll('#todos ul *')
    .forEach($ => $.remove())

const renderDOMList = () =>
  getVisibleTodos()
    .forEach(renderDOMListItem)
    
const renderDOMListItem = (item) => {
  /**
   * NOTE: item is now an object!
   * - item.text
   * - item.completed
   */
 
  const li = document.createElement('li')
  
  // NOTE: the items's text is not a 
  // property of the object
  const txt = document
    .createTextNode(item.text)
  
  li.appendChild(txt)
  
  // NOTE: you can play easily with 
  // conditional classes applied to the
  // list items
  if (item.completed) {
    li.className = 'is-done'
  }
  
  // NOTE: setting an item as completed is
  // now trivial. Just change the data an
  // trigger a new re-render!
  li.addEventListener('click', () => {
    item.completed = !item.completed
    render()
  })
  
  document
     .querySelector('#todos ul')
     .appendChild(li)
}

const getHelperMessage = () =>
  getVisibleTodos().length
    ? null
    : 'Create your first task!'

const renderDOMHelperMessage = () =>
  document
    .querySelector('#todos pre')
    .innerHTML = getHelperMessage()

const getFormClassName = () =>
  state.formIsValid
    ? 'is-valid'
    : ''

const renderDOMFormStatus = () =>
  document
    .querySelector('#todos form')
    .className = getFormClassName()
      



/**
 * =====================================
 * Handle New Item
 * =====================================
 */

const handleSubmit = (e) => {
  e.preventDefault()
  e.stopPropagation()
  
  // reference the text input from the form
  const textEl = e.target[0]
  
  // NOTE: a simple return can do wonders
  // in order to avoid to nest your code
  // too much.
  //
  // This technicque has a name:
  // return first
  if (!textEl.value) return
  
  // Update App's State
  //
  // NOTE: the new todo item now has to
  // respect the app's data structure and
  // implement some default values
  state.todos.push({
    completed: false,
    text: textEl.value,
  })
  
  // Trigger re-rendering
  render()
  
  // reset input
  textEl.value = ''
  textEl.focus()
}

document
  .querySelector('#todos form')
  .addEventListener('submit', handleSubmit)



/**
 * =====================================
 * Handle Form Validation
 * =====================================
 *
 * Ternary operator (inline if) comes in
 * handy for quick decisions like a 
 * class name!
 */
const handleForm = (e) => {
  // Update App's State
  const len = e.target.value.length
  state.formIsValid = len > 0
  
  // Trigger re-rendering
  render()
}

document
  .querySelector('#todos [type="text"]')
  .addEventListener('keyup', handleForm)



/**
 * =====================================
 * Handle Filter UI
 * =====================================
 */

const handleHide = (e) => {
  // Update App's State
  state.hideCompleted = e
    .target
    .checked
  
  // Trigger re-rendering
  render()
 }

document
  .querySelector('[type="checkbox"]')
  .addEventListener('click', handleHide)



/**
 * Run the first rendering loop
 * kinda "start the app" - kinda
 */
render()
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.