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:
@@ -11,6 +11,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
||||
- Start from product intent and requirements; inspect code only after the target behavior is clear.
|
||||
- Read `REQUIREMENTS.md` before changing behavior.
|
||||
- If behavior changes, update `REQUIREMENTS.md` in the same change.
|
||||
- If implementation work does not change durable product obligations, acceptance, safety/recovery behavior, or verification duties, say `REQUIREMENT IMPACT: none` in the handoff and why.
|
||||
- Cite relevant requirements in code, tests, or docs as `req: <component>/001`.
|
||||
- Do not add citation-only padding to satisfy tooling; cite only where the requirement constrains the text.
|
||||
- Use requirement tags consistently: stable area tags like `[parser]`, `[auth]`, `[ui]`; temporary planning tags like `[bootstrap]`, `[mvp]`, or `[milestone-1]` only while they are useful.
|
||||
|
||||
@@ -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")]))
|
||||
|
||||
@@ -1047,6 +1047,7 @@ fn __hemx_lower_html(html: &str, table: &[(&str, &str, u32)]) -> ::std::string::
|
||||
"data-hemx-slot" => "data-sid",
|
||||
"data-hemx-handle" => "data-hid",
|
||||
"data-hemx-form" => "data-fid",
|
||||
"data-hemx-revealed" => "data-hemx-revealed",
|
||||
"data-hemx-atom" => "data-aid",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user