fix(examples): repair html dynamic partial interactions

Preserve data-hemx-revealed through hemx-build and make html_examples lazy/load-more/infinite/search interactions return server-driven partial updates with regression coverage.

req: convention/005

req: htmx_equivalents/002

req: examples/001
This commit is contained in:
slhx agent
2026-06-25 12:56:43 +02:00
parent 3b14bb57ca
commit acddea21ee
3 changed files with 79 additions and 13 deletions
+77 -13
View File
@@ -498,10 +498,12 @@ mod gallery_handlers {
) -> impl IntoEffect {
let _ = input.request;
let mut count = state.infinite_count.lock().unwrap();
let first_new = *count + 1;
*count += 3;
let rows = loaded_rows(*count)
.into_iter()
.map(|row| gallery::infinite_row.replace(row))
.filter(|row| row.id.0 >= first_new as u64)
.map(|row| gallery::infinite_row.append(row))
.collect::<Vec<_>>();
let mut effects = rows;
effects.push(gallery::infinite_status.set(format!("Showing {} rows", *count)));
@@ -538,10 +540,12 @@ mod gallery_handlers {
) -> impl IntoEffect {
let _ = input.request;
let mut loaded = state.loaded_count.lock().unwrap();
let first_new = *loaded + 1;
*loaded += 2;
let rows = loaded_rows(*loaded)
.into_iter()
.map(|row| gallery::loaded_row.replace(row))
.filter(|row| row.id.0 >= first_new as u64)
.map(|row| gallery::loaded_row.append(row))
.collect::<Vec<_>>();
let mut effects = rows;
effects.push(gallery::load_status.set(format!("Showing {} rows", *loaded)));
@@ -554,12 +558,22 @@ mod gallery_handlers {
Form(input): Form<SearchInput>,
) -> impl IntoEffect {
let query = input.query.trim().to_owned();
*state.query.lock().unwrap() = query.clone();
let results = search_results_for(&query)
let previous_query = {
let mut stored_query = state.query.lock().unwrap();
let previous_query = stored_query.clone();
*stored_query = query.clone();
previous_query
};
let previous_results = search_results_for(&previous_query);
let mut effects = previous_results
.into_iter()
.map(|result| gallery::search_result.replace(result))
.map(|result| gallery::search_result.remove(result.id.to_string()))
.collect::<Vec<_>>();
let mut effects = results;
effects.extend(
search_results_for(&query)
.into_iter()
.map(|result| gallery::search_result.append(result)),
);
effects.push(gallery::search_status.set(if query.is_empty() {
"Showing all results".into()
} else {
@@ -675,6 +689,17 @@ mod tests {
);
}
#[test]
fn revealed_forms_preserve_data_hemx_revealed_attribute() {
// req: convention/005
let state = Arc::new(GalleryState::seeded());
let html = gallery_view(&state).render().expect("render gallery");
assert!(
html.contains("data-hemx-revealed=\"true\""),
"lazy-load and infinite-scroll forms must preserve data-hemx-revealed attribute"
);
}
#[test]
fn readme_maps_every_htmx_example_slug() {
let readme = include_str!("../README.md");
@@ -782,6 +807,17 @@ mod tests {
assert!(lazy.updates_text(gallery::lazy_panel));
assert!(lazy.payload_contains("Lazy content loaded"));
let more = inspect_batch(
InteractionRequest::from(form(gallery::load_more, &[("request", "more")]))
.dispatch_async(handlers(state.clone()))
.await
.unwrap()
.batch,
);
assert!(more.payload_contains("Loaded row 3"));
assert!(more.payload_contains("Loaded row 4"));
assert!(more.updates_text(gallery::load_status));
let invalid = inspect_batch(
InteractionRequest::from(form(gallery::validate_email, &[("email", "bad")]))
.dispatch_async(handlers(state.clone()))
@@ -792,6 +828,20 @@ mod tests {
assert!(invalid.payload_contains("Use a real email address"));
assert!(invalid.updates_text(gallery::email_status));
let valid = inspect_batch(
InteractionRequest::from(form(
gallery::validate_email,
&[("email", "xyz@example.com")],
))
.dispatch_async(handlers(state.clone()))
.await
.unwrap()
.batch,
);
assert!(valid.payload_contains("xyz@example.com is valid"));
assert!(!valid.payload_contains("Use a real email address"));
assert!(valid.updates_text(gallery::email_status));
let infinite = inspect_batch(
InteractionRequest::from(form(gallery::infinite_scroll, &[("request", "more")]))
.dispatch_async(handlers(state.clone()))
@@ -799,11 +849,8 @@ mod tests {
.unwrap()
.batch,
);
assert!(infinite.replaces_keyed_html_containing(
gallery::infinite_row,
"6",
"Loaded row 6"
));
assert!(infinite.payload_contains("Loaded row 4"));
assert!(infinite.payload_contains("Loaded row 6"));
let progress = inspect_batch(
InteractionRequest::from(form(gallery::tick_progress, &[("request", "tick")]))
@@ -841,7 +888,7 @@ mod tests {
.unwrap()
.batch,
);
assert!(load.replaces_keyed_html_containing(gallery::loaded_row, "4", "Loaded row 4"));
assert!(load.payload_contains("Loaded row 6"));
let search = inspect_batch(
InteractionRequest::from(form(gallery::search, &[("query", "ga")]))
@@ -850,7 +897,24 @@ mod tests {
.unwrap()
.batch,
);
assert!(search.replaces_keyed_html_containing(gallery::search_result, "3", "Gamma"));
assert!(search.removes_key(gallery::search_result, "1"));
assert!(search.inserts_html_containing(gallery::search_result, "3", "Gamma"));
let row_save = inspect_batch(
InteractionRequest::from(form(
editable_row::save_row,
&[("id", "1"), ("title", "Write dynamic HTML")],
))
.dispatch_async(handlers(state.clone()))
.await
.unwrap()
.batch,
);
assert!(row_save.replaces_keyed_html_containing(
gallery::editable_row,
"1",
"Write dynamic HTML"
));
let delete = inspect_batch(
InteractionRequest::from(form(editable_row::delete_row, &[("id", "1")]))