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.
|
- Start from product intent and requirements; inspect code only after the target behavior is clear.
|
||||||
- Read `REQUIREMENTS.md` before changing behavior.
|
- Read `REQUIREMENTS.md` before changing behavior.
|
||||||
- If behavior changes, update `REQUIREMENTS.md` in the same change.
|
- 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`.
|
- 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.
|
- 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.
|
- 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 {
|
) -> impl IntoEffect {
|
||||||
let _ = input.request;
|
let _ = input.request;
|
||||||
let mut count = state.infinite_count.lock().unwrap();
|
let mut count = state.infinite_count.lock().unwrap();
|
||||||
|
let first_new = *count + 1;
|
||||||
*count += 3;
|
*count += 3;
|
||||||
let rows = loaded_rows(*count)
|
let rows = loaded_rows(*count)
|
||||||
.into_iter()
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut effects = rows;
|
let mut effects = rows;
|
||||||
effects.push(gallery::infinite_status.set(format!("Showing {} rows", *count)));
|
effects.push(gallery::infinite_status.set(format!("Showing {} rows", *count)));
|
||||||
@@ -538,10 +540,12 @@ mod gallery_handlers {
|
|||||||
) -> impl IntoEffect {
|
) -> impl IntoEffect {
|
||||||
let _ = input.request;
|
let _ = input.request;
|
||||||
let mut loaded = state.loaded_count.lock().unwrap();
|
let mut loaded = state.loaded_count.lock().unwrap();
|
||||||
|
let first_new = *loaded + 1;
|
||||||
*loaded += 2;
|
*loaded += 2;
|
||||||
let rows = loaded_rows(*loaded)
|
let rows = loaded_rows(*loaded)
|
||||||
.into_iter()
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut effects = rows;
|
let mut effects = rows;
|
||||||
effects.push(gallery::load_status.set(format!("Showing {} rows", *loaded)));
|
effects.push(gallery::load_status.set(format!("Showing {} rows", *loaded)));
|
||||||
@@ -554,12 +558,22 @@ mod gallery_handlers {
|
|||||||
Form(input): Form<SearchInput>,
|
Form(input): Form<SearchInput>,
|
||||||
) -> impl IntoEffect {
|
) -> impl IntoEffect {
|
||||||
let query = input.query.trim().to_owned();
|
let query = input.query.trim().to_owned();
|
||||||
*state.query.lock().unwrap() = query.clone();
|
let previous_query = {
|
||||||
let results = search_results_for(&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()
|
.into_iter()
|
||||||
.map(|result| gallery::search_result.replace(result))
|
.map(|result| gallery::search_result.remove(result.id.to_string()))
|
||||||
.collect::<Vec<_>>();
|
.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() {
|
effects.push(gallery::search_status.set(if query.is_empty() {
|
||||||
"Showing all results".into()
|
"Showing all results".into()
|
||||||
} else {
|
} 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]
|
#[test]
|
||||||
fn readme_maps_every_htmx_example_slug() {
|
fn readme_maps_every_htmx_example_slug() {
|
||||||
let readme = include_str!("../README.md");
|
let readme = include_str!("../README.md");
|
||||||
@@ -782,6 +807,17 @@ mod tests {
|
|||||||
assert!(lazy.updates_text(gallery::lazy_panel));
|
assert!(lazy.updates_text(gallery::lazy_panel));
|
||||||
assert!(lazy.payload_contains("Lazy content loaded"));
|
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(
|
let invalid = inspect_batch(
|
||||||
InteractionRequest::from(form(gallery::validate_email, &[("email", "bad")]))
|
InteractionRequest::from(form(gallery::validate_email, &[("email", "bad")]))
|
||||||
.dispatch_async(handlers(state.clone()))
|
.dispatch_async(handlers(state.clone()))
|
||||||
@@ -792,6 +828,20 @@ mod tests {
|
|||||||
assert!(invalid.payload_contains("Use a real email address"));
|
assert!(invalid.payload_contains("Use a real email address"));
|
||||||
assert!(invalid.updates_text(gallery::email_status));
|
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(
|
let infinite = inspect_batch(
|
||||||
InteractionRequest::from(form(gallery::infinite_scroll, &[("request", "more")]))
|
InteractionRequest::from(form(gallery::infinite_scroll, &[("request", "more")]))
|
||||||
.dispatch_async(handlers(state.clone()))
|
.dispatch_async(handlers(state.clone()))
|
||||||
@@ -799,11 +849,8 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.batch,
|
.batch,
|
||||||
);
|
);
|
||||||
assert!(infinite.replaces_keyed_html_containing(
|
assert!(infinite.payload_contains("Loaded row 4"));
|
||||||
gallery::infinite_row,
|
assert!(infinite.payload_contains("Loaded row 6"));
|
||||||
"6",
|
|
||||||
"Loaded row 6"
|
|
||||||
));
|
|
||||||
|
|
||||||
let progress = inspect_batch(
|
let progress = inspect_batch(
|
||||||
InteractionRequest::from(form(gallery::tick_progress, &[("request", "tick")]))
|
InteractionRequest::from(form(gallery::tick_progress, &[("request", "tick")]))
|
||||||
@@ -841,7 +888,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.batch,
|
.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(
|
let search = inspect_batch(
|
||||||
InteractionRequest::from(form(gallery::search, &[("query", "ga")]))
|
InteractionRequest::from(form(gallery::search, &[("query", "ga")]))
|
||||||
@@ -850,7 +897,24 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.batch,
|
.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(
|
let delete = inspect_batch(
|
||||||
InteractionRequest::from(form(editable_row::delete_row, &[("id", "1")]))
|
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-slot" => "data-sid",
|
||||||
"data-hemx-handle" => "data-hid",
|
"data-hemx-handle" => "data-hid",
|
||||||
"data-hemx-form" => "data-fid",
|
"data-hemx-form" => "data-fid",
|
||||||
|
"data-hemx-revealed" => "data-hemx-revealed",
|
||||||
"data-hemx-atom" => "data-aid",
|
"data-hemx-atom" => "data-aid",
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user