fix(html-examples): tighten email and progress feedback

Require a complete dotted email before inline validation reports success, preserve the typed email after valid revalidation, and update progress via the actual progress element so visible progress is not stuck at 100%.

req: htmx_equivalents/005

req: examples/001

req: test/006
This commit is contained in:
slhx agent
2026-06-25 16:53:12 +02:00
parent 3d1d613eb2
commit 691c6f009a
3 changed files with 55 additions and 13 deletions
+49 -8
View File
@@ -170,6 +170,23 @@ impl hemx::KeyedPartial for ContactCard {
}
}
struct ProgressMeter {
percent: u8,
}
impl Hemplate for ProgressMeter {
fn render_into(&self, out: &mut String) -> Result<(), hemplate::error::HemplateError> {
use std::fmt::Write as _;
write!(
out,
"<progress value=\"{}\" max=\"100\">{}% complete</progress>",
self.percent, self.percent
)
.expect("write to String cannot fail");
Ok(())
}
}
#[derive(Hemplate, Clone)]
struct EditableRow {
id: Id,
@@ -372,6 +389,19 @@ fn lazy_panel_text(loads: usize) -> String {
}
}
fn is_demo_email(email: &str) -> bool {
let Some((local, domain)) = email.split_once('@') else {
return false;
};
!local.is_empty()
&& domain
.split('.')
.filter(|part| !part.is_empty())
.take(2)
.count()
>= 2
}
fn contact_card_view(record: ContactRecord) -> ContactCard {
ContactCard {
id: Id(record.id),
@@ -451,7 +481,7 @@ mod gallery_handlers {
let _ = input.request;
let mut progress = state.progress.lock().unwrap();
*progress = (*progress + 25).min(100);
gallery::progress_meter.set(format!("{}% complete", *progress))
gallery::progress_meter.replace(&ProgressMeter { percent: *progress })
}
#[hemx::handler]
@@ -521,20 +551,20 @@ mod gallery_handlers {
Form(input): Form<ValidateEmail>,
) -> impl IntoEffect {
let email = input.email.trim().to_owned();
if !email.contains('@') {
if !is_demo_email(&email) {
*state.email.lock().unwrap() = email;
*state.email_status.lock().unwrap() = "Email needs an @ sign".into();
*state.email_status.lock().unwrap() = "Email needs a name and dotted domain".into();
return vec![
gallery::validate_email_form.focus("email"),
gallery::validate_email_form.error("email", "Use a real email address"),
gallery::email_status.set("Email needs an @ sign"),
gallery::email_status.set("Email needs a name and dotted domain"),
];
}
*state.email.lock().unwrap() = email.clone();
*state.email_status.lock().unwrap() = format!("{email} is valid");
vec![
gallery::validate_email_form.error("email", ""),
gallery::email_status.set(format!("{email} is valid")),
gallery::validate_email_form.clear(),
]
}
@@ -770,7 +800,7 @@ mod tests {
}
assert!(html.contains("data-hemx-form=\"validate_email\""));
assert!(html.contains("data-hemx-revealed=\"true\""));
assert!(html.contains("data-hemx-interval=\"500ms\""));
assert!(html.contains("data-hemx-handle=\"tick_progress\""));
assert!(html.contains("data-hemx-slot=\"loaded_row\""));
assert!(html.contains("data-hemx-slot=\"infinite_row\""));
assert!(html.contains("data-hemx-slot=\"value_option\""));
@@ -831,8 +861,19 @@ mod tests {
.batch,
);
assert!(invalid.payload_contains("Use a real email address"));
assert!(invalid.payload_contains("Email needs a name and dotted domain"));
assert!(invalid.updates_text(gallery::email_status));
let partial = inspect_batch(
InteractionRequest::from(form(gallery::validate_email, &[("email", "xyz@")]))
.dispatch_async(handlers(state.clone()))
.await
.unwrap()
.batch,
);
assert!(partial.payload_contains("Email needs a name and dotted domain"));
assert!(!partial.payload_contains("xyz@ is valid"));
let valid = inspect_batch(
InteractionRequest::from(form(
gallery::validate_email,
@@ -845,6 +886,7 @@ mod tests {
);
assert!(valid.payload_contains("xyz@example.com is valid"));
assert!(!valid.payload_contains("Use a real email address"));
assert!(!valid.payload_contains("hemx:form-reset"));
assert!(valid.updates_text(gallery::email_status));
let infinite = inspect_batch(
@@ -864,8 +906,7 @@ mod tests {
.unwrap()
.batch,
);
assert!(progress.updates_text(gallery::progress_meter));
assert!(progress.payload_contains("25% complete"));
assert!(progress.payload_contains("<progress value=\"25\" max=\"100\">25% complete</progress>"));
let values = inspect_batch(
InteractionRequest::from(form(gallery::choose_category, &[("category", "numbers")]))
@@ -78,12 +78,13 @@
<section id="progress-bar" data-htmx-example="progress-bar" aria-labelledby="progress-heading">
<h2 id="progress-heading">progress-bar</h2>
<form data-hemx-handle="tick_progress" data-hemx-form="tick_progress" data-hemx-interval="500ms">
<form data-hemx-handle="tick_progress" data-hemx-form="tick_progress">
<input type="hidden" name="request" value="tick">
<button type="submit">Tick progress</button>
</form>
<progress max="100" +value="self.progress">{+ self.progress_label +}</progress>
<p data-hemx-slot="progress_meter">{+ self.progress_label +}</p>
<p data-hemx-slot="progress_meter">
<progress max="100" +value="self.progress">{+ self.progress_label +}</progress>
</p>
</section>
<section id="value-select" data-htmx-example="value-select" aria-labelledby="value-heading">