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 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.
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.
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.
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
Hi Kirupa! Thank you for the awesome tutorial 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.
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:
This addition now works somewhat 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
Thank you for your reply! @senocular If I use the concat here the following error occurs I used âtoStringâ here to get rid of the error, this is very confusing
@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
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 `<${closed ? "/" : ""}${name}${closed ? "" : this.attrs()}>`
}
/**
* 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>