<todo-app>
<form action="#">
<input-field>
<label for="add-todo">What needs to be done?</label>
<div class="row">
<div class="group auto">
<input id="add-todo" type="text" value="" required>
</div>
</div>
</input-field>
<input-button class="submit">
<button type="submit" class="primary" disabled>Add Todo</button>
</input-button>
</form>
<ol filter="all"></ol>
<template>
<li>
<input-checkbox class="todo">
<label>
<input type="checkbox" class="visually-hidden" />
<span></span>
</label>
</input-checkbox>
<input-button class="delete">
<button type="button">Delete</button>
</input-button>
</li>
</template>
<footer>
<div class="todo-count">
<p class="all-done">Well done, all done!</p>
<p class="remaining">
<span class="count"></span>
<span class="singular">task</span>
<span class="plural">tasks</span>
remaining
</p>
</div>
<input-radiogroup value="all" class="split-button">
<fieldset>
<legend class="visually-hidden">Filter</legend>
<label class="selected">
<input type="radio" class="visually-hidden" name="filter" value="all" checked>
<span>All</span>
</label>
<label>
<input type="radio" class="visually-hidden" name="filter" value="active">
<span>Active</span>
</label>
<label>
<input type="radio" class="visually-hidden" name="filter" value="completed">
<span>Completed</span>
</label>
</fieldset>
</input-radiogroup>
<input-button class="clear-completed">
<button type="button">Clear Completed</button>
</input-button>
</footer>
</todo-app>
todo-app {
display: flex;
flex-direction: column;
gap: var(--space-l);
container-type: inline-size;
& form {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-m);
justify-content: space-between;
}
& ol {
display: flex;
flex-direction: column;
gap: var(--space-m);
list-style: none;
margin: 0;
padding: 0;
& li {
display: flex;
justify-content: space-between;
gap: var(--space-m);
margin: 0;
padding: 0;
}
&[filter="completed"] li:not(:has([checked])) {
display: none;
}
&[filter="active"] li:has([checked]) {
display: none;
}
}
& footer {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
gap: var(--space-m);
margin: 0;
.todo-count {
justify-self: start;
& p {
font-size: var(--font-size-s);
margin: 0;
}
}
.clear-completed {
justify-self: end;
}
}
}
@container (width > 32rem) {
todo-app form {
flex-direction: row;
align-items: flex-end;
}
}
import { UIElement, setAttribute, setProperty, setText } from "../../.."
import type { InputCheckbox } from "../input-checkbox/input-checkbox"
import type { InputField } from "../input-field/input-field"
import type { InputRadiogroup } from "../input-radiogroup/input-radiogroup"
export class TodoApp extends UIElement<{
tasks: InputCheckbox[],
total: number,
completed: number,
active: number,
filter: string,
}> {
static localName = 'todo-app'
init = {
tasks: [],
total: () => this.get('tasks').length,
completed: () => this.get('tasks').filter(el => el.get('checked')).length,
active: () => {
const tasks = this.get('tasks')
return tasks.length - tasks.filter(el => el.get('checked')).length
},
filter: () => (this.querySelector<InputRadiogroup>('input-radiogroup')?.get('value')?? 'all'),
}
connectedCallback() {
super.connectedCallback()
// Set tasks state from the DOM
const updateTasks = () => {
this.set('tasks', this.all<InputCheckbox>('input-checkbox').targets)
}
updateTasks()
// Coordinate new todo form
const input = this.querySelector<InputField>('input-field')
this.first('form').on('submit', (e: Event) => {
e.preventDefault()
// Wait for microtask to ensure the input field value is updated
queueMicrotask(() => {
const value = input?.get('value').toString().trim()
if (value) {
const ol = this.querySelector('ol')
const fragment = this.querySelector('template')
?.content.cloneNode(true) as DocumentFragment
const span = fragment.querySelector('span')
if (ol && fragment && span) {
span.textContent = value
ol.appendChild(fragment)
}
updateTasks()
input?.clear()
}
})
})
// Coordinate .submit button
this.first('.submit').pass({
disabled: () => input?.get('empty') ?? true
})
// Event handler and effect on ol element
this.first('ol')
.sync(setAttribute('filter', 'filter'))
.on('click', (e: Event) => {
const el = e.target as HTMLElement
if (el.localName === 'button') {
el.closest('li')?.remove()
updateTasks()
}
})
// Effects on .todo-count elements
this.first('.count').sync(setText('active'))
this.first('.singular').sync(setProperty('hidden', () => (this.get('active') ?? 0) > 1))
this.first('.plural').sync(setProperty('hidden', () => this.get('active') === 1))
this.first('.remaining').sync(setProperty('hidden', () => !this.get('active')))
this.first('.all-done').sync(setProperty('hidden', () => !!this.get('active')))
// Coordinate .clear-completed button
this.first('.clear-completed')
.on('click', () => {
this.get('tasks')
.filter(el => el.get('checked'))
.forEach(el => el.parentElement?.remove())
updateTasks()
})
.pass({
disabled: () => !this.get('completed')
})
}
}
TodoApp.define()