A list PAD keeps
Every change shows at once and is posted to the page, which keeps the list in the session and answers it - the server's answer is the list. Reload, and it is still there.
padSession$padExpose
x-for@submit.preventfetch POST
Live
of done · saving ...
- Read the guide about braces
- Hand Alpine its state with {^}
- Let PAD keep the list
Requests to PAD
Nothing yet - use the example above, and every request a component makes to PAD shows here.
How it is built
PAD, on the server Alpine, in the browser
<div class="todos" x-data="todos({^todos})">
<form class="todo-add" @submit.prevent="add()">
<input class="input" x-model="text" placeholder="Something to do ..." aria-label="A new todo" maxlength="80">
<button>Add</button>
</form>
<div class="bar"><span :style="'width:' + progress() + '%'"></span></div>
<p class="muted small"><span x-text="done()"></span> of <span x-text="list.length"></span> done
<span x-show="syncing" x-cloak> · saving ...</span></p>
<ul class="todo-list">
<template x-for="todo in list" :key="todo.id">
<li :class="todo.done && 'is-done'">
<label><input type="checkbox" :checked="todo.done" :disabled="!!todo.pending" @change="toggle(todo)"> <span x-text="todo.text"></span></label>
<button class="btn-icon btn-ghost" @click="remove(todo)" :disabled="!!todo.pending" :aria-label="'Remove ' + todo.text">×</button>
</li>
</template>
</ul>
<button class="btn-sm btn-ghost" @click="clear()" x-show="done() > 0" x-cloak>Clear the done ones</button>
<ul class="todo-list" x-show="false">
{todos}<li>{$text}</li>{/todos}
</ul>
</div>
<script src="{$padRoot}alpine/examples/todos.js"></script><?php
// The list is the session's, started from _data/todos.json. Every change is a post - op=add
// with a text, toggle or remove with an id, clear for the done ones - and the answer is
// the list as it now stands ($padExpose): the component shows a change at once and then
// takes the server's list as the truth.
$todos = padSession ( 'alpineTodos', alpineData ( 'todos' ) );
if ( padRequestIs ( 'POST' ) ) {
$op = alpineText ( 'op' );
$id = (int) padRequest ( 'id', 0 );
$text = trim ( alpineText ( 'text' ) );
if ( $op == 'add' and $text !== '' )
$todos [] = [ 'id' => max ( array_column ( $todos, 'id' ) ?: [ 0 ] ) + 1, 'text' => mb_substr ( $text, 0, 80 ), 'done' => FALSE ];
foreach ( $todos as $at => $todo ) {
if ( $op == 'toggle' and $todo ['id'] == $id ) $todos [$at] ['done'] = ! $todo ['done'];
if ( $op == 'remove' and $todo ['id'] == $id ) unset ( $todos [$at] );
}
if ( $op == 'clear' )
$todos = array_filter ( $todos, fn ( $todo ) => ! $todo ['done'] );
$todos = array_values ( $todos );
padSessionPut ( 'alpineTodos', $todos );
}
$padExpose = [ 'todos' ];
?>// The list: what PAD wrote into the argument - todos({^todos}) - and from then on what PAD
// answers. A change shows at once; the post that follows answers the whole list, which
// replaces the component's own. If the post fails, the server's list stands.
//
// A todo just added has no id of the server's until the answer to its post is in: it shows
// with a key of its own and cannot be ticked or removed yet. It was id 'new' - a toggle or
// remove of it posted id=new, which matched nothing, so the change was lost when the answer
// came, and two quick adds were two items with the key 'new'.
document.addEventListener('alpine:init', () => {
Alpine.data('todos', (first) => ({
list: first,
text: '',
syncing: false,
added: 0,
done() { return this.list.filter(todo => todo.done).length; },
progress() { return this.list.length ? Math.round(this.done() / this.list.length * 100) : 0; },
async sync(body) {
this.syncing = true;
try {
const answer = await pad.post('examples/todos', body);
this.list = answer.todos;
} catch (error) {
const answer = await pad.get('examples/todos');
this.list = answer.todos;
} finally {
this.syncing = false;
}
},
add() {
const text = this.text.trim();
if (!text) return;
this.list.push({ id: 'new-' + ++this.added, text, done: false, pending: true });
this.text = '';
this.sync({ op: 'add', text });
},
toggle(todo) {
if (todo.pending) return;
todo.done = !todo.done;
this.sync({ op: 'toggle', id: todo.id });
},
remove(todo) {
if (todo.pending) return;
this.list = this.list.filter(one => one !== todo);
this.sync({ op: 'remove', id: todo.id });
},
clear() {
this.list = this.list.filter(todo => !todo.done);
this.sync({ op: 'clear' });
}
}));
});