Skip to content

Instantly share code, notes, and snippets.

@thekid
Last active August 26, 2024 18:16
Show Gist options
  • Save thekid/175cc260d5ce6199c2128d48606b87bb to your computer and use it in GitHub Desktop.
Save thekid/175cc260d5ce6199c2128d48606b87bb to your computer and use it in GitHub Desktop.
TODO application (PHP, HTMX, SQLite)
{
"require": {
"xp-forge/handlebars-templates": "^3.1",
"xp-forge/frontend": "^6.3",
"xp-framework/rdbms": "^13.1",
"php": ">=8.0.0",
"ext-sqlite3": "*"
},
"scripts": {
"dev": "xp -supervise web -m develop Todo",
"serve": "xp -supervise web Todo",
"post-update-cmd": "xp bundle"
}
}
{
"dependencies": {
"htmx.org": "^2.0"
},
"bundles": {
"vendor": {"htmx.org": ["dist/htmx.min.js"], "fonts://display=swap": "Lato:wght@300"}
}
}
<?php
use rdbms\{DriverManager, DBConnection};
use web\frontend\{Frontend, AssetsFrom, Handlebars, Get, Post, Delete, Put, View, Param};
use web\{Application, Environment, Handler};
class Todo extends Application {
const PATH= 'todos.db';
/** Returns a database connection */
private function connection(): DBConnection {
return DriverManager::getConnection("sqlite://./{$this->environment->path(self::PATH)->name()}");
}
/** Creates database if it doesn't exist */
public function initialize(): void {
if ($this->environment->path(self::PATH)->exists()) return;
$conn= $this->connection();
$conn->query('create table todo (
id integer primary key,
description text,
completed integer,
created datetime default CURRENT_TIMESTAMP
)');
$conn->insert('into todo (description, completed) values ("Welcome πŸ‘‹", 0)');
}
/** @return [:Handler] */
public function routes() {
$impl= new class($this->connection()) {
public function __construct(private $conn) { }
/** Renders a given TODO */
private function render(int $id, string $view= 'item'): View {
$todo= $this->conn->query('select * from todo where id = %d', $id)->next();
return View::named('todo')->fragment($view)->with($todo);
}
#[Get]
public function list() {
return View::named('todo')->with(['todos' => $this->conn->select('* from todo')]);
}
#[Post('/todos')]
public function create(#[Param] string $description, #[Param] bool $completed= false) {
$this->conn->insert(
'into todo (description, completed) values (%s, %d)',
$description,
$completed
);
return $this->render($this->conn->identity());
}
#[Get('/todos/{id}')]
public function view(int $id) {
return $this->render($id);
}
#[Delete('/todos/{id}')]
public function remove(int $id) {
$this->conn->delete('from todo where id = %d', $id);
return View::empty()->status(202);
}
#[Get('/todos/{id}/edit')]
public function edit(int $id) {
return $this->render($id, 'edit');
}
#[Put('/todos/{id}/completed')]
public function complete(int $id) {
$this->conn->update('todo set completed = 1 where id = %d', $id);
return $this->render($id);
}
#[Put('/todos/{id}/description')]
public function rename(int $id, #[Param] string $description) {
$this->conn->update('todo set description = %s where id = %d', $description, $id);
return $this->render($id);
}
};
return [
'/static' => new AssetsFrom($this->environment->webroot()),
'/' => new Frontend($impl, new Handlebars('.'))
];
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TODOs</title>
<link href="/static/vendor.css" rel="stylesheet">
<link href="https://avatars.githubusercontent.com/u/1721237?s=48&v=4" rel="icon">
<style type="text/css">
* {
font-family: 'Lato', sans-serif;
}
form {
display: flex;
flex-direction: column;
width: max-content;
align-items: flex-start;
gap: 1rem;
}
input[type='text'] {
padding: .25rem;
}
button[type='submit'] {
padding: .25rem .5rem;
}
label {
font-weight: bold;
display: block;
}
th, td {
padding: .5rem;
}
tbody tr:nth-child(odd) {
background-color: #efefef;
}
th {
text-align: left;
}
.editable {
cursor: pointer;
display: block;
}
</style>
</head>
<body {{#with request.values.token}}hx-headers='{"X-CSRF-Token": "{{.}}"}'{{/with}}>
<h1>TODOs 🐝</h1>
<hr>
<form hx-post="/todos" hx-target="#listing" hx-swap="beforeend" hx-on::after-request="this.reset()">
<div>
<label for="description">Description</label>
<input type="text" name="description" size="50">
</div>
<div>
<label for="completed">Completed</label>
<input type="checkbox" name="completed" value="true">
</div>
<button type="submit">Save</button>
</form>
<hr>
<table width="100%">
<thead>
<tr><th width="50%">Description</th><th>Created</th><th>Completed</th><th>Actions</th></tr>
</thead>
<tbody id="listing">
{{#each todos}}
{{#*fragment "item"}}
<tr>
<td>
<span class="editable" hx-get="/todos/{{id}}/edit" hx-target="this" hx-swap="outerHTML">
{{description}}
</span>
</td>
<td>{{created}}</td>
<td>{{#if completed}}βœ…{{/if}}</td>
<td>
{{#unless completed}}
<button hx-put="/todos/{{id}}/completed" hx-target="closest tr" hx-swap="outerHTML">Complete</button>
{{/unless}}
<button hx-delete="/todos/{{id}}" hx-target="closest tr" hx-swap="delete">Delete</button>
</td>
</tr>
{{/fragment}}
{{/each}}
</tbody>
</table>
{{#*inline "edit"}}
<form hx-put="/todos/{{id}}/description" hx-target="closest tr" hx-swap="outerHTML">
<div>
<input type="text" name="description" size="50" value="{{description}}">
<button type="submit">πŸ’Ύ</button>
<button type="cancel" hx-get="/todos/{{id}}">x</button>
</div>
</form>
{{/inline}}
<script src="/static/vendor.js"></script>
</body>
</html>
@thekid
Copy link
Author

thekid commented Nov 18, 2023

@thekid
Copy link
Author

thekid commented Nov 18, 2023

Demo application for xp-forge/frontend#41

$ composer up
Loading composer repositories with package information
# ...
Resolving package versions
  - Resolving npm/htmx.org (^1.9 => 1.9.8)
  - Resolving Lato:wght@300 (fonts)
# ...

$ xp serve
@xp.web.srv.Standalone(HTTP @ peer.ServerSocket(Resource id #139 -> tcp://127.0.0.1:8080))
Serving prod:Todo(static)[] > web.logging.ToConsole
════════════════════════════════════════════════════════════════════════
> Server started: http://localhost:8080 in 0.060 seconds
  Sun, 19 Nov 2023 18:45:24 +0100 - PID 6084; press Ctrl+C to exit

# ...

image

@bugbytes-io
Copy link

@thekid Nice! Really interesting to see a PHP / HTMX example! Thanks for sharing this πŸ™‚

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment