Add ability to update items using htmx.

This commit is contained in:
Drew Galbraith 2024-12-03 22:05:32 -08:00
parent 381910d462
commit e6ff8adb14
9 changed files with 109 additions and 16 deletions

12
Cargo.lock generated
View File

@ -125,6 +125,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
"http",
@ -172,6 +173,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "backtrace"
version = "0.3.74"

View File

@ -4,14 +4,14 @@ version = "0.1.0"
edition = "2021"
[dependencies]
askama = { version = "0.12.1", features=[ "with-axum" ] }
askama = { version = "0.12.1", features = ["with-axum"] }
askama_axum = "0.4.0"
axum = "0.7"
axum = { version = "0.7.2", features = ["macros"] }
dotenvy = "0.15"
http = "1.1.0"
serde = "1.0"
serde_json = "1.0"
sqlx = { version = "0.7", features=[ "runtime-tokio", "sqlite" ] }
tokio = { version = "1.38", features=[ "full" ] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1.38", features = ["full"] }
tower = "0.4.13"
tower-http = { version = "0.5.2", features=[ "cors", "fs" ] }
tower-http = { version = "0.5.2", features = ["cors", "fs"] }

View File

@ -8,7 +8,6 @@ There are 3 servers to run to enable hot reloading of the frontend
and backend. The server can then be accessed at `localhost:8000` in
the browser.
```bash
$ cargo watch -x run
$ cd frontend/

View File

@ -4,7 +4,7 @@ mod routes;
use axum::http::{StatusCode, Uri};
use axum::response::IntoResponse;
use axum::routing::{delete, get};
use axum::routing::{get, put};
use dotenvy::dotenv;
use http::{HeaderName, Method};
use sqlx::SqlitePool;
@ -35,7 +35,11 @@ async fn main() {
"/tasks",
get(routes::tasks::list).post(routes::tasks::create),
)
.route("/tasks/:task_id", delete(routes::tasks::delete))
.route(
"/tasks/:task_id",
put(routes::tasks::put).delete(routes::tasks::delete),
)
.route("/tasks/:task_id/edit", get(routes::tasks::edit))
.layer(
ServiceBuilder::new().layer(
CorsLayer::new()

View File

@ -22,6 +22,18 @@ impl Task {
.await
}
pub async fn delete(pool: &SqlitePool, id: i64) -> Result<SqliteQueryResult, sqlx::Error> {
sqlx::query!("DELETE FROM task WHERE id = ?1", id)
.execute(pool)
.await
}
pub async fn by_id(pool: &SqlitePool, id: i64) -> Result<Task, sqlx::Error> {
sqlx::query_as!(Task, "SELECT * FROM task WHERE id = ?1", id,)
.fetch_one(pool)
.await
}
pub async fn insert(&self, pool: &SqlitePool) -> Result<Task, sqlx::Error> {
sqlx::query_as!(
Task,
@ -33,8 +45,13 @@ impl Task {
.await
}
pub async fn delete(pool: &SqlitePool, id: i64) -> Result<SqliteQueryResult, sqlx::Error> {
sqlx::query!("DELETE FROM task WHERE id = ?1", id)
pub async fn update(&self, pool: &SqlitePool) -> Result<SqliteQueryResult, sqlx::Error> {
sqlx::query!(
"UPDATE task SET title=?2, description=?3 WHERE id = ?1",
self.id,
self.title,
self.description,
)
.execute(pool)
.await
}

View File

@ -2,6 +2,7 @@ use askama_axum::Template;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Form;
use serde::Deserialize;
use crate::global::FormErrorResponse;
@ -60,3 +61,33 @@ pub async fn delete(state: State<AppState>, Path(id): Path<i64>) -> impl IntoRes
(StatusCode::OK, "")
}
#[derive(Template)]
#[template(path = "partials/tasks/edit.html")]
struct TaskEditTemplate {
pub task: Task,
}
pub async fn edit(state: State<AppState>, Path(id): Path<i64>) -> impl IntoResponse {
TaskEditTemplate {
task: Task::by_id(&state.db_pool, id).await.unwrap(),
}
}
#[derive(Template)]
#[template(path = "partials/tasks/item.html")]
struct TaskItemTemplate {
pub task: Task,
}
pub async fn put(
state: State<AppState>,
Path(id): Path<i64>,
Form(req): Form<NewTask>,
) -> impl IntoResponse {
let mut task = Task::by_id(&state.db_pool, id).await.unwrap();
task.title = req.title;
task.description = req.description;
task.update(&state.db_pool).await.unwrap();
TaskItemTemplate { task }
}

View File

@ -2,7 +2,7 @@
<head>
<meta charset="UTF-8">
<title>Captains Log</title>
<script type="text/javascript" src="/build/main.js"></script>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<link href="/assets/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
@ -22,7 +22,7 @@
</header>
<ul>
{% for task in tasks %}
<li>{{ task.title }}</li>
{% include "partials/tasks/item.html" %}
{% endfor %}
</ul>
</body>

View File

@ -0,0 +1,21 @@
<form hx-target="this", hx-swap="outerHtml">
<div>
<label>Title</label>
<input type="text" name="title" value="{{ task.title }}"> </input>
</div>
<div>
<label>Description</label>
{% if let Some(desc) = task.description %}
<input type="text" name="description" value="{{ desc }}"> </input>
{% else %}
<input type="text" name="description" value=""> </input>
{% endif %}
</div>
<button hx-get="/tasks/{{ task.id }}">
Cancel
</button>
<button hx-put="/tasks/{{ task.id }}">
Submit
</button>
</form>

View File

@ -0,0 +1,9 @@
<li hx-target="this" hx-swap="outerHtml">
{{ task.title }}
{% match task.description %}
{% when Some with (val) %}
-- <span> {{ val }}</span>
{% when None %}
{% endmatch %}
<button hx-get="/tasks/{{task.id}}/edit">Edit</button>
</li>