2023-10-02 03:36:13 +03:00
|
|
|
/*
|
2025-04-02 21:25:59 +00:00
|
|
|
Example of a TODO app built using the Preact library
|
2023-10-02 03:36:13 +03:00
|
|
|
*/
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
import React from 'https://esm.sh/preact/compat'
|
2023-06-19 07:59:21 +03:00
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
// Core
|
|
|
|
|
|
|
|
|
|
// Global application state
|
|
|
|
|
let state
|
|
|
|
|
|
|
|
|
|
// Preserve state (list of TODOs) when editing code
|
|
|
|
|
if (globalThis.leporello) {
|
|
|
|
|
// Retrieve initial state from Leporello storage
|
|
|
|
|
// See: https://github.com/leporello-js/leporello-js?tab=readme-ov-file#saving-state-between-page-reloads
|
|
|
|
|
state = globalThis.leporello.storage.get('state')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Application logic is structured as pure functions with the signature `(state, ...args) => state`.
|
|
|
|
|
This helper function wraps such a function so that its result updates the global state
|
|
|
|
|
and can be used as an event handler.
|
|
|
|
|
*/
|
|
|
|
|
const handler = fn => (...args) => {
|
|
|
|
|
state = fn(state, ...args)
|
|
|
|
|
if (globalThis.leporello) {
|
|
|
|
|
// Persist state to Leporello storage to restore it after page reloads
|
|
|
|
|
globalThis.leporello.storage.set('state', state)
|
|
|
|
|
}
|
|
|
|
|
render()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Higher-order function that injects the current state into a component
|
|
|
|
|
const connect = comp => props => comp(props, state)
|
|
|
|
|
|
|
|
|
|
const render = () => React.render(<App />, document.body)
|
|
|
|
|
|
|
|
|
|
// Initialize application state if not already restored from storage
|
|
|
|
|
if (state == null) {
|
|
|
|
|
state = {
|
|
|
|
|
todos: [],
|
|
|
|
|
text: '',
|
|
|
|
|
filter: 'ALL',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener('load', render)
|
2023-06-19 07:59:21 +03:00
|
|
|
|
|
|
|
|
// Components
|
|
|
|
|
|
|
|
|
|
const App = () => (
|
2025-04-02 21:25:59 +00:00
|
|
|
<div>
|
|
|
|
|
<AddTodo />
|
|
|
|
|
<TodoList />
|
|
|
|
|
<Footer />
|
|
|
|
|
</div>
|
2023-06-19 07:59:21 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const Footer = () => (
|
2025-04-02 21:25:59 +00:00
|
|
|
<div>
|
|
|
|
|
<span>Show: </span>
|
|
|
|
|
<FilterLink filter="ALL">All</FilterLink>
|
|
|
|
|
<FilterLink filter="ACTIVE">Active</FilterLink>
|
|
|
|
|
<FilterLink filter="COMPLETED">Completed</FilterLink>
|
|
|
|
|
</div>
|
2023-06-19 07:59:21 +03:00
|
|
|
)
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
const FilterLink = connect(({ filter, children }, state) => {
|
2023-06-19 07:59:21 +03:00
|
|
|
const disabled = state.filter == filter
|
2025-04-02 21:25:59 +00:00
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handler(changeFilter.bind(null, filter))}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
style={{ marginLeft: '4px' }}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</button>
|
2023-06-19 07:59:21 +03:00
|
|
|
)
|
2025-04-02 21:25:59 +00:00
|
|
|
})
|
2023-06-19 07:59:21 +03:00
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
const TodoList = connect((_, state) => (
|
|
|
|
|
<ul>
|
|
|
|
|
{visibleTodos(state).map(todo => (
|
|
|
|
|
<Todo key={todo.id} todo={todo} />
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
const Todo = ({ todo }) => (
|
|
|
|
|
<li
|
|
|
|
|
onClick={handler(toggleTodo.bind(null, todo))}
|
|
|
|
|
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
|
|
|
|
|
>
|
|
|
|
|
{todo.text}
|
|
|
|
|
</li>
|
2023-06-19 07:59:21 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const AddTodo = connect((_, state) => {
|
|
|
|
|
return (
|
2025-04-02 21:25:59 +00:00
|
|
|
<div>
|
|
|
|
|
<form onSubmit={handler(createTodo)}>
|
|
|
|
|
<input
|
|
|
|
|
value={state.text}
|
|
|
|
|
onChange={handler(changeText)}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
<button type="submit">Add Todo</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
2023-06-19 07:59:21 +03:00
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Selectors
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
// Returns a filtered list of TODOs based on the current filter state
|
2023-06-19 07:59:21 +03:00
|
|
|
function visibleTodos(state) {
|
2025-04-02 21:25:59 +00:00
|
|
|
if (state.filter == 'ALL') {
|
2023-06-19 07:59:21 +03:00
|
|
|
return state.todos
|
|
|
|
|
} else if (state.filter == 'ACTIVE') {
|
|
|
|
|
return state.todos.filter(t => !t.completed)
|
2025-04-02 21:25:59 +00:00
|
|
|
} else if (state.filter == 'COMPLETED') {
|
2023-06-19 07:59:21 +03:00
|
|
|
return state.todos.filter(t => t.completed)
|
|
|
|
|
} else {
|
2025-04-02 21:25:59 +00:00
|
|
|
throw new Error('Unknown filter')
|
2023-06-19 07:59:21 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reducers
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
// Updates the input text state
|
2023-06-19 07:59:21 +03:00
|
|
|
function changeText(state, e) {
|
2025-04-02 21:25:59 +00:00
|
|
|
return { ...state, text: e.target.value }
|
2023-06-19 07:59:21 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
// Updates the active filter state
|
2023-06-19 07:59:21 +03:00
|
|
|
function changeFilter(filter, state) {
|
2025-04-02 21:25:59 +00:00
|
|
|
return { ...state, filter }
|
2023-06-19 07:59:21 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
// Creates a new TODO item if the input text is not empty
|
2023-06-19 07:59:21 +03:00
|
|
|
function createTodo(state, e) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
if (!state.text.trim()) {
|
2023-06-19 07:59:21 +03:00
|
|
|
return state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
2025-04-02 21:25:59 +00:00
|
|
|
todos: [...state.todos, { text: state.text }],
|
2023-06-19 07:59:21 +03:00
|
|
|
text: '',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 21:25:59 +00:00
|
|
|
// Toggles the completion state of a TODO item
|
2023-06-19 07:59:21 +03:00
|
|
|
function toggleTodo(todo, state) {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
todos: state.todos.map(t =>
|
2025-04-02 21:25:59 +00:00
|
|
|
t == todo ? { ...todo, completed: !todo.completed } : t
|
2023-06-19 07:59:21 +03:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|