<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>
<span class="label">Add Todo</span>
</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="tertiary destructive small"
aria-label="Delete"
>
<span class="label">✕</span>
</button>
</basic-button>
</li>
</template>
<footer>
<basic-pluralize>
<p class="none">Well done, all done!</p>
<p class="some">
<span class="count"></span>
<span class="one">task</span>
<span class="other">tasks</span>
remaining
</p>
</basic-pluralize>
<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="tertiary 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;
grid-template-areas: "filter filter" "count clear";
align-items: center;
gap: var(--space-m);
margin: 0;
& basic-pluralize {
grid-area: count;
justify-self: start;
& p {
font-size: var(--font-size-s);
margin: 0;
}
}
.split-button {
grid-area: filter;
justify-self: center;
}
.clear-completed {
grid-area: clear;
justify-self: end;
}
}
}
@container (width > 32rem) {
module-todo {
& form {
flex-direction: row;
align-items: flex-end;
}
& footer {
grid-template-columns: 1fr 1fr 1fr;
grid-template-areas: "count filter clear";
}
}
}
import {
type Component,
component,
fromSelector,
on,
pass,
setAttribute,
} from '../../..'
export type ModuleTodoProps = {
readonly active: HTMLElement[]
readonly completed: HTMLElement[]
}
import '../form-textbox/form-textbox'
export default component(
'module-todo',
{
active: fromSelector('form-checkbox:not([checked])'),
completed: fromSelector('form-checkbox[checked]'),
},
(el, { first, useElement }) => {
const textbox = useElement(
'form-textbox',
'Needed to enter a new todo item.',
)
const template = useElement(
'template',
'Needed to define the list item template.',
)
const list = useElement('ol', 'Needed to display the list of todos.')
const filter = useElement('form-radiogroup')
return [
// Control todo input form
first('basic-button.submit', [
pass({ disabled: () => !textbox.length }),
]),
first('form', [
on('submit', ({ event }) => {
event.preventDefault()
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', () => filter?.value || 'all'),
on('click', ({ event }) => {
const target = event.target as HTMLElement
if (target.closest('button')) target.closest('li')!.remove()
}),
]),
// Update count elements
first('basic-pluralize', [pass({ count: () => el.active.length })]),
// Control clear-completed button
first('basic-button.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('form-checkbox')
if (task?.checked) items[i].remove()
}
}),
]),
]
},
)
declare global {
interface HTMLElementTagNameMap {
'module-todo': Component<ModuleTodoProps>
}
}