Building an Awesome Todo List App

You are right in that an arrow function would be more concise. There isn’t a right or wrong way, but I think I will revise this content in the future to just use arrow functions.

I wrote a bit about this here: React, Class Syntax, and Autobinding Shenanigans!

@jersoe You might also be interested in reading Arrow Functions in Class Properties Might Not Be As Great As We Think which talks about some of the differences and why using arrow functions isn’t always a good thing.

I tried to redo the Todo List App with functional components today. The one problem that I have is that the component is not refreshed after the submit method (addItem) is called. So the items do not get shown from the TodoItems component. I am not sure what I did wrong or missed.

Hi @ksanders - can you share your code? It’s hard to know what may be going on there :slight_smile:

Yes. This is my version of the TodoList.js using functional components. I have also included the TodoItems.js as reference. It is using a class component as you did in the tutorial. I have not converted it yet.

import React, { useState, useRef } from 'react'
import TodoItems from './TodoItems'

const TodoList = () => {
  const [items, setItems] = useState([])

  const inputElement = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
    const itemArray = items
    if (inputElement.current.value !== '') {
      itemArray.unshift({
        text: inputElement.current.value,
        key: Date.now()
      })

      setItems(itemArray)
      inputElement.current.value = ''
    }

    console.log('handleSubmit', itemArray)
    console.log('items', items)
  }

  return (
    <div className='todoListMain'>
      <div className='header'>
        <form onSubmit={handleSubmit}>
          <input ref={inputElement} placeholder='enter task'></input>
          <input type='submit' value='add'/>
        </form>
      </div>
      <TodoItems entries={items} />
    </div>
  )
}

export default TodoList


import React, { Component } from 'react'
import FlipMove from 'react-flip-move';

class TodoItems extends Component {
  constructor(props) {
    super(props)

    this.createTasks = this.createTasks.bind(this)
  }

  createTasks(item) {
    return <li onClick={() => this.delete(item.key)}
                key={item.key}>{item.text}</li>
  }

  delete(key) {
    this.props.delete(key);
  }

  render() {
    const { entries } = this.props
    console.log('Inside TodoItems', entries)
    var listItems = entries.map(this.createTasks)

      return (
        <ul className='theList'>
          <FlipMove duration={250} easing='ease-out'>
            {listItems}
          </FlipMove>
        </ul>
      )
  }
}

export default TodoItems

Thanks for the code! Does your items array contain an accurate list of the tasks added? If you add a console.log call to createTasks in TodoItems, does it get called for each task added as well?

Yes sir. I just verified that that the items array in the state does have the accurate list each time I add a new task. I added a console.log() in the createTasks call in TodoItems, but it never gets called when I add a task. It is as if the TodoList component is not refreshing on submit.

This is a bit puzzling. Can you replace <TodoItems entries={items} /> with <TodoItems entries={setItems(items)} />

Does that make the example work?

Hello kirupa, why do we need to bind createTasks function, as this function doesnt use any React state?
class TodoItems extends Component {

constructor(props) {

super (props);

this .createTasks = this .createTasks.bind( this );

}
createTasks(item) {

return <li onClick={() => this . delete (item.key)}

key={item.key}>{item.text}</li>

}

Hi @nishaanth_vikram - you are right. If it works without the binding, then it is just an oversight on my part. Something for me to revise in a subsequent edition :slight_smile:

Hi Kirupa! Thank you for the awesome tutorial :slight_smile: I am however having trouble getting the ul list to show. When I type a task and press “add” the entire page goes blank.

" TypeError: todoEntries.map is not a function. (In ‘todoEntries.map(this.createTasks)’, ‘todoEntries.map’ is undefined) " This is the error code I get.


here is my code in TodoItems.js

And my Todo.js

import React, { Component } from "react";
import TodoItems from "./TodoItems";
import "./index.css";

class TODO extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: [],
    };

    this.addItem = this.addItem.bind(this);
    this.deleteItem = this.deleteItem.bind(this);
  }

  addItem(e) {
    if (this._inputElement.value !== "") {
      var newItem = {
        text: this._inputElement,
        key: Date.now(),
      };

      this.setState((prevState) => {
        return {
          items: prevState.items.toString(newItem),
        };
      });
    }
    this._inputElement.value = "";

    console.log(this.state.items);

    e.preventDefault();
  }

  deleteItem(key) {
    var filteredItems = this.state.items.filter(function (item) {
      return item.key !== key;
    });
    this.setState({
      items: filteredItems,
    });
  }

  render() {
    return (
      <div className="todolistMain">
        <div className="header">
          <form onSubmit={this.addItem}>
            <input
              ref={(a) => (this._inputElement = a)}
              placeholder="enter task"
            ></input>
            <button type="submit">add</button>
          </form>
        </div>
        <TodoItems entries={this.state.items} delete={this.deleteItem} />
      </div>
    );
  }
}

export default TODO;

Hi @Sanbu94! Welcome to the forums :slight_smile:

When you do a console.log on this.props.entries in the render() in TodoItems.js, do you see anything printed?

Hi @kirupa , thanks for the quick reply! :slight_smile: I get this in the console :thinking:

This is very strange! The type of this.props.entries is correctly set to be an Array. The error you are seeing typically happens when map is being called on something that isn’t Array-like.

Just for kicks, can you replace the var todoEntries line with this:

var todoEntries = Array.from(this.props.entries);

This seems like a culprit. You’re setting items to a toString of the prevState items. Doing this means its no longer an array, rather a string.

1 Like

This addition now works somewhat :slight_smile: Now after when I press “add” the page does not disappear and this is what is shown at console after one input. But somehow I still don’t get it to show the list of added items :thinking:

Thank you for your reply! :slight_smile: @senocular If I use the concat here the following error occurs :thinking: I used “toString” here to get rid of the error, this is very confusing :smiley:

@kirupa @senocular Could this happen because of the style of my app? I´m practicing a single page application with two other “pages” and I am using react-router-dom :thinking:

Edit: I figured it out ! Thank you for your help :slight_smile: @kirupa @senocular

Now how would you add the possibility to rename tasks on the list and move tasks up or down like drag and drop etc. ? :thinking:

A web component version of a todo list that… renders as it’s own source code!

<script>
/**
 * Base class for todo elements containing shared functionality
 * between both the <todo-list> and <todo-task> elements.
 */
class TodoElement extends HTMLElement {

  /**
   * Generates an HTML string representation of the current
   * element's HTML tag.
   * @param closed - When true generates a closing tag.
   */
  tag(closed = false) {
    const name = this.tagName.toLowerCase()
    return `&lt;${closed ? "/" : ""}${name}${closed ? "" : this.attrs()}&gt;`
  }

  /**
   * Provides the attributes string included in tag()-generated
   * tag HTML. This will be empty by default. Individual
   * components can define their own values by overriding
   * this method.
   */
  attrs() {
    return ""
  }

  /**
   * Adds styles and sets up listeners for add buttons. Add buttons
   * exist both in the todo-list and individual todo-task elements.
   */
  setupAdd() {
    const addBtn = this.shadowRoot.getElementById("add-btn")
    addBtn.addEventListener("click", () => this.onAddClick())
    const style = document.createElement("style")
    
    // Using a ::before pseudo element for the add button text
    // keeps it from being copied when the user selects all the
    // text in the component.
    style.textContent = `
    #add-btn::before{content:"+";color:green;}
    #add-btn{opacity:0.333;}
    #add-btn:hover,#add-btn:focus{opacity:1;}
    `
    this.shadowRoot.appendChild(style)
  }

  /**
   * Add button click handler. When clicked, add buttons generate an
   * event handled by todo-list.
   */
  onAddClick() {
    this.dispatchEvent(new Event("todo-add", {
      bubbles: true
    }))
  }
}

/**
 * Component definition for <todo-list> elements. A todo-list can
 * contain any number of <todo-task> elements that together make up
 * a todo list.
 */
customElements.define("todo-list", class extends TodoElement {

  /**
   * Constructor called when creating new <todo-list> elements.
   */
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: "open" })
    shadow.innerHTML = `
    <style>
    *{font-family:monospace;}
    </style>
    
    <article>
      <div id="open">${this.tag()}</div>
      <slot></slot>
      <div><button id="add-btn"></button></div>
      <div id="close">${this.tag(true)}</div>
    </article>
    `
    this.setupAdd()
    this.addEventListener("todo-add", event => this.onAdd(event))
  }

  /**
   * Add event handler. Adds a new task in a position based on
   * which button triggered the event.
   */
  onAdd({ target }) {
  
    // If the event came from this todo-list element, it came
    // from the list's own add button positioned below the
    // todo-task items so it should be added at the end of
    // the task list (null). Otherwise the button is from a
    // todo-task which means adding before that task.
    const before = target === this ? null : target
    const task = document.createElement("todo-task")
    this.insertBefore(task, before)
    task.done = false
    task.innerText = "TODO"
    
    // When we add a new todo-task we also select the task
    // message. To ensure the selection takes effect, we delay
    // it a single render frame.
    requestAnimationFrame(() => task.selectMessage())
  }
})

/**
 * Component definition for <todo-task> elements. A todo-task
 * represents a single task item within a <todo-list> element.
 * A todo-task can be given a message and be marked as being
 * done (true) or not (false).
 */
customElements.define("todo-task", class extends TodoElement {

  /**
   * Done property mirroring the done attribute. The value
   * can be either the boolean values true or false, though
   * the attribute uses the strings "true" and "false".
   */
  get done() {
    return this.getAttribute("done") === "true"
  }
  set done(value) {
    // Converting the value to boolean first (!!) ensures
    // the String conversion gives us either "true" or "false".
    this.setAttribute("done", String(!!value))
  }

  /**
   * Constructor called when creating new <todo-task> elements.
   */
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: "open" })
    
    // Using a ::before pseudo element for the remove button text
    // keeps it from being copied when the user selects all the
    // text in the component.
    shadow.innerHTML = `
    <style>
    *{font-family:monospace;}
    #remove-btn::before{content:"-";color:red;}
    #remove-btn{opacity:0.333;}
    #remove-btn:hover,#remove-btn:focus{opacity:1;}
    #done-btn{
      background-color:transparent;
      border-color:transparent;
    }
    #done-btn:hover,#done-btn:focus{
      background-color:revert;
      border-color:revert;
    }
    </style>
    
    <div>
    	<button id="add-btn"></button>
    </div>
    <section id="tag"
      ><button id="remove-btn"></button
      > <span id="open">${this.tag()}</span
      ><span id="message" contenteditable><slot></slot></span
      ><span id="close">${this.tag(true)}</span
    ></section>
    `

    this.message = shadow.getElementById("message")
    this.doneBtn = shadow.getElementById("done-btn")
    const removeBtn = shadow.getElementById("remove-btn")
    this.setupAdd()

    // Child content won't always be accessible immediately,
    // but we can get notified with a slotchange event. On a
    // slotchange we know the children have been added and
    // are accessible so we can pull them out and update the
    // rendered text.
    shadow.addEventListener("slotchange", () => this.fetchContent())
    this.doneBtn.addEventListener("click", () => this.onDoneToggle())
    
    // While the done button inherently updates the internal
    // attribute value, we will manually need to copy over
    // changes to the editable rendered text back to the content
    // text of the component since what is editable is part of
    // this component and not the original child content. This
    // ensure anyone checking the contents of the component will
    // find the same content as seen in the rendered text.
    this.message.addEventListener("blur", () => this.onMessageUpdate())
    removeBtn.addEventListener("click", () => this.remove())
  }

  /**
   * The todo-task attributes include a done attribute whose value
   * can be true or false. A button is inserted to allow users
   * to toggle this value when clicked.
   * @override
   */
  attrs() {
    return ` done="<button id="done-btn"></button>"`
  }

  /**
   * Pulls attribute values from the source HTML into the
   * rendered text.
   */
  fetchAttr() {
    this.doneBtn.textContent = String(this.done)
  }

  /**
   * Done attribute toggle event handler. When activated, the done
   * attribute button toggles the done attribute between true and
   * false.
   */
  onDoneToggle() {
    // This updates the source attribute.
    this.done = !this.done
    // This updates the rendered text from that attribute.
    this.fetchAttr()
  }

  /**
   * Pulls child text from the source HTML into the
   * rendered text.
   */
  fetchContent() {
    this.message.textContent = this.textContent
  }

  /**
   * Selects the todo message text to make it easier for users
   * to edit a todo message when a task is first added.
   */
  selectMessage() {
    const range = document.createRange()
    range.setStart(this.message, 0)
    range.setEnd(this.message, 1)
    const sel = window.getSelection()
    sel.removeAllRanges()
    sel.addRange(range)
    this.message.focus()
  }
  
  /**
   * Message update event handler. This updates the source HTML
   * with changes made to the rendered text.
   */
  onMessageUpdate() {
    this.textContent = this.message.textContent
  }

  /**
   * When connected we pull the attribute values to show in
   * the rendered text. Note that the message text is not
   * pulled here since it may not yet be available. That is
   * instead handled by a slotchange event.
   */
  connectedCallback() {
    this.fetchAttr()
  }
})
</script>
<todo-list></todo-list>

See it live on JSFiddle.

P.S. @kirupa looks like there’s a few retroactive badges to hand out with this one too :upside_down_face: .

2 Likes