mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
deploy: leporello-js/app@5ce3bdca40
This commit is contained in:
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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 |
Reference in New Issue
Block a user