Replace the input! macro with an Input builder (#646)

* Replace the input! macro with an Input builder

* Use a BTreeMap instead of an HashMap

Followinf @fdb-hiroshima's advice

* Rename Input::to_html to Input::html

To make clippy happy

* Wrap error messages in red paragraphs
This commit is contained in:
Ana Gelez
2019-08-27 16:50:24 +02:00
committed by Igor Galić
parent 935d331e97
commit 8ab690001d
14 changed files with 297 additions and 146 deletions
+124 -95
View File
@@ -5,7 +5,7 @@ use rocket::http::{Method, Status};
use rocket::request::Request;
use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog;
use std::collections::hash_map::DefaultHasher;
use std::collections::{btree_map::BTreeMap, hash_map::DefaultHasher};
use std::hash::Hasher;
use templates::Html;
@@ -231,11 +231,108 @@ macro_rules! icon {
};
}
macro_rules! input {
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{
use std::borrow::Cow;
use validator::ValidationErrorsKind;
let cat = $catalog;
/// A builder type to generate `<input>` tags in a type-safe way.
///
/// # Example
///
/// This example uses all options, but you don't have to specify everything.
///
/// ```rust
/// # let current_email = "foo@bar.baz";
/// # let catalog = gettext::Catalog::parse("").unwrap();
/// Input::new("mail", "Your email address")
/// .input_type("email")
/// .default(current_email)
/// .optional()
/// .details("We won't use it for advertising.")
/// .set_prop("class", "email-input")
/// .to_html(catalog);
/// ```
pub struct Input {
/// The name of the input (`name` and `id` in HTML).
name: String,
/// The description of this field.
label: String,
/// The `type` of the input (`text`, `email`, `password`, etc).
input_type: String,
/// The default value for this input field.
default: Option<String>,
/// `true` if this field is not required (will add a little badge next to the label).
optional: bool,
/// A small message to display next to the label.
details: Option<String>,
/// Additional HTML properties.
props: BTreeMap<String, String>,
/// The error message to show next to this field.
error: Option<String>,
}
impl Input {
/// Creates a new input with a given name.
pub fn new(name: impl ToString, label: impl ToString) -> Input {
Input {
name: name.to_string(),
label: label.to_string(),
input_type: "text".into(),
default: None,
optional: false,
details: None,
props: BTreeMap::new(),
error: None,
}
}
/// Set the `type` of this input.
pub fn input_type(mut self, t: impl ToString) -> Input {
self.input_type = t.to_string();
self
}
/// Marks this field as optional.
pub fn optional(mut self) -> Input {
self.optional = true;
self
}
/// Fills the input with a default value (useful for edition form, to show the current values).
pub fn default(mut self, val: impl ToString) -> Input {
self.default = Some(val.to_string());
self
}
/// Adds additional information next to the label.
pub fn details(mut self, text: impl ToString) -> Input {
self.details = Some(text.to_string());
self
}
/// Defines an additional HTML property.
///
/// This method can be called multiple times for the same input.
pub fn set_prop(mut self, key: impl ToString, val: impl ToString) -> Input {
self.props.insert(key.to_string(), val.to_string());
self
}
/// Shows an error message
pub fn error(mut self, errs: &validator::ValidationErrors) -> Input {
if let Some(field_errs) = errs.clone().field_errors().get(self.name.as_str()) {
self.error = Some(
field_errs[0]
.message
.clone()
.unwrap_or_default()
.to_string(),
);
}
self
}
/// Returns the HTML markup for this field.
pub fn html(mut self, cat: &Catalog) -> Html<String> {
if !self.optional {
self = self.set_prop("required", true);
}
Html(format!(
r#"
@@ -247,98 +344,30 @@ macro_rules! input {
{error}
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/>
"#,
name = stringify!($name),
label = i18n!(cat, $label),
kind = stringify!($kind),
optional = if $optional {
name = self.name,
label = self.label,
kind = self.input_type,
optional = if self.optional {
format!("<small>{}</small>", i18n!(cat, "Optional"))
} else {
String::new()
},
details = if $details.len() > 0 {
format!("<small>{}</small>", i18n!(cat, $details))
} else {
String::new()
},
error = if let Some(ValidationErrorsKind::Field(errs)) =
$err.errors().get(stringify!($name))
{
format!(
r#"<p class="error" dir="auto">{}</p>"#,
errs[0]
.message
.clone()
.unwrap_or(Cow::from("Unknown error"))
)
} else {
String::new()
},
val = escape(&$form.$name),
props = $props
details = self
.details
.map(|d| format!("<small>{}</small>", d))
.unwrap_or_default(),
error = self
.error
.map(|e| format!(r#"<p class="error" dir="auto">{}</p>"#, e))
.unwrap_or_default(),
val = escape(&self.default.unwrap_or_default()),
props = self
.props
.into_iter()
.fold(String::new(), |mut res, (key, val)| {
res.push_str(&format!("{}=\"{}\" ", key, val));
res
})
))
}};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!(
$catalog,
$name($kind),
$label,
true,
$details,
$form,
$err,
$props
)
};
($catalog:expr, $name:tt (optional $kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!(
$catalog,
$name($kind),
$label,
true,
"",
$form,
$err,
$props
)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {
input!(
$catalog,
$name($kind),
$label,
false,
$details,
$form,
$err,
$props
)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr, $props:expr) => {
input!(
$catalog,
$name($kind),
$label,
false,
"",
$form,
$err,
$props
)
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $form:expr, $err:expr) => {
input!($catalog, $name($kind), $label, false, "", $form, $err, "")
};
($catalog:expr, $name:tt ($kind:tt), $label:expr, $props:expr) => {{
let cat = $catalog;
Html(format!(
r#"
<label for="{name}" dir="auto">{label}</label>
<input type="{kind}" id="{name}" name="{name}" {props} dir="auto"/>
"#,
name = stringify!($name),
label = i18n!(cat, $label),
kind = stringify!($kind),
props = $props
))
}};
}
}