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
Edit: I figured it out ! Thank you for your help @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. ?
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>
See it live on JSFiddle.
P.S. @kirupa looks like there’s a few retroactive badges to hand out with this one too .
2 Likes