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

View file

@ -5,7 +5,7 @@ use rocket::http::{Method, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::{self, content::Html as HtmlCt, Responder, Response}; use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog; use rocket_i18n::Catalog;
use std::collections::hash_map::DefaultHasher; use std::collections::{btree_map::BTreeMap, hash_map::DefaultHasher};
use std::hash::Hasher; use std::hash::Hasher;
use templates::Html; use templates::Html;
@ -231,11 +231,108 @@ macro_rules! icon {
}; };
} }
macro_rules! input { /// A builder type to generate `<input>` tags in a type-safe way.
($catalog:expr, $name:tt ($kind:tt), $label:expr, $optional:expr, $details:expr, $form:expr, $err:expr, $props:expr) => {{ ///
use std::borrow::Cow; /// # Example
use validator::ValidationErrorsKind; ///
let cat = $catalog; /// 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!( Html(format!(
r#" r#"
@ -247,98 +344,30 @@ macro_rules! input {
{error} {error}
<input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/> <input type="{kind}" id="{name}" name="{name}" value="{val}" {props} dir="auto"/>
"#, "#,
name = stringify!($name), name = self.name,
label = i18n!(cat, $label), label = self.label,
kind = stringify!($kind), kind = self.input_type,
optional = if $optional { optional = if self.optional {
format!("<small>{}</small>", i18n!(cat, "Optional")) format!("<small>{}</small>", i18n!(cat, "Optional"))
} else { } else {
String::new() String::new()
}, },
details = if $details.len() > 0 { details = self
format!("<small>{}</small>", i18n!(cat, $details)) .details
} else { .map(|d| format!("<small>{}</small>", d))
String::new() .unwrap_or_default(),
}, error = self
error = if let Some(ValidationErrorsKind::Field(errs)) = .error
$err.errors().get(stringify!($name)) .map(|e| format!(r#"<p class="error" dir="auto">{}</p>"#, e))
{ .unwrap_or_default(),
format!( val = escape(&self.default.unwrap_or_default()),
r#"<p class="error" dir="auto">{}</p>"#, props = self
errs[0] .props
.message .into_iter()
.clone() .fold(String::new(), |mut res, (key, val)| {
.unwrap_or(Cow::from("Unknown error")) res.push_str(&format!("{}=\"{}\" ", key, val));
) res
} else { })
String::new()
},
val = escape(&$form.$name),
props = $props
)) ))
}}; }
($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
))
}};
} }

View file

@ -19,7 +19,11 @@
<!-- Rocket hack to use various HTTP methods --> <!-- Rocket hack to use various HTTP methods -->
<input type=hidden name="_method" value="put"> <input type=hidden name="_method" value="put">
@input!(ctx.1, title (text), "Title", form, errors.clone(), "minlenght=\"1\"") @(Input::new("title", i18n!(ctx.1, "Title"))
.default(&form.title)
.error(&errors)
.set_prop("minlenght", 1)
.html(ctx.1))
<label for="summary">@i18n!(ctx.1, "Description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label> <label for="summary">@i18n!(ctx.1, "Description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
<textarea id="summary" name="summary" rows="20">@form.summary</textarea> <textarea id="summary" name="summary" rows="20">@form.summary</textarea>

View file

@ -9,7 +9,11 @@
@:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, { @:base(ctx, i18n!(ctx.1, "New Blog"), {}, {}, {
<h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1> <h1 dir="auto">@i18n!(ctx.1, "Create a blog")</h1>
<form method="post" action="@uri!(blogs::create)"> <form method="post" action="@uri!(blogs::create)">
@input!(ctx.1, title (text), "Title", form, errors, "required minlength=\"1\"") @(Input::new("title", i18n!(ctx.1, "Title"))
.default(&form.title)
.error(&errors)
.set_prop("minlength", 1)
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Create blog")" dir="auto"/> <input type="submit" value="@i18n!(ctx.1, "Create blog")" dir="auto"/>
</form> </form>
}) })

View file

@ -17,7 +17,11 @@
]) ])
<form method="post" action="@uri!(instance::update_settings)"> <form method="post" action="@uri!(instance::update_settings)">
@input!(ctx.1, name (text), "Name", form, errors.clone(), "props") @(Input::new("name", i18n!(ctx.1, "Name"))
.default(&form.name)
.error(&errors)
.set_prop("minlength", 1)
.html(ctx.1))
<label for="open_registrations"> <label for="open_registrations">
<input type="checkbox" name="open_registrations" id="open_registrations" @if instance.open_registrations { checked }> <input type="checkbox" name="open_registrations" id="open_registrations" @if instance.open_registrations { checked }>
@ -30,7 +34,11 @@
<label for="long_description">@i18n!(ctx.1, "Long description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label> <label for="long_description">@i18n!(ctx.1, "Long description")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
<textarea id="long_description" name="long_description">@Html(form.long_description)</textarea> <textarea id="long_description" name="long_description">@Html(form.long_description)</textarea>
@input!(ctx.1, default_license (text), "Default article license", form, errors, "minlenght=\"1\"") @(Input::new("default_license", i18n!(ctx.1, "Default article license"))
.default(&form.default_license)
.error(&errors)
.set_prop("minlength", 1)
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Save these settings")"/> <input type="submit" value="@i18n!(ctx.1, "Save these settings")"/>
</form> </form>

View file

@ -7,22 +7,19 @@
@:base(ctx, i18n!(ctx.1, "Media upload"), {}, {}, { @:base(ctx, i18n!(ctx.1, "Media upload"), {}, {}, {
<h1>@i18n!(ctx.1, "Media upload")</h1> <h1>@i18n!(ctx.1, "Media upload")</h1>
<form method="post" enctype="multipart/form-data" action="@uri!(medias::upload)"> <form method="post" enctype="multipart/form-data" action="@uri!(medias::upload)">
<label for="alt"> @(Input::new("alt", i18n!(ctx.1, "Description"))
@i18n!(ctx.1, "Description") .details(i18n!(ctx.1, "Useful for visually impaired people, as well as licensing information"))
<small>@i18n!(ctx.1, "Useful for visually impaired people, as well as licensing information")</small> .set_prop("minlenght", 1)
</label> .html(ctx.1))
<input type="text" id="alt" name="alt" required minlenght="1"/>
<label for="cw"> @(Input::new("cw", i18n!(ctx.1, "Content warning"))
@i18n!(ctx.1, "Content warning") .details(i18n!(ctx.1, "Leave it empty, if none is needed"))
<small>@i18n!(ctx.1, "Leave it empty, if none is needed")</small> .optional()
</label> .html(ctx.1))
<input type="txt" id="cw" name="cw"/>
<label for="file"> @(Input::new("file", i18n!(ctx.1, "File"))
@i18n!(ctx.1, "File") .input_type("file")
</label> .html(ctx.1))
<input type="file" id="file" name="file" required/>
<input type="submit" value="@i18n!(ctx.1, "Send")"/> <input type="submit" value="@i18n!(ctx.1, "Send")"/>
</form> </form>

View file

@ -144,7 +144,11 @@
@if ctx.2.is_some() { @if ctx.2.is_some() {
<form method="post" action="@uri!(comments::create: blog_name = &blog.fqn, slug = &article.slug)"> <form method="post" action="@uri!(comments::create: blog_name = &blog.fqn, slug = &article.slug)">
@input!(ctx.1, warning (optional text), "Content warning", comment_form, comment_errors, "") @(Input::new("warning", i18n!(ctx.1, "Content warning"))
.default(&comment_form.warning)
.error(&comment_errors)
.optional()
.html(ctx.1))
<label for="plume-editor">@i18n!(ctx.1, "Your comment")</label> <label for="plume-editor">@i18n!(ctx.1, "Your comment")</label>
@if let Some(ref prev) = previous_comment { @if let Some(ref prev) = previous_comment {

View file

@ -25,8 +25,15 @@
} else { } else {
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len"> <form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len">
} }
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required") @(Input::new("title", i18n!(ctx.1, "Title"))
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "") .default(&form.title)
.error(&errors)
.html(ctx.1))
@(Input::new("subtitle", i18n!(ctx.1, "Subtitle"))
.default(&form.subtitle)
.error(&errors)
.optional()
.html(ctx.1))
@if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("content") { @if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("content") {
@format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or_else(|| Cow::from("Unknown error"))) @format!(r#"<p class="error">{}</p>"#, errs[0].message.clone().unwrap_or_else(|| Cow::from("Unknown error")))
@ -40,9 +47,17 @@
<a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload media")</a> <a href="@uri!(medias::new)">@i18n!(ctx.1, "Upload media")</a>
</p> </p>
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "") @(Input::new("tags", i18n!(ctx.1, "Tags, separated by commas"))
.default(&form.tags)
@input!(ctx.1, license (optional text), "License", "Leave it empty to reserve all rights", form, errors, "") .error(&errors)
.optional()
.html(ctx.1))
@(Input::new("license", i18n!(ctx.1, "License"))
.default(&form.license)
.error(&errors)
.optional()
.details("Leave it empty to reserve all rights")
.html(ctx.1))
@:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias, form.cover) @:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias, form.cover)

View file

@ -16,15 +16,28 @@
<form method="post" action="/login"> <form method="post" action="/login">
<h2>@i18n!(ctx.1, "I'm from this instance")</h2> <h2>@i18n!(ctx.1, "I'm from this instance")</h2>
<p>@login_msg</p> <p>@login_msg</p>
@input!(ctx.1, email_or_name (text), "Username, or email", login_form, login_errs.clone(), "minlenght=\"1\"") @(Input::new("email_or_name", i18n!(ctx.1, "Username, or email"))
@input!(ctx.1, password (password), "Password", login_form, login_errs, "minlenght=\"1\"") .default(&login_form.email_or_name)
.error(&login_errs)
.set_prop("minlenght", 1)
.html(ctx.1))
@(Input::new("password", i18n!(ctx.1, "Password"))
.default(login_form.password)
.error(&login_errs)
.set_prop("minlength", 1)
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Log in")" /> <input type="submit" value="@i18n!(ctx.1, "Log in")" />
</form> </form>
<form method="post"> <form method="post">
<h2>@i18n!(ctx.1, "I'm from another instance")</h2> <h2>@i18n!(ctx.1, "I'm from another instance")</h2>
<p>@remote_msg</p> <p>@remote_msg</p>
@input!(ctx.1, remote (text), "Username", "Example: user@plu.me", remote_form, remote_errs.clone(), "minlenght=\"1\"") @(Input::new("remote", i18n!(ctx.1, "Username"))
.details("Example: user@plu.me")
.default(&remote_form.remote)
.error(&remote_errs)
.set_prop("minlenght", 1)
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Continue to your instance")"/> <input type="submit" value="@i18n!(ctx.1, "Continue to your instance")"/>
</form> </form>
</div> </div>

View file

@ -6,21 +6,48 @@
@:base(ctx, i18n!(ctx.1, "Search"), {}, {}, { @:base(ctx, i18n!(ctx.1, "Search"), {}, {}, {
<h1>@i18n!(ctx.1, "Search")</h1> <h1>@i18n!(ctx.1, "Search")</h1>
<form method="get" id="form"> <form method="get" id="form">
<input id="q" name="q" placeholder="@i18n!(ctx.1, "Your query")" type="search" style="-webkit-appearance: none;"> @(Input::new("q", "Your query")
.input_type("search")
.set_prop("style", "-webkit-appearance: none;")
.html(ctx.1))
<details> <details>
<summary>@i18n!(ctx.1, "Advanced search")</summary> <summary>@i18n!(ctx.1, "Advanced search")</summary>
@input!(ctx.1, title (text), "Article title matching these words", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Title")))
@input!(ctx.1, subtitle (text), "Subtitle matching these words", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Subtitle - byline")))
@input!(ctx.1, content (text), "Content matching these words", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Body content")))
@input!(ctx.1, after (date), "From this date", &format!("max={}", now))
@input!(ctx.1, before (date), "To this date", &format!("max={}", now))
@input!(ctx.1, tag (text), "Containing these tags", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Tags"))) @(Input::new("title", i18n!(ctx.1, "Article title matching these words"))
@input!(ctx.1, instance (text), "Posted on one of these instances", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Instance domain"))) .set_prop("placeholder", i18n!(ctx.1, "Title"))
@input!(ctx.1, author (text), "Posted by one of these authors", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Author(s)"))) .html(ctx.1))
@input!(ctx.1, blog (text), "Posted on one of these blogs", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Blog title"))) @(Input::new("subtitle", i18n!(ctx.1, "Subtitle matching these words"))
@input!(ctx.1, lang (text), "Written in this language", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Language"))) .set_prop("placeholder", i18n!(ctx.1, "Subtitle"))
@input!(ctx.1, license (text), "Published under this license", &format!("placeholder=\"{}\"", i18n!(ctx.1, "Article license"))) .html(ctx.1))
@(Input::new("content", i18n!(ctx.1, "Content macthing these words"))
.set_prop("placeholder", i18n!(ctx.1, "Body content"))
.html(ctx.1))
@(Input::new("after", i18n!(ctx.1, "From this date"))
.input_type("date")
.set_prop("max", now)
.html(ctx.1))
@(Input::new("before", i18n!(ctx.1, ""))
.input_type("date")
.set_prop("max", now)
.html(ctx.1))
@(Input::new("tag", i18n!(ctx.1, "Containing these tags"))
.set_prop("placeholder", i18n!(ctx.1, "Tags"))
.html(ctx.1))
@(Input::new("instance", i18n!(ctx.1, "Posted on one of these instances"))
.set_prop("placeholder", i18n!(ctx.1, "Instance domain"))
.html(ctx.1))
@(Input::new("author", i18n!(ctx.1, "Posted by one of these authors"))
.set_prop("placeholder", i18n!(ctx.1, "Author(s)"))
.html(ctx.1))
@(Input::new("blog", i18n!(ctx.1, "Posted on one of these blogs"))
.set_prop("placeholder", i18n!(ctx.1, "Blog title"))
.html(ctx.1))
@(Input::new("lang", i18n!(ctx.1, "Written in this language"))
.set_prop("placeholder", i18n!(ctx.1, "Language"))
.html(ctx.1))
@(Input::new("license", i18n!(ctx.1, "Published under this license"))
.set_prop("placeholder", i18n!(ctx.1, "Article license"))
.html(ctx.1))
</details> </details>
<input type="submit" value="@i18n!(ctx.1, "Search")"/> <input type="submit" value="@i18n!(ctx.1, "Search")"/>
</form> </form>

View file

@ -12,8 +12,17 @@
<p>@message</p> <p>@message</p>
} }
<form method="post" action="@uri!(session::create)"> <form method="post" action="@uri!(session::create)">
@input!(ctx.1, email_or_name (text), "Username, or email", form, errors.clone(), "minlenght=\"1\"") @(Input::new("email_or_name", i18n!(ctx.1, "Username, or email"))
@input!(ctx.1, password (password), "Password", form, errors, "minlenght=\"1\"") .default(&form.email_or_name)
.error(&errors)
.set_prop("minlenght", 1)
.html(ctx.1))
@(Input::new("password", i18n!(ctx.1, "Password"))
.default(&form.password)
.error(&errors)
.set_prop("minlenght", 8)
.input_type("password")
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Log in")" /> <input type="submit" value="@i18n!(ctx.1, "Log in")" />
</form> </form>
<a href="@uri!(session::password_reset_request_form)">Forgot your password?</a> <a href="@uri!(session::password_reset_request_form)">Forgot your password?</a>

View file

@ -9,8 +9,18 @@
<h1>@i18n!(ctx.1, "Reset your password")</h1> <h1>@i18n!(ctx.1, "Reset your password")</h1>
<form method="POST"> <form method="POST">
@input!(ctx.1, password (password), "New password", form, errors.clone(), "minlenght=\"8\"") @(Input::new("password", i18n!(ctx.1, "New password"))
@input!(ctx.1, password_confirmation (password), "Confirmation", form, errors.clone(), "minlenght=\"8\"") .default(&form.password)
.error(&errors)
.set_prop("minlenght", 8)
.input_type("password")
.html(ctx.1))
@(Input::new("password_confirmation", i18n!(ctx.1, "Confirmation"))
.default(&form.password_confirmation)
.error(&errors)
.set_prop("minlenght", 8)
.input_type("password")
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Update password")" /> <input type="submit" value="@i18n!(ctx.1, "Update password")" />
</form> </form>
}) })

View file

@ -9,7 +9,12 @@
<h1>@i18n!(ctx.1, "Reset your password")</h1> <h1>@i18n!(ctx.1, "Reset your password")</h1>
<form method="POST"> <form method="POST">
@input!(ctx.1, email (email), "E-mail", form, errors.clone(), "minlenght=\"1\"") @(Input::new("email", i18n!(ctx.1, "Email"))
.default(&form.email)
.error(&errors)
.set_prop("minlenght", 1)
.input_type("email")
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Send password reset link")" /> <input type="submit" value="@i18n!(ctx.1, "Send password reset link")" />
</form> </form>
}) })

View file

@ -18,8 +18,15 @@
<!-- Rocket hack to use various HTTP methods --> <!-- Rocket hack to use various HTTP methods -->
<input type=hidden name="_method" value="put"> <input type=hidden name="_method" value="put">
@input!(ctx.1, display_name (text), "Display name", form, errors.clone()) @(Input::new("display_name", i18n!(ctx.1, ""))
@input!(ctx.1, email (text), "Email", form, errors.clone()) .default(&form.display_name)
.error(&errors)
.html(ctx.1))
@(Input::new("email", i18n!(ctx.1, ""))
.default(&form.email)
.error(&errors)
.input_type("email")
.html(ctx.1))
<label for="summary">@i18n!(ctx.1, "Summary")</label> <label for="summary">@i18n!(ctx.1, "Summary")</label>
<textarea id="summary" name="summary">@form.summary</textarea> <textarea id="summary" name="summary">@form.summary</textarea>

View file

@ -11,13 +11,32 @@
@if enabled { @if enabled {
<h1>@i18n!(ctx.1, "Create an account")</h1> <h1>@i18n!(ctx.1, "Create an account")</h1>
<form method="post" action="@uri!(user::create)"> <form method="post" action="@uri!(user::create)">
@input!(ctx.1, username (text), "Username", form, errors.clone(), "minlenght=\"1\"")
@input!(ctx.1, email (text), "Email", form, errors.clone())
@input!(ctx.1, password (password), "Password", form, errors.clone(), "minlenght=\"8\"")
@if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") { @if let Some(ValidationErrorsKind::Field(errs)) = errors.clone().errors().get("__all__") {
<p class="error">@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))</p> <p class="error">@errs[0].message.as_ref().unwrap_or(&Cow::from("Unknown error"))</p>
} }
@input!(ctx.1, password_confirmation (password), "Password confirmation", form, errors, "minlenght=\"8\"")
@(Input::new("username", i18n!(ctx.1, ""))
.default(&form.username)
.error(&errors)
.set_prop("minlength", 1)
.html(ctx.1))
@(Input::new("email", i18n!(ctx.1, ""))
.default(&form.email)
.error(&errors)
.set_prop("minlength", 1)
.html(ctx.1))
@(Input::new("password", i18n!(ctx.1, ""))
.default(&form.password)
.error(&errors)
.set_prop("minlength", 8)
.input_type("password")
.html(ctx.1))
@(Input::new("password_confirmation", i18n!(ctx.1, ""))
.default(&form.password_confirmation)
.error(&errors)
.set_prop("minlength", 8)
.input_type("password")
.html(ctx.1))
<input type="submit" value="@i18n!(ctx.1, "Create your account")" /> <input type="submit" value="@i18n!(ctx.1, "Create your account")" />
</form> </form>