• HTML

    todo-app.html html

    <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>
    CSS

    todo-app.css css

    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;
    	}
    }
    TS

    todo-app.ts ts

    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()