<module-todo>
<form action="#">
<form-textbox>
<label for="add-todo">What needs to be done?</label>
<div class="input">
<input id="add-todo" type="text" value="" />
</div>
</form-textbox>
<basic-button class="submit">
<button type="submit" class="constructive" disabled>
Add Todo
</button>
</basic-button>
</form>
<ol filter="all"></ol>
<template>
<li>
<form-checkbox class="todo">
<label>
<input type="checkbox" class="visually-hidden" />
<span class="label"><slot></slot></span>
</label>
</form-checkbox>
<basic-button class="delete">
<button type="button" class="destructive small">Delete</button>
</basic-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>
<form-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>
</form-radiogroup>
<basic-button class="clear-completed">
<button type="button" class="destructive">
<span class="label">Clear Completed</span>
<span class="badge"></span>
</button>
</basic-button>
</footer>
</module-todo>
module-todo {
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) {
module-todo form {
flex-direction: row;
align-items: flex-end;
}
}
import {
type Component,
component,
fromSelector,
on,
pass,
read,
requireDescendant,
setAttribute,
setText,
show,
} from '../../..'
import type { BasicButtonProps } from '../basic-button/basic-button'
import type { FormCheckboxProps } from '../form-checkbox/form-checkbox'
import '../form-textbox/form-textbox'
export type ModuleTodoProps = {
active: HTMLElement[]
completed: HTMLElement[]
}
export default component(
'module-todo',
{
active: fromSelector('form-checkbox:not([checked])'),
completed: fromSelector('form-checkbox[checked]'),
},
(el, { first }) => {
const textbox = requireDescendant(el, 'form-textbox')
const template = requireDescendant(el, 'template')
const list = requireDescendant(el, 'ol')
return [
// Control todo input form
first<Component<BasicButtonProps>>(
'.submit',
pass({
disabled: () => !textbox.length,
}),
),
first(
'form',
on('submit', (e: Event) => {
e.preventDefault()
queueMicrotask(() => {
const value = textbox.value.trim()
if (!value) return
const li = document.importNode(
template.content,
true,
).firstElementChild
if (!(li instanceof HTMLLIElement))
throw new Error(
'Invalid template for list item; expected <li>',
)
li.querySelector('slot')?.replaceWith(value)
list.append(li)
textbox.clear()
})
}),
),
// Control todo list
first(
'ol',
setAttribute('filter', () =>
read(el, 'form-radiogroup', (radiogroup, upgraded) =>
radiogroup && upgraded ? radiogroup.value : 'all',
),
),
on('click', (e: Event) => {
const target = e.target as HTMLElement
if (target.localName === 'button')
target.closest('li')!.remove()
}),
),
// Update count elements
first(
'.count',
setText(() => String(el.active.length)),
),
first(
'.singular',
show(() => el.active.length === 1),
),
first(
'.plural',
show(() => el.active.length > 1),
),
first(
'.remaining',
show(() => !!el.active.length),
),
first(
'.all-done',
show(() => !el.active.length),
),
// Control clear-completed button
first<Component<BasicButtonProps>>(
'.clear-completed',
pass({
disabled: () => !el.completed.length,
badge: () =>
el.completed.length > 0
? String(el.completed.length)
: '',
}),
on('click', () => {
const items = Array.from(el.querySelectorAll('ol li'))
for (let i = items.length - 1; i >= 0; i--) {
const task = items[i].querySelector<
HTMLElement & FormCheckboxProps
>('form-checkbox')
if (task?.checked) items[i].remove()
}
}),
),
]
},
)
declare global {
interface HTMLElementTagNameMap {
'module-todo': Component<ModuleTodoProps>
}
}