This commit is contained in:
dmitry-vsl
2025-04-02 21:25:59 +00:00
parent fc23d1c367
commit 1a30311de4
118 changed files with 398088 additions and 1081 deletions

View File

@@ -1,31 +0,0 @@
import {render} from 'https://unpkg.com/preact?module';
let state, component, root
if(globalThis.leporello) {
// See https://github.com/leporello-js/leporello-js?tab=readme-ov-file#saving-state-between-page-reloads
// Get initial state from Leporello storage
state = globalThis.leporello.storage.get('state')
}
export const createApp = initial => {
/* if state was loaded from Leporello storage then keep it,
* otherwise initialize with initial state */
state = state ?? initial.initialState
component = initial.component
root = initial.root
do_render()
}
export const handler = fn => (...args) => {
state = fn(state, ...args)
if(globalThis.leporello) {
// Save state to Leporello storage to load it after page reload
globalThis.leporello.storage.set('state', state)
}
do_render()
}
export const connect = comp => props => comp(props, state)
const do_render = () => render(component(), root)

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Todos Example</title>
<script type='module'>
if(new URLSearchParams(window.location.search).get('leporello') == null) {
await import('./index.js');
}
</script>
</head>
<body>
</body>
</html>

View File

@@ -1,135 +1,163 @@
/*
Example of TODO HTML5 app built using preact library
Example of a TODO app built using the Preact library
*/
import {h, render} from 'https://unpkg.com/preact?module';
import React from 'https://esm.sh/preact/compat'
import {createApp, handler, connect} from './app.js'
// 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)
// Components
const App = () => (
h('div', null,
h(AddTodo),
h(TodoList),
h(Footer),
)
<div>
<AddTodo />
<TodoList />
<Footer />
</div>
)
const Footer = () => (
h('div', null,
h('span', null, 'Show: '),
h(FilterLink, {filter: 'ALL'}, 'All'),
h(FilterLink, {filter: 'ACTIVE'}, 'Active'),
h(FilterLink, {filter: 'COMPLETED'}, 'Completed'),
)
<div>
<span>Show: </span>
<FilterLink filter="ALL">All</FilterLink>
<FilterLink filter="ACTIVE">Active</FilterLink>
<FilterLink filter="COMPLETED">Completed</FilterLink>
</div>
)
const FilterLink = connect(({filter, children}, state) => {
const FilterLink = connect(({ filter, children }, state) => {
const disabled = state.filter == filter
return h('button', {
onClick: handler(changeFilter.bind(null, filter)),
disabled,
style:{
marginLeft: '4px',
}
}, children)
return (
<button
onClick={handler(changeFilter.bind(null, filter))}
disabled={disabled}
style={{ marginLeft: '4px' }}
>
{children}
</button>
)
})
const TodoList = connect( (_, state) =>
h('ul', null,
visibleTodos(state).map(todo =>
h(Todo, { todo })
)
)
)
const TodoList = connect((_, state) => (
<ul>
{visibleTodos(state).map(todo => (
<Todo key={todo.id} todo={todo} />
))}
</ul>
))
const Todo = ({ onClick, todo }) => (
h('li', {
onClick: handler(toggleTodo.bind(null, todo)),
style: {
textDecoration: todo.completed ? 'line-through' : 'none'
},
}, todo.text)
const Todo = ({ todo }) => (
<li
onClick={handler(toggleTodo.bind(null, todo))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
)
const AddTodo = connect((_, state) => {
return (
h('div', null,
h('form', {
onSubmit: handler(createTodo),
},
h('input', {
value: state.text,
onChange: handler(changeText),
autoFocus: true,
}),
h('button', {type: 'submit'}, 'Add Todo')
)
)
<div>
<form onSubmit={handler(createTodo)}>
<input
value={state.text}
onChange={handler(changeText)}
autoFocus
/>
<button type="submit">Add Todo</button>
</form>
</div>
)
})
// Selectors
// Returns a filtered list of TODOs based on the current filter state
function visibleTodos(state) {
if(state.filter == 'ALL') {
if (state.filter == 'ALL') {
return state.todos
} else if (state.filter == 'ACTIVE') {
return state.todos.filter(t => !t.completed)
} else if(state.filter == 'COMPLETED') {
} else if (state.filter == 'COMPLETED') {
return state.todos.filter(t => t.completed)
} else {
throw new Error('unknown filter')
throw new Error('Unknown filter')
}
}
// Reducers
// Updates the input text state
function changeText(state, e) {
return {...state, text: e.target.value}
return { ...state, text: e.target.value }
}
// Updates the active filter state
function changeFilter(filter, state) {
return {...state, filter}
return { ...state, filter }
}
// Creates a new TODO item if the input text is not empty
function createTodo(state, e) {
e.preventDefault()
if(!state.text.trim()) {
if (!state.text.trim()) {
return state
}
return {
...state,
todos: [...state.todos, {text: state.text}],
todos: [...state.todos, { text: state.text }],
text: '',
}
}
// Toggles the completion state of a TODO item
function toggleTodo(todo, state) {
return {
...state,
todos: state.todos.map(t =>
(t == todo)
? {...todo, completed: !todo.completed}
: t
t == todo ? { ...todo, completed: !todo.completed } : t
)
}
}
createApp({
initialState: {
todos: [],
text: '',
filter: 'ALL',
},
component: App,
root: document.body,
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB