Skip to content

Example: To-Do List

example
js
import { compose } from 'paintor'
import { ToDoList } from './components/ToDoList.js'

const initialTasks = [
  { title: 'Make to-do list', completed: true },
  { title: 'Hit the gym', completed: false }
]

compose(
  ToDoList({ initialTasks })
).paint('#to-do-list')
js
import { state, template } from 'paintor'
import { ToDoHeader } from './ToDoHeader.js'
import { ToDoTasks } from './ToDoTasks.js'

const tasks = state([])

export function ToDoList({ initialTasks }) {
  // Push the initial tasks into the state
  for (const initialTask of initialTasks) {
    tasks.push(initialTask)
  }

  return template(() => [
    ToDoHeader({ tasks }),
    ToDoTasks({ tasks }),
  ])
}
js
import { template } from 'paintor'

const newTask = { title: '', completed: false }

function addTask(tasks, newTask) {
  if (!newTask.title) {
    alert('You must write a title for your new task!')
  }
  else {
    tasks.push({ ...newTask })
  }
}

export function ToDoHeader(props) {
  return template((x) => {
    x.h3('Tasks ', x.span(() => `(${props.tasks.length})`)),
      x.div({ class: 'header' },
        x.input(
          {
            type: 'text',
            placeholder: 'New Task...',
            onKeyUp: (event) => {
              newTask.title = event.target.value
            }
          }
        ),
        x.button({
          onClick: () => addTask(props.tasks, newTask) },
          'Add'
        )
      )
  })
}
js
import { template } from 'paintor'
import { ToDoTaskItem } from './ToDoTaskItem.js'

export function ToDoTasks(props) {
  return template((x) => {
    x.ul(
      x.$each(props.tasks, ToDoTaskItem.bind(null, props.tasks))
    )
  })
}
js
import { template } from 'paintor'

function deleteTask(tasks, task) {
  tasks.splice(getTaskIndex(tasks, task), 1)
}

function getTaskIndex(tasks, task) {
  return tasks.indexOf(task)
}

function toggleCompleted(task) {
  task.completed = !task.completed
}

export function ToDoTaskItem(tasks, task) {
  return template((x) => {
    x.li({
        class: () => (task.completed ? 'completed' : ''),
        onClick: () => toggleCompleted(task)
      },
      task.title,
      x.button(
        {
          onClick: () => deleteTask(tasks, task)
        },
        '\u00D7'
      )
    )
  })
}
css
#to-do-list {
  color: black;

  & h3 {
    margin: 0;
    color: deepskyblue;
    font-weight: bold;
  }

  & .header {
    padding: 1rem;
    background: deepskyblue;

    & input {
      float: left;
      width: 75%;
      padding: 0.3rem;
      background: white;
      color: black;

      &::placeholder {
        color: gray
      }
    }

    & button {
      width: 25%;
      padding: 0.3rem;
      background: #d9d9d9;
      text-align: center;
      color: #555;

      &:hover {
        background: #ccc;
      }
    }
  }

  & ul {
    list-style-type: none;
    margin: 0;
    padding: 0;

    & li {
      position: relative;
      user-select: none;
      margin: 0;
      padding: 0.3rem;
      background: #eee;
      cursor: pointer;

      &:nth-child(odd) {
        background: #f9f9f9;
      }

      &:hover {
        background: #ddd;
      }

      & button {
        position: absolute;
        right: 0;
        top: 0;
        padding: 0.3rem 0.6rem;
        background: transparent;
        border: none;
        font-size: larger;

        &:hover {
          background: deepskyblue;
        }
      }

      &.completed {
        background: #888;
        text-decoration: line-through;
      }
    }
  }
}
html
<div id="to-do-list"></div>